一. 准备工作
ndk下载:https://developer.android.google.cn/ndk/downloads/
- 首先配置终端的ndk命令:
- 启动终端Terminal
- 输入cd~,进入当前用户的home目录
- 如果没有.bash_profile文件,先输入touch .bash_profile进行创建
- 输入open -e .bash_profile 编辑.bash_profile文件,加入下面语句
- export PATH=${PATH}:“这里填你的ndk安装路径”
- 保存并关闭.bash_profile文件
- 输入source .bash_profile,更新刚刚配置的环境变量
- 命令行输入ndk-build,验证是否配置成功,出现下图表示配置成功
- 另外,Android Studio中也不要忘记设置(一般会默认设置成Android Sdk中的ndk路径,但是自己如果下载了最新的ndk,可以替换),打开File --> Project Structure:
二. JNI文件编写
- 创建一个含有native方法的java文件:
package com.hyf.kaviewer.jni;
/**
* Created by heyf on 2019/2/28
*/
public class JniTest {
public static native void getSecret();
}
- 通过javah命令生成.h头文件
首先需要进入到当前项目当前module的java路径下:
cd /Users/admin/Demo-Project/KAViewer/app/src/main/java
然后输入:
javah -d ../jni com.hyf.kaviewer.jni.JniTest
生成的.h文件如下所示(jni路径也是自动生成)
.h文件的内容为:
红框中的方法与java文件中所写的方法相对应,我们马上也会在.cpp文件中实现它。
- 创建.cpp文件
复制一份.h文件并将后缀改为.cpp
接下来编辑.cpp文件的内容:
具体代码如下:
#include <android/log.h>
#include <string.h>
#include "com_hyf_kaviewer_jni_JniTest.h"
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "security", __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "security", __VA_ARGS__))
static const char *SIGN = "ABCDEFGHIJKLMN";
static const char *SECRET = "1111111111111111111";
static int verifySign(JNIEnv *env);
jstring Java_com_hyf_kaviewer_jni_JniTest_getSecret(JNIEnv *env, jclass type){
return env->NewStringUTF(SECRET);
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
return JNI_ERR;
}
if (verifySign(env) == JNI_OK) {
return JNI_VERSION_1_4;
}
LOGE("签名不一致!");
return JNI_ERR;
}
static jobject getApplication(JNIEnv *env) {
jobject application = NULL;
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
if (activity_thread_clz != NULL) {
jmethodID currentApplication = env->GetStaticMethodID(
activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
if (currentApplication != NULL) {
application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
} else {
LOGE("Cannot find method: currentApplication() in ActivityThread.");
}
env->DeleteLocalRef(activity_thread_clz);
} else {
LOGE("Cannot find class: android.app.ActivityThread");
}
return application;
}
static int verifySign(JNIEnv *env) {
// Application object
jobject application = getApplication(env);
if (application == NULL) {
return JNI_ERR;
}
// Context(ContextWrapper) class
jclass context_clz = env->GetObjectClass(application);
// getPackageManager()
jmethodID getPackageManager = env->GetMethodID(context_clz, "getPackageManager",
"()Landroid/content/pm/PackageManager;");
// android.content.pm.PackageManager object
jobject package_manager = env->CallObjectMethod(application, getPackageManager);
// PackageManager class
jclass package_manager_clz = env->GetObjectClass(package_manager);
// getPackageInfo()
jmethodID getPackageInfo = env->GetMethodID(package_manager_clz, "getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
// context.getPackageName()
jmethodID getPackageName = env->GetMethodID(context_clz, "getPackageName",
"()Ljava/lang/String;");
// call getPackageName() and cast from jobject to jstring
jstring package_name = (jstring) (env->CallObjectMethod(application, getPackageName));
// PackageInfo object
jobject package_info = env->CallObjectMethod(package_manager, getPackageInfo, package_name, 64);
// class PackageInfo
jclass package_info_clz = env->GetObjectClass(package_info);
// field signatures
jfieldID signatures_field = env->GetFieldID(package_info_clz, "signatures",
"[Landroid/content/pm/Signature;");
jobject signatures = env->GetObjectField(package_info, signatures_field);
jobjectArray signatures_array = (jobjectArray) signatures;
jobject signature0 = env->GetObjectArrayElement(signatures_array, 0);
jclass signature_clz = env->GetObjectClass(signature0);
jmethodID toCharsString = env->GetMethodID(signature_clz, "toCharsString",
"()Ljava/lang/String;");
// call toCharsString()
jstring signature_str = (jstring) (env->CallObjectMethod(signature0, toCharsString));
// release
env->DeleteLocalRef(application);
env->DeleteLocalRef(context_clz);
env->DeleteLocalRef(package_manager);
env->DeleteLocalRef(package_manager_clz);
env->DeleteLocalRef(package_name);
env->DeleteLocalRef(package_info);
env->DeleteLocalRef(package_info_clz);
env->DeleteLocalRef(signatures);
env->DeleteLocalRef(signature0);
env->DeleteLocalRef(signature_clz);
const char *sign = env->GetStringUTFChars(signature_str, NULL);
if (sign == NULL) {
LOGE("分配内存失败");
return JNI_ERR;
}
LOGI("应用中读取到的签名为:%s", sign);
LOGI("native中预置的签名为:%s", SIGN);
int result = strcmp(sign, SIGN);
// 使用之后要释放这段内存
env->ReleaseStringUTFChars(signature_str, sign);
env->DeleteLocalRef(signature_str);
if (result == 0) { // 签名一致
return JNI_OK;
}
return JNI_ERR;
}
为什么要在so文件中验证签名?是为了让我们的so库只能被我们想要的应用去调用,因此在so库中会验证预置的签名与当前调用so库的应用的签名是否一致,防止特定so库的滥用,进一步保证安全。
- 获取应用签名
/**
* 展示了如何用Java代码获取签名
*/
private String getSign() {
try {
// 下面几行代码展示如何任意获取Context对象,在jni中也可以使用这种方式
// Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
// Method currentApplication = activityThreadClz.getMethod("currentApplication");
// Application application = (Application) currentApplication.invoke(null);
// PackageManager pm = application.getPackageManager();
// PackageInfo pi = pm.getPackageInfo(application.getPackageName(), PackageManager.GET_SIGNATURES);
PackageManager pm = getPackageManager();
PackageInfo pi = pm.getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = pi.signatures;
Signature signature0 = signatures[0];
String str = signature0.toCharsString();
Log.i(TAG, "getSign: " + signature0.toCharsString());
return signature0.toCharsString();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
并在build.gradle中加入:
signingConfigs {
debug {
storeFile file("../xx.keystore")
storePassword "xxxxxx"
keyAlias "xx"
keyPassword "xxxxxx"
}
}
可以用上述代码获取签名(可以将方法放入启动Activity中,运行后在log中获取签名)
- 在jni目录下创建Android.mk文件
写入以下配置:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS := -lm -llog //这一句是为了支持.cpp文件中的Log
LOCAL_MODULE := security
LOCAL_SRC_FILES := com_hyf_kaviewer_jni_JniTest.cpp
include $(BUILD_SHARED_LIBRARY)
需要注意的是:
- LOCAL_MODULE := security ,security是将要生成的so库的名字,可以按需要改成自己想要的
- LOCAL_SRC_FILES := com_hyf_kaviewer_jni_JniTest.c ,此处需要替换成自己的c文件
- 在jni目录下创建Application.mk文件
写入以下配置:
APP_ABI := all
该配置表示会生成所有主流ABI类型的so库
至此,所需的文件已创建完毕:
- 在当前Module下的build.gradle下增加配置
在 defaultConfig{} 下增加以下配置:
ndk {
moduleName "security"
}
- 最后生成so库
- 在终端中,cd到jni目录下
- 输入ndk-build
- 成功后打印如下所示
- 生成so库的路径如下图
- PS:在使用so库的时候,方法调用处的路径和.h中的路径名一定要一致(即包名和文件名),否则会报错,因为编译器是根据这个路径去调用so库的代码