标签:JNI

安卓安全

JNI开发注意事项集锦

这个是JNI开发必须得注意的,因为JNI开发不像java开发,在java中我们基本上不用去考虑内存释放的问题,因为java的垃圾回收机制会处理这类问题,虽然说JNI层的Local Reference的生命周期持续到一个Native Method的结束,当Native Method返回时Java Heap中的对象不再被持有,也将会被GC回收,但是在JNI中如果我们使用了过多的局部引用,但是未及时释放这些内存,可能会导致出现局部引用表溢出的错误local reference table overflow (max=512),这个错误和安卓中的OOM类似,都是内存不够导致程序crash。之所以会出现局部引用表溢出的错误,是因为当代码从java层进入JNI层的时候虚拟机会创建一个local reference table来存储局部引用,但是这张表的大小是有限制的,一般为512项,当在JNI层创建一个局部引用时就会将其添加到该表中,如果创建的局部引用的个数超过了local reference table的大小就会出现local reference table overflow (max=512)这个错误,因此当我们创建了一个局部引用时在其不使用的地方最好释放掉,尤其是当我们在for/while循环中创建了局部引用的时候,一定要注意局部引用的释放问题。一般下面这些API获取的对象需要调用对应的API释放内存。因为这些API的调用会创建一个局部引用(不是说创建了局部引用就一定要释放掉,而是说这是一种好的习惯,能够避免出现局部引用表溢出的错误) 对象创建: jclass FindClass(JNIEnv *env, const char *name) jclass GetObjectClass(JNIEnv *env, jobject obj) jobject GetObjectField(JNIEnv *env, jobject obj,jfieldID fieldID) jobject CallObjectMethod(JNIEnv *env,jobject obj,jmethodID methodID) 从上述这些API的返回值类型可以看到这些API都会返回一个对象,对于此类API创建的对象统一调用void DeleteLocalRef(JNIEnv *env, jobject localRef)来释放对象 字符串的创建与释放 const char * GetStringUTFChars(JNIEnv *env, jstring string,jboolean *isCopy)对应—>void ReleaseStringUTFChars(JNIEnv *env, jstring string,const char *utf); jstring NewStringUTF(JNIEnv *env, const char *bytes) NewStringUTF调用void DeleteLocalRef(JNIEnv *env, jobject localRef)来释放,因为字符串本身是一种特殊的对象类型 数组系列对象创建与释放 NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env,ArrayType array, jboolean *isCopy)对应—>void Release<PrimitiveType>ArrayElements(JNIEnv *env,ArrayType array, NativeType *elems, jint mode) 调用对应的数组类型释放函数,如GetByteArrayElements则调用ReleaseByteArrayElements,GetIntArrayElements则调用ReleaseIntArrayElements 注意:JNI中的局部引用和C/C++中的局部变量是完全不同的概念,这两者之间存在本质区别。https://www.ibm.com/developerworks/cn/java/j-lo-jnileak/index.html 在JNI中使用GetObjectField函数获取对象很容易出现获取对象失败的情况,因此在使用该函数得到对象后在使用前最好进行下判空操作,像下面这样: [crayon-5f03a7306eb8a342429739/] 在自己创建的线程(类似通过pthread_create)中调用FindClass查找非系统类会失败,返回值为NULL,这是因为通过AttachCurrentThread附加到虚拟机的线程在查找类时只会通过系统类加载器进行查找,不会通过应用类加载器进行查找,因此可以加载系统类,但是不能加载非系统类,如自己在java层定义的类会返回NULL。 因此解决这个问题有2种思路: 在从java代码进入到JNI代码的主线程中先创建一个全局的类对象,这样在任何一个子线程中都可以直接使用 缓存应用类的ClassLoader,这样在子线程中通过缓存的ClassLoader来查找类,而不是通过JNI env来FindClass 第一种思路适合在子线程中查找自定义类较少的情况,这样可以直接在主线程中创建一个全局的引用对象,然后直接在其他线程中使用,从而避免在子线程中调用FindClass,示例代码如下: [crayon-5f03a7306eb91039143805/]   如果在子线程中要使用自定义的Common这个类,通过env->FindClass会查找失败,首先定义一个全局对象g_common,然后在主线程中,如在JNI_OnLoad的逻辑中通过env->FindClass查找到该类,然后通过NewGlobalRef创建一个全局对象g_common,这个全局对象g_common就可以被其他线程直接使用。如果其他线程中需要使用的自定义类较多,则需要创建的全局对象较多,此时不推荐使用这种思路,推荐使用第二种思路。关于第二种思路请参考:https://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=2653579453&idx=1&sn=6fe3cd3002edbfedf56f8fc30bd158c0&chksm=84b3babab3c433ac4b9d5165ff2c1611da76da800b5b4bfbce45f34650c78ff279b551fbbe13&mpshare=1&scene=1&srcid=1130B8HIZIFElNEUfTg1PPnl#rd  

NDK

安卓NDK开发之JNI函数动态注册

在安卓NDK开发之Hello JNI 中相信大家已经注意到,AS向导为我们自动生成的NDK工程的JNI函数中,和其对应的java层函数相比,除了参数会自动增加JNIEnv *和 jobject这2个类型的参数以外,函数的名称也发生了变化,如下所示: [crayon-5f03a7306ef9b379871347/] 首先增加了JNICALL修饰符,其次函数的名称也不再是getString,而是在前面追加了一些其他内容,事实上,如过你多观察几个函数就会发现,前面追加的内容不是随意的,而是符合一定规则的,这个规则就是JNI函数注册命名规则: Java_完整包名_类名_方法名 //其中包名的.分隔符要用下划线分隔符_替代 其实仔细想想也对,当我们在java层调用一个native函数时逻辑就会进入到so中,但是虚拟机如何知道这个java层定义的函数在so中叫什么名称呢?所以肯定需要一套java层函数和c/c++层函数映射的机制,这个机制就是JNI的函数注册机制。上面AS向导生成的JNI代码中已经依据该机制自动为我们生成了.h文件中的函数名称,所以我们没感觉到这套机制的存在。这套机制可以分为静态注册和动态注册2大类。其中AS向导为我们自动生成的.h文件的形式就属于静态注册方式。而实际开发过程中静态注册实际使用的比较少,更多的时候是使用动态注册的方式,下面重点讲解JNI函数的动态注册。 假设java层的类为com.example.MethodRegisterSample,在该类中定义了2个如下的native方法: [crayon-5f03a7306efb4917375620/] 则JNI示例代码如下: [crayon-5f03a7306efb7704311714/] 从上面可以总结出动态注册JNI函数的几大步骤: 在JNI_OnLoad回调函数中通过JavaVM*参数调用GetEnv方法得到JNIEnv对象指针 通过JNIEnv对象指针获取要注册的java方法所在的类的jclass对象 定义JNINativeMethod数组,建立Java层方法和JNI方法的映射关系 通过JNIEnv对象指针调用RegisterNatives方法,传入前面获取的jclass对象和JNINativeMethod数组完成注册

NDK

安卓ndk开发之java JNI技术

在前面安卓NDK开发之Hello JNI中,我们讲解了安卓上的ndk开发的一般流程,初步了解了jni技术。那么ndk和jni到底是什么关系呢?事实上jni和ndk没什么关系,但ndk和jni有关系,准确的说是ndk开发需要用到jni技术。就像水和鱼没关系,但是鱼和水有关系,鱼需要在水中生存。 JNI(Java Native Interface)技术是java本身提供的技术,是用来在java层和c/c++层进行通信的接口,和安卓没啥关系。 NDK(Native Development Kit)是谷歌为了更好的支持安卓开发者进行jni开发而提供的一套开发工具包,通过Android.mk和Application.mk文件来管理编译c/c++代码,然后将其so打包到apk中。 也就是说ndk开发部分的核心是jni技术,ndk编译c/c++代码的核心部分是mk文件的编写。所以我们先了解下ndk开发的用到的jni技术。 java的jni开发官方文档地址:https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html。推荐大家先大概浏览下。 在正式讲解本节内容之前,我们来看一下上一节中利用javah命令自动生成的.h文件中的函数与我们在java层定义的函数之间的关系: [crayon-5f03a7306f2b8675921712/] 可以看到在java层原本无参数的函数对应到c++层却多了两个参数JNIEnv*和jobject,那么这两个参数的作用是什么呢?jobject很容易理解就是java层对应的C++层引用类型,即表示java中的Object类型,当我们在java代码中调用某个native函数时,该类即为该native函数对应到.cpp代码函数的jobject参数。这一点和java代码中类编译为class文件时,函数参数中会自动多一个this指针用来表示调用该函数的类的对象一样,这个很容易理解,因此重点来看下JNIEnv*是个什么东东。 前面提到过javah为我们自动生成的c/c++方法中包含一个jobject类型参数。这个jobject类型就是jni开发中的一种数据结构。就像c/c++中包含基本数据类型int等一样。下面讲解下JNI中的核心数据类型以及java中数据类型在JNI中对应的数据类型的映射关系。 通过前面介绍大家可能发现了在JNI中对象数据类型只提供了jclass,jobject和jstring这3个数据结构,但是java中的对象数据类型虽然只有Object,但是对应的类实在是太多了,比如java.lang这个包下就有很多类。如何用有限的数据类型表示所有的类呢?这就涉及到JNI中的类型描述符了。 类型描述符可以概括为类描述符,基本类型/数组类型/引用类型描述符2大类。 弄清楚了最核心的JNIEnv*的概念以及JNI中的数据类型及其描述,接下来就看看如何使用JNIEnv提供的API接口。前面提到过,首先需要得到该类的字节码对象,然后实例化该对象,接着得到某个方法的方法ID,最终通过该类对象/实例调用该方法(静态方法通过类对象直接调用,非静态方法通过类实例调用)。在JNI中是通过JNIEnv对象的FindClass函数来获取字节码对象的。通过GetMethodID/GetStaticMethodID函数来获取某个方法ID。 那么我们就在前面一节代码的基础上进行改进,此时的主界面包含三个按钮,分别对应c调java中的void函数,c调java中的返回值为int的函数,c调java中函数参数为字符串的函数。此时的MainActivity代码如下: [crayon-5f03a7306f2f6286586583/] 代码很简单,大家应该都能够看得懂,就是三个Button用来响应调用三个不同签名格式的native函数。其中的NDKTest类就是用来定义native函数的类,代码如下: [crayon-5f03a7306f2f8750509479/] 最核心的当然是.cpp代码,在cpp代码中反过来调用java层的代码,调用的原理即是前面讲解的JNI技术,代码如下: [crayon-5f03a7306f2f9830226503/] 代码注释很详细,大家应该能够看懂。然后运行程序,依次点击void,int,参数为String三种情况对应的按钮,程序输出结果如下: 当点击void按钮时首先在MainActivity的java代码中调用了native函数callBackMethod(),然后在该函数中我们通过JNI中的一系列API接口反过来调用了java层的prinString函数,在该函数中将字符串”this string is from java code but it called by native code”赋值给NDKTest类的text成员变量,最终在MainActivity中通过TextView的setText函数将text显示在控件上。这样就完成了Java层和C++层相互调用的过程。 源码下载地址: