本人为初学者,文章写得不好,如有错误,请大力怼我
如何使用jni进行开发
本文主要针对Android环境进行NDK\Native\Jni开发进行介绍
使用2.2版本之前的Android Studio进行ndk开发是比较繁琐的,如果你还在使用旧版本的Android Studio,那么建议更新到3.0,现阶段3.0已经比较稳定了(虽然旧项目的gradle升级可能需要折腾一下)。下面介绍旧版本的开发流程只是为了能够更加详细地介绍jni。
jni并不是android框架内的概念,所以也会提及其他环境使用jni开发的方法,基本上大同小异,不过你可能还需要查阅其他文章来处理一些细节问题(如Windows下生成dll文件)
AS 2.2之前的做法
1.编写C/C++
- 首先创建一个java文件,声明一个自定义的native方法,对我们来说,这个方法就是java层到native层的入口,另外,还需要使用静态域将so包加载进来
package com.linjiamin.jnishare;
/**
* Created by Albert on 17/11/16.
*/
public class JniUtil {
static {
System.loadLibrary("sotest");
}
public static native int sum(int num1,int num2);
}
- 开始编写 C/C++代码之前我们需要两个头文件。其中一个是 jni.h,该头文件包含了对jni数据类型和接口的定义(之后还会介绍),现在开始你所编写的所有C/C++代码都需要引入这个头文件。另外你还需要一个根据刚刚编写的native方法签名及类信息生成的头文件。对前者,简单地include进来即可,而对于后者,可以使用javah命令生成,当然你也可以选择亲自编写,使用命令生成的方法如下
//在终端中
cd app/src/main/java
javac com/linjiamin/jnishare/JniUtil.java
javah com.linjiamin.jnishare.JniUtil
//生成的头文件如下
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_linjiamin_jnishare_JniUtil */
#ifndef _Included_com_linjiamin_jnishare_JniUtil
#define _Included_com_linjiamin_jnishare_JniUtil
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_linjiamin_jnishare_JniUtil
* Method: sum
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_linjiamin_jnishare_JniUtil_sum
(JNIEnv *, jclass, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
- 简单说一下如何手写这个头文件,预处理指令的写法都是相同的,将完整类名替换进去即可,对于函数签名,从左到右按以下顺序编写,当然还是使用javah方法生成更好
JNIEXPORT: 在android和linux中是空定义的宏,而在windows下被定义为__declspec(dllexport),具体的作用我们不需要关心
jni数据类型:如jint,jboolean,jstring,它们对应于本地方法的返回类型(int,boolean,String)之后会进一步介绍、
JNICALL : 这是__stdcall等函数调用约定(calling conventions)的宏,这些宏用于提示编译器该函数的参数如何入栈(从左到右,从右到左),以及清理堆栈的方式等
方法名: Java + 完整类名 + 方法名
参数列表:JNIEnv * + jclass\jobject + 所有你定义的参数所对应的jni数据类型 ,JNIEnv*是指向jvm函数表的指针,如果该方法为静态方法则第二个参数为class否则为jobject,它是含有该方法的class对象或实例
注意JNIEXPORT和JNICALL是固定的
- 函数具体实现如下,相信大家都能看懂
#include "jni.h"
#include "com_linjiamin_jnishare_JniUtil.h"
//
// Created by Albert Humbert on 17/11/17.
//
JNIEXPORT jint JNICALL Java_com_linjiamin_jnishare_JniUtil_sum
(JNIEnv * env, jclass obj, jint num1, jint num2){
return num1 + num2;
}
2.使用ndk编译so包
包结构
- 现在在main包下创建一个jni包,将你的头文件和c/c++文件放进去,然后,你还需要两份mk文件,mk文件是makefile文件的一部分,makefile包含c/c++编译器的编译命令、顺序和规则,如果你不了解makefile是什么,那也没什么关系,后面会讲解Android.mk和Application.mk文件的书写规范
- 注意请在Android Library中进行ndk开发,不要使用Java Library,前者会生成aar包,可以包含so以及其他资源文件,后者会生成jar,jar通常只能调用外部so包,网上也有文章将jar当中的so包用文件流写到本地调用的,建议不要尝试这种骚操作
编写Android.mk
- 你可以直接登录 http://android.mk ,查看相关文档
- 一个最基本的Android.mk如下
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libsotest
LOCAL_SRC_FILES := com_linjiamin_jnishare_JniUtil.cpp
include $(BUILD_SHARED_LIBRARY)
- LOCAL_PATH := $(call my-dir),Android.mk必须以该属性开头,它用于指定源文件的路径,my-dir是一个返回Android.mk文件所在目录的宏
- include $(CLEAR_VARS) ,指定负责文件的清理的makefile文件,一般固定这么写就好
- LOCAL_SRC_FILES,需要编译的c/c++文件,如无后缀则默认为cpp文件
- include $(BUILD_SHARED_LIBRARY) ,收集上次清理后的源文件信息,并决定如何编译
- LOCAL_C_INCLUDES,头文件的搜索路径
- TARGET_ARCH,指定AB,如armeabi,armeabi-v7a
编写Application.mk
- 一个典型的Application.mk文件如下
APP_PLATFORM = android-24
APP_ABI := armeabi,armeabi-v7a,x86_64,arm64-v8a
APP_STL := stlport_static
APP_OPTIM := debug
- APP_PLATFORM,ndk版本号,你可以在ndk-bundle文件夹中查看所本地ndk版本
# for mac
/Users/alberthumbert/Library/Android/sdk/ndk-bundle/platforms
- APP_ABI,指定APP_ABI版本,这会决定ndk编译出的so包数量,关于ABI的介绍见下文,推荐至少包含armeabi或armeabi-v7a
- APP_STL 如何连接c++标准库 ,包括 stlport_static ,stlport_shared ,system,分别表示静态,动态,系统默认
- APP_OPTIM,包括debug,和release,这会决定so中是否包含调试信息
- APP_MODULES,填写so包的名字,如果没有这个属性,则按照Android.mk中的进行命名,注意如果文件中含有多个该属性,则会按照先后顺序为你编译出来的so文件命名
- 填写完这两个mk文件之后,需要在gradle中指定so库的路径,gradle会自动将so文件打包进来,在andorid闭包中添加
sourceSets.main {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = []
}
- 如无意外这个时候我们的项目就可以运行起来了,打印log如下
11-17 16:33:37.563 3824-3824/? D/JniUtil: test: 2
人生苦短,我用AS 3.0
自动生成函数
- 如果出于不幸、粗心、经验不足等原因,你的项目无法运行,请不要怀疑你的智商,下面为你带来傻瓜式的ndk开发流程
- 最新版的AS,可以为你使用ndk开发提供很大的方便,请确保你在SDK Tools中下载了CMake、LLDB、NDK
- 首先创建一个新项目,并勾选Include C++ support
- C++ Standard中可以选择使用的C++标准,默认是CMake所使用的标准,Exceptions Support可以启用C++异常处理,一般这些选项使用默认的就可以了
- 项目创建完毕之后你可以看见官方已经为你做好了很多工作,并且带了一个c++的hello world示例,你需要关注的主要有cpp和External Build Files 两个目录,前者用于放置你的C++源文件,后者根据不同的ABI版本放置了CMake脚本
- 接着我们直接在MainActivity中添加一个native方法,然后选中该方法,按下alt+enter,让IDE为我们自动生成C++函数
public native boolean booleanFromJNI();
- 在native-lib.cpp中可以看见自动生成的函数,我们只需要实现该函数即可
JNIEXPORT jboolean JNICALL
Java_com_linjiamin_myapplication_MainActivity_booleanFromJNI(JNIEnv *env, jobject instance) {
// TODO
}
- 注意使用上面的方法你可以在任意一个java文件中声明native方法,IDE会自动在native-lib.cpp中为你生成对应的函数签名,当然,你也不是非要把所有的C/C++代码都写在一个文件里,下面来讲解一下CMakeList的基本写法
编写CMakeList
- 下面是一份官方写好的CMakeList.txt,这个文件可以在你当前项目的app目录下看到
add_library( # 设置编译出来的so包的名字. 不需要添加lib前缀
native-lib
# 设置为共享链接库. 有SHARED,STATIC两种可选
SHARED
# 设置源文件的相对路径,可将多个源文件进行编译
src/main/cpp/native-lib.cpp )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # 设置外部引用库.这个库支持你在c/c++中打印log,具体请见 android/log.h
log-lib
# 外部引用库的名称
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # 指定被链接的库.
native-lib
# 链接log-lib到native-lib
${log-lib} )
- 现在我们想要将不同的源文件编译成多份so包,例如我在cpp目录下添加一份test-lib.cpp文件,代码如下
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_linjiamin_myapplication_JniUtil_booleanFromJNI(JNIEnv *env, jobject instance) {
//上面提到的log库可以这么使用,而且你应该使用宏让它好看些
__android_log_print(ANDROID_LOG_DEBUG,"stringFromJNI","%d",0);
return (jboolean) true;
}
- 那么可以在刚刚的CMakeList中设定我们的so包,在最后加上
add_library( test-lib SHARED src/main/cpp/test-lib.cpp )
- 编译之后 可以看到 build/intermediates/cmake/debug/obj/ 路径下不同的ABI目录中都有了两份so文件,分别是libnative-lib.so,libtest-lib.so
- 如果你在不同的路径下放置了源文件,并且希望对于每一个特定的路径都有一份自己特定的CMakeList文件来描述这些源文件的打包规则(这看起来是个好习惯),可以使用add_subdirectory("目录名")方法指定子路径,子路径当中的放置CMakeList会被执行
使用g++编译so包
- 对于非安卓开发者,这里再简单介绍一个使用g++编译so包的方法,使用这种方法你无需ndk环境,也不用编写mk、CMakeList文件,完全使用命令行进行编译,当然我更推荐你去学习cmake
- 编写java文件并用javah指令生成头文件,再编写cpp文件,这个流程对于不同平台的jni开发都是相同的(虽然Intelligent Idea这种IDE可以为你自动生成头文件),那么现在需要一份对应平台下的jni.h文件,可以在你的jdk当中查找,编译器可能还会提示你需要一份jni_md.h文件,它也在jdk当中
$ cd /Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk
$ find . -iname jni.h
./Contents/Home/include/jni.h
- 我这里用的是ndk当中的jni.h文件,下载ndk之后在sdk当中可以找到
$ cd /Users/alberthumbert/Library/Android/sdk/ndk-bundle
$ find . -iname jni.h
./sysroot/usr/include/jni.h
- 现在假定你以及有了一份java文件,两份头文件,一份cpp或c文件,那么可以使用如下命令将他们编译成so文件,注意最后的参数不一定是必要的,如果你想编译安卓平台可用的so包那么建议加上
g++ com_linjiamin_jnishare_JniUtil.cpp -fPIC -shared -o libsotest.so -Wl,--hash-style=sysv
*注意,nix平台使用lib表示一个so库,所以so文件必须以lib开头,但加载时请将lib前缀去掉**
- 你可以用绝对路径加载一个so文件
System.load("\***\***\***.so")
- 你也可以将so文件放入系统加载路径当中,调用 System.getProperty("java.library.path")方法得到系统加载路径,想要修改这个路径,可以在修改.bashrc(或者当前使用的其他shell)中添加
export PATH=PATH:/XXX
- 运行jar时动态指定路径也是可以的,这样会暂时覆盖上面写入的属性
java -jar -Djava.library.path=/**/** **.jar
- 重新在java代码中加载so文件,*nix平台注意去掉lib前缀
System.loadLibrary ("***");
ABI与so
- 这里再啰嗦一下别的东西,可以先跳过,之后再倒回来看
- 由于目前我们的项目很简单,没有用到第三方so库和也没有去除多余so库为apk瘦身,因此不用考虑兼容问题,但实际开发项目时通常没这么简单。我们知道,编译出来的so是二进制文件,由于不同的CPU支持不同的指令集,所以我们需要考虑兼容性的问题。一个包含多种指令集及其相关约定的实现被称之为ABI,一个CPU架构支持一种到多种ABI。安卓平台就是针对ABI进行编译和打包的。
- 可能看了上面这一段会比较晕,那么我就举一个例子来说明,比如ARMv7架构的CUP,支持 armeabi和armeabi-v7a两种ABI,而armeabi这种ABI支持Thumb-1,ARMV5TE等指令集,armeabi-v7a这种ABI又支持Thumb-2和VFPv3-D16等指令集,也就是说,一种CPU架构对应多种ABI类型,一种ABI类型对于多种指令集
- 一个so文件只支持一种ABI,因此你会发现在lib下每一个包都是以ABI来命名的,同名的so文件被按照其支持的ABI进行分类
- 目前ABI一共有七种,那么是不是意味者我们的每一个so都需要编译成七种,然后全都打包进apk当中呢,答案是否定的。目前CUP流行的架构主要有ARM系列,x86,x86_64,但移动设备大部分都是ARMv7架构,少数是ARM架构,由于ARMv7架构兼容armeabi,因此类似淘宝、微信、饿了么的国内大厂通常只使用armeabi一种ABI,Facebook,Twitter等外国大厂则是只保留了armeabi-v7a,这是十分合理的,apk只保留一种通用的ABI,而最适应的so可以在外部去下载
- 那么是不是只要编译一种so文件就可以了呢?不完全正确,如果你引用了第三方的ndk,而第三方在兼容性做得比较好的情况下适配了多种ABI,又或者目前你的lib下so包的数量参差不齐。当一个apk安装时就有可能查找到了最适用的ABI的路径存在,但里面又没有想要的so,这时它不会自动去查找其他ABI版本的so,而是会crash,为了解决这个问题,请在lib包下只保留一个包(通常是armeabi或者armeabi-v7a),或者每个名字的so在不同包下都存在对应版本,并且在app的gradle的defaultConfig闭包中添加你所适配好的ABI,这样安装时只会从你所指定的ABI中查找so包
ndk{
abiFilters "armeabi-v7a", "x86", "armeabi"
}
什么是jni
现在我们已经可以进行简单的ndk开发了,但为了加深理解认识,让我再来啰嗦一下jni
看过这一部分之后你对jni应该会有更近一步的感性认识
jni.h
jni数据类型
- 现在来看看c/c++层的数据类型是怎么对应到java层的
- 首先是基本类型,根据java中的定义定义了j*类型
/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
- 对于引用类型,c和c++有区别,在c++中 jobject是类,而jstring和各种类型的数组都是jobject的子类的指针,在c中jobject是一个void*指针,而其他引用类型其实都是jobject
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
typedef _jthrowable* jthrowable;
typedef _jobject* jweak;
/*
* Reference types, in C.
*/
typedef void* jobject;
typedef jobject jclass;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jobjectArray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jobject jthrowable;
typedef jobject jweak;
- jvalue是一个比较特殊的联合体,一般在需要调用java层方法时做为方法参数传入,如 void CallVoidMethodA(jobject obj, jmethodID methodID, jvalue* args) ,通过jobject和表示其方法的jmethodID即可特定一个具体的方法,然后将我们的jvalue作为函数列表传入
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
- 在这一方面我们可以讨论的内容比较少,总的来说,由于C/C++ 中基本类型的字节数依赖与实现,所以在native层转换到java层是不能直接使用原本的int,long等类型而是根据java中的约定使用jni.h指定了相同长度与有符号的类型,而java中的类则可以使用类或结构体的指针来解决
常用的接口
在讲解JNIEnv和JavaVM之前先来尝试一下各种jni的基本操作,版本较新的AS已经支持了对C/C++ 的智能提示和代码补全功能,你可以很方便地试用JNIEnv提供的接口
这里只介绍几个例子,以后有时间我会另写文章介绍这些接口,强烈推荐你使用AS把可调用的函数浏览并选择性地使用一遍
修改成员变量
- 通过之前的例子你应该已经知道怎么从native层中获取一个变量了,现在再进一步,我们使用native方法直接改变成员变量的值,在MainActivity中定义一个native方法
public class MainActivity extends AppCompatActivity {
public String mString = null;
static {
System.loadLibrary("native-lib");
}
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "onCreate: "+mString);
getFieldFromJNI();
Log.d(TAG, "onCreate: "+ mString);
}
public native String getFieldFromJNI();
}
- 在来看C++实现,注意你可能在阅览其他文章时发现调用jni函数时有两种不同的写法 env-> 和 (*env)->,这是由于C与C++的实现有所差异,不影响我们使用。如果你使用过反射改变成员变量的值,应该可以毫不费力地理解下面这段代码
extern "C"
JNIEXPORT jstring JNICALL
Java_com_linjiamin_jnilearning_MainActivity_getFieldFromJNI(JNIEnv *env, jobject instance) {
jclass clazz = env->GetObjectClass(instance);
//获取调用者的class对象
jfieldID jfID = env ->GetFieldID(clazz,"mString","Ljava/lang/String;");
//获取成员变量的键
jstring strValue = (jstring) env->GetObjectField(instance, jfID);
//获取成员变量的值,不做操作
char chars[10] = "whatever";
jstring newValue = env->NewStringUTF(chars);
//创建一个String对象
env->SetObjectField(instance,jfID,newValue);
//设置新的值
return strValue;
}
创建引用
引用类型
- 了解jni中的引用类型,有助于你编写高效的代码并且解决内存泄漏等问题,jni中的引用类型可以分为三种,局部引用,全局引用,弱(全局)引用,通常jvm会在函数返回后自动为你释放局部引用,但你需要自行管理全局引用的生命周期
局部引用
- 其实我们之前已经接触过局部引用了,调用jni函数通常会创建新对象的实例并返回一个局部引用,局部引用只在一次native函数的调用周期中存在,在函数结束时被释放
- 通常我们需要避免返回全局引用,而是返回创建出来的局部引用,例如这样
return (jstring) env->NewLocalRef(someValue);
- 我们刚刚说过,局部引用在函数的调用过程中存在,也就是说如果不进行人为的销毁操作,它将一直存在,在任意native函数中执行下面这段代码,你将接受到一个异常,跟java不同,gc不会及时回收通过这种方法创建出来的变量
for(int i = 0;i<1000000000;i++){
jstring newValue = env->NewStringUTF(chars);
}
- 你可以调用 DeleteLocalRef函数销毁一个局部引用,这个函数现在就可以执行了,不过他会比一般函数耗时些
for(int i = 0;i<1000000000;i++){
jstring newValue = env->NewStringUTF(chars);
env->DeleteLocalRef(newValue);
}
全局引用
- 之前说过局部引用在函数的调用过程中存在,我们不能直接使用显式赋值的方式将局部引用强行将其缓存起来
jobject gInstance;
…
extern "C" JNIEXPORT void JNICALL
Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
methodID = env->GetMethodID(clazz,"sayHello","()V");
gInstance = instance;
...
}
- 上面的例子将会报错,不过对于jni开发,你可能常常只能收到含糊的报错信息,甚至收不到报错信息
JNI DETECTED ERROR IN APPLICATION: native code passing in reference to invalid stack indirect reference table or invalid reference: 0x7fff871642e0
- 我们可以使用 NewGlobalRef 函数来创建一个全局引用,但是注意,你需要对自己的行为负责,全局引用只有在你的手动调用 DeleteGlobalRef 函数之后才会被释放,你可以在JNI_OnLoad 中进行缓存工作,在JNI_OnUnload函数中进行缓存的清除
jobject gInstance;
…
extern "C" JNIEXPORT void JNICALL
Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
methodID = env->GetMethodID(clazz,"sayHello","()V");
gInstance = env->NewGlobalRef(instance);
...
}
…
void fun() {
...
...
env->DeleteGlobalRef(gInstance);
}
弱引用
- 弱引用和全局引用大体上类似,但是当内存不足时它会被GC回收,通过 NewWeakGlobalRef 函数可以创建一个弱引用,和Java层的弱引用一致,它不会阻止自己所指向的对象被GC回收
gInstance = env->NewWeakGlobalRef(instance);
- 但是这不意味着你可以不用管理弱引用的生命周期,在不需要它时请主动释放弱引用,注意,弱引用的释放不会导致它所指向的对象被GC回收
env->DeleteWeakGlobalRef(gInstance)
- 最好在使用弱引用时判断它的对象是否已被释放,你可能会理所当然地使用 == 进行判断,这种方法是错误的,除非这个引用从来就没有被初始化过,不然表达式将永远为真,解决方案是使用jni提供的接口进行比较,有的文章也推荐再次使用NewWeakGlobalRef来达到这样的效果,个人认为这两种方案除了在可读性上的区别外没什么不同
if (env->IsSameObject(gInstance,NULL)) {
__android_log_print(ANDROID_LOG_DEBUG,"fun","%s","instance is NULL");
}
//或者
if (!gInstance || !env->NewWeakGlobalRef(gInstance)) {
__android_log_print(ANDROID_LOG_DEBUG,"fun","%s","instance is NULL");
}
JNIEnv,JavaVM 以及多线程
- 你可能已经意识到,目前为止我们都是通过JNIEnv来使用jni的,实际上JNIEnv提供了Native函数的基础环境,具体来说,它包含了一个指向函数表的指针,这也就是为什么我们需要通过JNIEnv才能调用native方法,JNIEnv也代表了具体的进程环境,因此不允许跨进程调用,最好的做法是永远不要缓存JNIEnv,你可以通过JavaVM来创造它的实例
- JavaVM是java虚拟机的代表,它可以跨线程调用,它是一个全局对象,典型的jni环境中一个进程可以有多个JavaVM,但是在安卓环境当中他在每个进程中只有一个实例,通常你可以在JNI_OnLoad 函数,或其可以获取JNIEnv的地方得到它_
env->GetJavaVM(&gVm);
- 下面在C++线程中模仿耗时操作,并调用Java层方法传回数据,首先定义接受数据的方法和一个native方法,这里的参数列表稍微定义得复杂一点,方便之后演示jvalue的使用方法
public void resultCallback(boolean isSuccess,int result,String data){
Log.d(TAG, "resultCallback: "+ isSuccess + " "+result + " " +data);
}
public native void useCPlusThread();
- native方法的实现如下,这里我们通过GetJavaVM方法得到了JavaVM对象,JavaVM用于我们之后获取JNIEnv,同时我们把调用者通过全局引用缓存起来,注意这里的methodID不需要使用NewGlobalRef,它是一个结构体,直接赋值即可,由于java支持重载,需要输入方法函数列表的标识才可以特定一个方法,每个基本类型都有其对应的缩写,而对于类我们需要通过包名和类名来指定。然后我们开启五个线程进行耗时操作
extern "C"
JNIEXPORT void JNICALL
Java_com_linjiamin_jnilearning_MainActivity_useCPlusThread(JNIEnv *env, jobject instance) {
env->GetJavaVM(&gVm);
jclass clazz = env->GetObjectClass(instance);
methodID = env->GetMethodID(clazz, "resultCallback", "(ZILjava/lang/String;)V");
gInstance = env->NewGlobalRef(instance);
pthread_t pthread[5];
for(int i = 0;i<5;i++){
pthread_create(&pthread[i], NULL, &fun, NULL);
}
}
- 线程方法的实现如下,linux系统的sleep函数定义在unistd.h文件中,我们使用它来模仿耗时操作,像刚刚说过的一样JNIEnv不能跨进程调用,那么这里使用AttachCurrentThread函数得到实例,这个函数同时也会将当前线程绑定到JavaVM上,然后我们使用CallVoidMethodA来调用刚刚缓存起来的实例的方法,也就是java层的resultCallback方法,jvalue数组可以作为参数列表传入,另外你也可以使用更为简便的CallVoidMethod函数,最后记得使用DetachCurrentThread函数解绑,除非你使用DeleteLocalRef函数释放引用,不然你通过JNIEnv获取的局部引用在你调用DetachCurrentThread之前都不会被销毁,并且在函数结束后造成内存泄漏
void *fun(void *arg) {
sleep(3);
JNIEnv *env;
if (gVm->AttachCurrentThread(&env, NULL) != JNI_OK) {
__android_log_print(ANDROID_LOG_DEBUG, "callJniInDifferentThread", "%s", "attach failed");
return NULL;
}
jvalue * args = new jvalue[3];
args[0].z = (jboolean) true;
args[1].i = 1000;
args[2].l = env->NewStringUTF("some data");
env->CallVoidMethodA(gInstance, methodID, args);
if (gVm->DetachCurrentThread() != JNI_OK) {
__android_log_print(ANDROID_LOG_DEBUG, "callJniInDifferentThread", "%s", "detach failed");
}
return NULL;
}
- 注,各种类型对应的缩写如下,请使用一下的缩写特定具体的方法
类型 | 缩写 |
---|---|
Boolean | Z |
Byte | B |
Char | C |
Short | S |
Int | I |
Long | L |
Float | F |
Double | D |
Void | V |
Object | 以"L"开头,以";"结尾,中间是用"/" 隔开的包及类名。比如:Ljava/lang/String;如果是嵌套类,则用$来表示嵌套。例如 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z" |
返回值与参数 | 例 (IB)L 表示返回类型为long,参数为int和byte的函数 |
内存泄漏
局部引用的内存模型
- 刚刚提到JVM在一定程度上会为你管理局部引用的生命周期,但这不意味着局部引用等于局部变量。每当线程从Java层切换到native层时,JVM会创建局部引用表,它们维系了你的C/C++变量和Java层变量。
- 下图中出现的本地方法栈是和虚拟机栈类似的一种概念,但它用于运行native方法,注意,规范只约定了jni的操作和使用方法,对实现没有明确的要求,有些虚拟机会将虚拟机栈与本地方法栈合并实现,这里只大致地描绘本地方法栈的结构。当java层切入到native层(以下简称J2N过程,反之为N2J),或者在native函数中调用了jni接口时会导致本地方法栈的入栈操作,本地引用表在J2N时创建,并在N2J时销毁,在这个过程中,每当局部引用被合法创建,该局部引用都会被添加到表中并映射到java堆中的一个对象
- 看回前面这个例子,我们在循环中不断创建新的局部引用,并且赋值给变量newValue,这些不断创建的引用并不会立即释放,并且我们之后也无法获取到这些还留在表中的引用,所以他们都导致了内存泄漏。一般情况下局部引用表分配到的内存空间很小,这种内存泄漏很容易就会导致内存溢出,虚拟机崩溃。为了编写更加安全流畅的代码,我建议你遵循下面几个规范
for(int i = 0;i<1000000000;i++){
jstring newValue = env->NewStringUTF(chars);
}
引用的使用规范
- native编程首先需要遵循C/C++自身的内存管理机制,除了局部引用以外,JVM不会为你做更多的内存释放工作,所以当你使用malloc函数分配内存空间后必须使用free函数进行释放,这和其他平台上的C/C++编程没什么不同
- 全局变量对java层对象的引用一直有效,请在不用时进行删除,否它所指向的对象将一直留在堆中
- 和刚刚介绍局部引用时说的一样,在函数返回之前,局部引用不会自动释放,如果创建过多的引用将会导致内存溢出的风险,如果你的函数只会创建为数不多的局部引用,那么完全可以将删除引用的操作交给JVM去处理,但如果你的函数会创建大量的引用,特别是在开启循环的请况下,请自行调用DeleteLocalRef函数