Android学习--JNI


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. 数据类型

  1. 基础类型
    在这里插入图片描述
  2. 引用数据类型
    除了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将参数类型和返回值类型作为函数的签名信息。

  1. JNI规范定义的函数签名信息格式:
    (参数1类型字符…)返回值类型字符
  2. 函数签名例子:
    在这里插入图片描述
  3. JNI常用的数据类型及对应字符:
    在这里插入图片描述

二、JNI实现

1.简单实现

创建JNI工程的时候,项目会默认自动创建一个简单的实现,如下:
在这里插入图片描述
在这里插入图片描述

页面显示:
在这里插入图片描述

2.静态注册

按照JNI规范书写函数名:Java_类路径_方法名(路径用下划线分隔)
优缺点:实现方式简单,灵活性差

  1. 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相关的密钥信息

如果需要在本地存储一个密钥串,典型的方式有

  1. 直接写在java source code中
  2. 写在gradle脚本中,使用BuildConfig读取
  3. 写在gradle.properties中,再到gradle脚本中读取,后面同第二点
  4. 使用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

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android JNI学习路线可以按照以下步骤进行: 1. 了解JNI的基本概念和作用:JNI(Java Native Interface)是Java提供的一种机制,用于实现Java与其他编程语言(如C、C++)之间的交互。它允许在Java代码中调用本地代码(Native Code),并且可以在本地代码中调用Java代码。 2. 学习JNI的基本语法和规则:JNI使用一组特定的函数和数据类型来实现Java与本地代码之间的交互。你需要学习如何声明本地方法、如何在Java代码中调用本地方法、如何在本地代码中调用Java方法等。 3. 学习JNI的数据类型映射:JNI提供了一套数据类型映射规则,用于将Java数据类型映射到本地代码中的数据类型。你需要学习如何处理基本数据类型、对象类型、数组类型等。 4. 学习JNI的异常处理:在JNI中,Java代码和本地代码之间的异常处理是非常重要的。你需要学习如何在本地代码中抛出异常、如何在Java代码中捕获异常等。 5. 学习JNI的线程处理:JNI允许在本地代码中创建和操作线程。你需要学习如何创建和销毁线程、如何在线程之间进行通信等。 6. 学习JNI的性能优化:JNI涉及到Java代码和本地代码之间的频繁切换,因此性能优化是非常重要的。你需要学习如何减少JNI调用的次数、如何避免不必要的数据拷贝等。 7. 学习JNI的调试和测试:在开发JNI程序时,调试和测试是非常重要的。你需要学习如何使用调试器调试本地代码、如何进行单元测试等。 8. 学习JNI的进阶主题:一旦掌握了基本的JNI知识,你可以进一步学习JNI的进阶主题,如JNI与Java虚拟机的交互、JNI与动态链接库的交互、JNI与多线程的交互等。 总结起来,Android JNI学习路线包括了基本概念、基本语法、数据类型映射、异常处理、线程处理、性能优化、调试和测试以及进阶主题等内容。通过系统地学习这些知识,你将能够更好地理解和应用JNI技术。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值