文章目录
- JNI(Java Native Interface)
- NDK(Native Development Kit)
- 一、创建一个JNI项目
- 1.创建项目
- 2.C++文件字段说明
- 1. Extern “C”
- 2. JNIEXPORTh和JNICALL
- 3. JNI接口命名规则
- 4. JNIEnv
- 5. jclass和jobject
- 6. 数据类型
- 7.JNI函数签名信息
- 二、JNI实现
- 1.简单实现
- 2.静态注册
- 3.动态注册
- 三、案例:利用JNI存储SDK相关的密钥信息
- 1.静态注册方案
- 2.动态注册方案
- 四、原理解析
- 1. 动态注册原理解析
- 2. 静态注册原理解析
- 3. Java调用Native的流程
- 总结
JNI(Java Native Interface)
提供一种Java字节码调用C/C++的解决方案,JNI描述的是一种技术。
NDK(Native Development Kit)
Android NDK 是一组允许您将 C 或 C++(“原生代码”)嵌入到Android 应用中的工具,NDK描述的是工具集。 能够在 Android 应用中使用原生代码对于想执行以下一项或多项操作的开发者特别有用:
在平台之间移植其应用。
重复使用现有库,或者提供其自己的库供重复使用。
在某些情况下提高性能,特别是像游戏这种计算密集型应用。
一、创建一个JNI项目
1.创建项目
选择NativeC++ 创建项目
创建好的项目如下:
会有java目录和C++的cpp目录
2.C++文件字段说明
在native-lib.cpp文件中,有如下一些字段,详细记录说明下:
1. Extern “C”
作用:避免编绎器按照C++的方式去编绎C函数
1、C不支持函数的重载,编译之后函数名不变;
2、C++支持函数的重载(这点与Java一致),编译之后函数名会改变;
2. JNIEXPORTh和JNICALL
JNIEXPORT :用来表示该函数是否可导出(即:方法的可见性)
JNICALL :用来表示函数的调用规范(如:__stdcall)
1、宏 JNIEXPORT 代表的就是右侧的表达式: __attribute__((visibility ("default")))
2、或者也可以说: JNIEXPORT 是右侧表达式的别名;
3、宏可表达的内容很多,如:一个具体的数值、一个规则、一段逻辑代码等;
3. JNI接口命名规则
Java_<PackageName>_<ClassName>_<MethodName>
JNI Native函数有两种注册方式:
1、静态注册:按照JNI接口规范的命名规则注册;
2、动态注册:在.cpp的JNI_OnLoad方法里注册;
4. JNIEnv
JNIEnv是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境。通过JNIEnv可以调用到一系列JNI系统函数。
每个线程中都有一个 JNIEnv 指针。JNIEnv只在其所在线程有效, 它不能在线程之间进行传递。
通过JNIEnv*就可以对Java端的代码进行操作:
1、创建Java对象
2、调用Java对象的方法
3、获取Java对象的属性等
5. jclass和jobject
jclass :定义native函数的Java类
jobject :定义native函数的Java类的实例
如果native函数是static,参数则代表类class对象(jclass)
如果native函数非static,参数则代表类的实例对象( jobject)
6. 数据类型
- 基础类型
- 引用数据类型
除了Class、String、Throwable和基本数据类型的数组外,其余所有Java对象的数据类型在JNI中都用jobject表示。Java中的String也是引用类型,但是由于使用频率较高,所以在JNI中单独创建了一个jstring类型。
引用类型不能直接在 Native 层使用,需要根据 JNI 函数进行类型的转化后,才能使用;
多维数组(含二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值;
例如,二维整型数组就是指向一位数组的数组,其声明使用方式如下:
//获得一维数组的类引用,即jintArray类型
jclass intArrayClass = env->FindClass("[I");
//构造一个指向jintArray类一维数组的对象数组,该对象数组初始大小为length,类型为 jsize
jobjectArray obejctIntArray = env->NewObjectArray(length ,intArrayClass ,
NULL);
7.JNI函数签名信息
由于Java支持函数重载,因此仅仅根据函数名是没法找到对应的JNI函数。为了解决这个问题,JNI将参数类型和返回值类型作为函数的签名信息。
- JNI规范定义的函数签名信息格式:
(参数1类型字符…)返回值类型字符 - 函数签名例子:
- JNI常用的数据类型及对应字符:
二、JNI实现
1.简单实现
创建JNI工程的时候,项目会默认自动创建一个简单的实现,如下:
页面显示:
2.静态注册
按照JNI规范书写函数名:Java_类路径_方法名(路径用下划线分隔)
优缺点:实现方式简单,灵活性差
- C++调用JAVA方法
java代码
public native void test1();
public void callBack(int code){
Toast.makeText(this, "native层回调 " + code, Toast.LENGTH_SHORT).show();
}
public void callBack(byte[] code){
Toast.makeText(this, "native层回调 " + code, Toast.LENGTH_SHORT).show();
}
//
public String callBack(String code,int code1){
Toast.makeText(this, "native层回调 " + code +" code1:"+code1, Toast.LENGTH_SHORT).show();
return null;
}
C代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnidemo_MainActivity_test1(JNIEnv *env, jobject thiz) {
//获取jclass对象
jclass pJclass = env->GetObjectClass(thiz);
//获取java 方法
jmethodID idcode = env->GetMethodID(pJclass, "callBack", "(I)V");
jmethodID id = env->GetMethodID(pJclass, "callBack", "([B)V");
jmethodID pId = env->GetMethodID(pJclass, "callBack",
"(Ljava/lang/String;I)Ljava/lang/String;");
//反射调用方法
// env->CallVoidMethod(thiz,idcode,200);
jstring pJstring = env->NewStringUTF("200");
env->CallObjectMethod(thiz,pId,pJstring,300);
}
目前执行调取callBack(String code,int code1)方法,执行结果如下:
2) C++修改JAVA成员变量的值
java代码:
String text = "我是JAVA";
public native void test2();
public void test2(View view) {
test2();
binding.sampleText.setText(text);
}
C代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnidemo_MainActivity_test2(JNIEnv *env, jobject thiz) {
jclass pJclass = env->GetObjectClass(thiz);
// 方法签名 String 对象1 基本类型
jfieldID textStringid = env->GetFieldID(pJclass, "text", "Ljava/lang/String;");
//创建C字符串
jstring pJstring = env->NewStringUTF("native 层修改 代码");
//执行修改
env->SetObjectField(thiz,textStringid,pJstring);
}
结果:
3.动态注册
JNI_Onload中指定Java Native函数与C实现函数的对应关系
优缺点:实现方式复杂,灵活性强
java写一个接口,C++动态注册接口方法。
java代码:
interface IAntiDebugCallback {
void beInjectedDebug(String s);
}
MainActivity:
public native void setAntiBiBCallback(IAntiDebugCallback callback);
public void test7(View view) {
setAntiBiBCallback(this);
}
@Override
public void beInjectedDebug(String s) {
binding.sampleText.setText(s);
}
C代码:
void regist(JNIEnv *env, jobject thiz, jobject jCallback) {
LOGD("--动态注册调用成功-->");
jstring pJstring = env->NewStringUTF("动态注册调用成功");
jclass pJclass = env->GetObjectClass(thiz);
jmethodID id = env->GetMethodID(pJclass, "beInjectedDebug", "(Ljava/lang/String;)V");
//执行函数
env->CallVoidMethod(thiz,id,pJstring);
}
jint RegisterNatives(JNIEnv *env) {
//动态反射MainActivity
jclass activityClass = env->FindClass("com/example/jnidemo/MainActivity");
if (activityClass == NULL) {
return JNI_ERR;
}
//注册MainActivity中setAntiBiBCallback方法
JNINativeMethod method_MainActivity [] = {
"setAntiBiBCallback",
"(Lcom/example/jnidemo/IAntiDebugCallback;)V",
(void *)regist
};
//注册 参数1 页面实体类
//参数2 注册的方法
//参数3 注册方法的数量
return env->RegisterNatives(activityClass,method_MainActivity, sizeof(method_MainActivity)/sizeof(method_MainActivity[0]));
}
/**
* 动态注册
* @param vm
* @param reserved
* @return
*/
JNIEXPORT jint JNICALL JNI_OnLoad (JavaVM* vm,void* reserved){
// 手机app 手机开机了
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jint result = RegisterNatives(env);
//如果注册失败,打印日志
if (result != JNI_OK) {
LOGD("--动态注册失败-->");
return -1;
}
return JNI_VERSION_1_6;
}
结果:
三、案例:利用JNI存储SDK相关的密钥信息
如果需要在本地存储一个密钥串,典型的方式有
- 直接写在java source code中
- 写在gradle脚本中,使用BuildConfig读取
- 写在gradle.properties中,再到gradle脚本中读取,后面同第二点
- 使用native方法,读取存放在C/C++中的字段
本质上来讲方式1,2,3没有什么区别。1为硬编码,2可以做到在不同的BuildType使用不同的密钥,3将配置写到脚本之外,方便管理查看。
然而,在项目编译之后,方式1,2,3都会把密钥直接替换到字节码文件中,对于反编译如此方便的Android来说,无疑是将密钥拱手让人。
因此,将密钥放在难以反编译的C/C++代码中,是一个解决的办法。
简单实现:
在类中声明native方法。
public class A {
public native String nativeMethod();
}
C代码实现:
#include <jni.h>
#include <stdio.h>
#include <string.h>
#ifdef __cplusplus
extern "C"{
#endif
jstring Java_[ClassAPackage]_A_nativeMethod(JNIEnv *env,jobject thiz) {
// 返回密钥
return (env)->NewStringUTF("你的密钥");
}
这是简单的代码,如果这么实现的话,别人想破解,找到相关的方法,执行就能拿到密钥,我们可以改进一下,android 应用只有自己有的,别人拿不到的,只有应用签名,我们在native代码里面,先验证一下应用的签名是否是我们的,如果是,才返回正确的密钥。
首先通过java相关的方法,拿到咱们当前项目的签名信息,然后配置到C++文件相关配置方法中。
/**
* 获取签名唯一字符串
*/
public String getSignInfo() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(
getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signs = packageInfo.signatures;
Signature sign = signs[0];
Log.e("----->",sign.toCharsString());
return sign.toCharsString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
然后编写相关代码,有静态和动态注册两种,本质是一样的,可以参考下。
1.静态注册方案
java代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
init();
}
/**
* 初始化,获取应用签名信息,改成动态注册了,这个是静态注册
* @return
*/
public static native boolean init();
/**
* 获取配置的SDK key信息
* @return
*/
public static native String getKey();
public void test5(View view) {
binding.sampleText.setText(getKey());
}
C代码:
const char *APP_PACKAGE_NAME = "com.example.jnidemo";
// 验证是否通过
static jboolean auth = JNI_FALSE;
/*
* 获取全局 Application
*/
jobject getApplicationContext(JNIEnv *env) {
jclass activityThread = env->FindClass("android/app/ActivityThread");
jmethodID currentActivityThread = env->GetStaticMethodID(activityThread, "currentActivityThread", "()Landroid/app/ActivityThread;");
jobject at = env->CallStaticObjectMethod(activityThread, currentActivityThread);
jmethodID getApplication = env->GetMethodID(activityThread, "getApplication", "()Landroid/app/Application;");
return env->CallObjectMethod(at, getApplication);
}
/**
* 通过反射获取签名信息
*/
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_example_jnidemo_MainActivity_init(JNIEnv *env, jclass clazz) {
jclass binderClass = env->FindClass("android/os/Binder");
jclass contextClass = env->FindClass("android/content/Context");
jclass signatureClass = env->FindClass("android/content/pm/Signature");
jclass packageNameClass = env->FindClass("android/content/pm/PackageManager");
jclass packageInfoClass = env->FindClass("android/content/pm/PackageInfo");
jmethodID packageManager = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jmethodID packageName = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
jmethodID toCharsString = env->GetMethodID(signatureClass, "toCharsString", "()Ljava/lang/String;");
jmethodID packageInfo = env->GetMethodID(packageNameClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jmethodID nameForUid = env->GetMethodID(packageNameClass, "getNameForUid", "(I)Ljava/lang/String;");
jmethodID callingUid = env->GetStaticMethodID(binderClass, "getCallingUid", "()I");
jint uid = env->CallStaticIntMethod(binderClass, callingUid);
// 获取全局 Application
jobject context = getApplicationContext(env);
jobject packageManagerObject = env->CallObjectMethod(context, packageManager);
jstring packNameString = (jstring) env->CallObjectMethod(context, packageName);
jobject packageInfoObject = env->CallObjectMethod(packageManagerObject, packageInfo, packNameString, 64);
jfieldID signaturefieldID = env->GetFieldID(packageInfoClass, "signatures", "[Landroid/content/pm/Signature;");
jobjectArray signatureArray = (jobjectArray) env->GetObjectField(packageInfoObject, signaturefieldID);
jobject signatureObject = env->GetObjectArrayElement(signatureArray, 0);
jstring runningPackageName = (jstring) env->CallObjectMethod(packageManagerObject, nameForUid, uid);
if (runningPackageName) {// 正在运行应用的包名
const char *charPackageName = env->GetStringUTFChars(runningPackageName, 0);
if (strcmp(charPackageName, APP_PACKAGE_NAME) != 0) {
return JNI_FALSE;
}
env->ReleaseStringUTFChars(runningPackageName, charPackageName);
} else {
return JNI_FALSE;
}
jstring signatureStr = (jstring) env->CallObjectMethod(signatureObject, toCharsString);
const char *signature = env->GetStringUTFChars(
(jstring) env->CallObjectMethod(signatureObject, toCharsString), NULL);
env->DeleteLocalRef(binderClass);
env->DeleteLocalRef(contextClass);
env->DeleteLocalRef(signatureClass);
env->DeleteLocalRef(packageNameClass);
env->DeleteLocalRef(packageInfoClass);
LOGE("current apk signature %s", signature);
// 应用签名,通过 JNIDecryptKey.getSignature(getApplicationContext())
//TODO 获取,注意开发版和发布版的区别,发布版需要使用正式签名打包后获取
const char *SIGNATURE_KEY = "密钥信息,上面java代码获取到的,配置到这里";
if (strcmp(signature, SIGNATURE_KEY) == 0) {
LOGE("verification passed");
env->ReleaseStringUTFChars(signatureStr, signature);
auth = JNI_TRUE;
return JNI_TRUE;
} else {
LOGE("verification failed");
auth = JNI_FALSE;
return JNI_FALSE;
}
return auth;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_jnidemo_MainActivity_getKey(JNIEnv *env, jclass clazz) {
const char * APP_KEY = "qweqw12312asdasdasda231231asdasdasd";
if (auth) {
return env->NewStringUTF(APP_KEY);
} else {// 你没有权限,验证没有通过。
return env->NewStringUTF("You don't have permission, the verification didn't pass.");
}
}
结果:
2.动态注册方案
java代码:
public native String nativeMethod_key(Context context);
public void test6(View view) {
binding.sampleText.setText(nativeMethod_key(this));
}
C代码:
static jclass contextClass;
static jclass signatureClass;
static jclass packageNameClass;
static jclass packageInfoClass;
/*
根据context对象,获取签名字符串
*/
const char* getSignString(JNIEnv *env,jobject contextObject) {
jmethodID getPackageManagerId = (env)->GetMethodID(contextClass, "getPackageManager","()Landroid/content/pm/PackageManager;");
jmethodID getPackageNameId = (env)->GetMethodID(contextClass, "getPackageName","()Ljava/lang/String;");
jmethodID signToStringId = (env)->GetMethodID(signatureClass, "toCharsString","()Ljava/lang/String;");
jmethodID getPackageInfoId = (env)->GetMethodID(packageNameClass, "getPackageInfo","(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jobject packageManagerObject = (env)->CallObjectMethod(contextObject, getPackageManagerId);
jstring packNameString = (jstring)(env)->CallObjectMethod(contextObject, getPackageNameId);
jobject packageInfoObject = (env)->CallObjectMethod(packageManagerObject, getPackageInfoId,packNameString, 64);
jfieldID signaturefieldID =(env)->GetFieldID(packageInfoClass,"signatures", "[Landroid/content/pm/Signature;");
jobjectArray signatureArray = (jobjectArray)(env)->GetObjectField(packageInfoObject, signaturefieldID);
jobject signatureObject = (env)->GetObjectArrayElement(signatureArray,0);
return (env)->GetStringUTFChars((jstring)(env)->CallObjectMethod(signatureObject, signToStringId),0);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_jnidemo_MainActivity_nativeMethod_1key(JNIEnv *env, jobject thiz,
jobject context) {
const char *RELEASE_SIGN = "密钥信息,上面java代码获取到的,配置到这里";
const char* signStrng = getSignString(env,context);
if(strcmp(signStrng,RELEASE_SIGN)==0)//签名一致 返回合法的 api key,否则返回错误
{
return (env)->NewStringUTF("dongtaizhucemiyao123123123123");
}else
{
return (env)->NewStringUTF("error");
}
}
/**
* 动态注册
* @param vm
* @param reserved
* @return
*/
JNIEXPORT jint JNICALL JNI_OnLoad (JavaVM* vm,void* reserved){
// 手机app 手机开机了
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
//初始化函数
contextClass = (jclass)env->NewGlobalRef((env)->FindClass("android/content/Context"));
signatureClass = (jclass)env->NewGlobalRef((env)->FindClass("android/content/pm/Signature"));
packageNameClass = (jclass)env->NewGlobalRef((env)->FindClass("android/content/pm/PackageManager"));
packageInfoClass = (jclass)env->NewGlobalRef((env)->FindClass("android/content/pm/PackageInfo"));
return JNI_VERSION_1_6;
}
结果:
四、原理解析
1. 动态注册原理解析
动态注册的主要流程
流程3:执行开发人员在JNI_OnLoad中写的注册方法: env->RegisterNatives()
流程4:Android中,Java、Java native函数,在虚拟机中对应的都是一个Method*对象;
设置nativeFunc 指向函数 dvmCallJNIMethod(通常情况下)
设置insns 指向native层的C函数指针 (我们写的C函数)
动态注册的详细流程
Method*属性映射源码
2. 静态注册原理解析
静态注册的主要流程
3. Java调用Native的流程
总结
本文中的项目代码:https://github.com/renbin1990/JNIDemo/tree/master