why
为什么学习jni,掌握这项技能,对于学习一些主流的三方框架,如性能优化等,或者了解安卓系统framework一些功能的具体实现,都是必不可少的。
what
当我们讲学jni,我们到底要学习它的什么。
了解其本质,它就是java调用c/c++的一套桥接框架,和jsbridge等工具的职责并无本质不同,区别在于其是虚拟机提供相应的支持能力。
那先确定一个目标,通过这一波时间投入,需要完全掌握的几个技能点:
- jni的相关使用语法。
- java层调用c/c++方法。
- c/c++调用java层方法。
how
怎么学,总体来说还是采用吸星大法,吸别人总结的知识,学会这个工具相关的使用即可,主打一个点到为止。
detail
JNI(Java Native Interface),中文为java本地调用。
对于jni的学习,最好结合一个实际存在的项目学,比如一些技术书籍中会参考一些aosp的系统服务来讲解,这里我们换个口味,参考腾讯的性能优化框架matrix来看,更实用一些哈哈,毕竟有些系统服务不一定能用上,学了白学。。。
这里我们参照matrix里的anr采集模块。
我们把代码分为3层来看:
- java层:SignalAnrTracer类
- jni层:MatrixTracer.cc
- native层:SignalHandler.cc,AnrDumper.cc
先看java层的代码,这里把和jni相关的部分提取出来:
public class SignalAnrTracer extends Tracer {
//1.加载对应的JNI库,trace-canary是native库的名字,这里我们的jni代码和
//native代码都在这个库中
static {
System.loadLibrary("trace-canary");
}
//2.声明native函数,注意使用native关键字
private static native void nativeInitSignalAnrDetective(String anrPrintTraceFilePath, String printTraceFilePath);
//3.声明java函数,供native调用
@Keep
private synchronized static void onANRDumped() {
...
}
}
加载JNI库
调用System.load函数即可加载native库,这里我们加载的是libtrace-canary.so。
java层调用native层
我们在MatrixTracer.cc里找到了疑似和java层调用native函数关联的逻辑:
static void nativeInitSignalAnrDetective(JNIEnv *env, jclass, jstring anrTracePath, jstring printTracePath) {
const char* anrTracePathChar = env->GetStringUTFChars(anrTracePath, nullptr);
const char* printTracePathChar = env->GetStringUTFChars(printTracePath, nullptr);
anrTracePathString = std::string(anrTracePathChar);
printTracePathString = std::string(printTracePathChar);
sAnrDumper.emplace(anrTracePathChar, printTracePathChar);
}
但这里大家可能疑惑的是,是怎么在native函数中找到这个jni调用的native方法的,难道仅仅是因为名字一样吗,显然不是的。
其实jni函数的注册有两种方式,静态注册方式和动态注册方式:
静态方式
静态方式一般可以通过javah自动生成,类似一种生成模板方法的模式,这是因为生成的jni方法名有固定的套路,这里我们通过手动的方式拆解这个套路。
1.先找到SignalAnrTracer类的包名:com.tencent.matrix.trace.tracer
2.将包名的.replace成_,即com_tencent_matrix_trace_tracer,记为xxx
生成jni层的方法名字:Java_xxx_nativeInitSignalAnrDetective
3.最后,jni静态生成的方法名为:Java_com_tencent_matrix_trace_tracer_nativeInitSignalAnrDetective
动态方式
这里我们不去纠结到底哪种方式更好,只知道可以动态注册即可,而matrix库使用的是动态注册方式。 首先使用一个结构体:
typedef struct {
//java中native函数的名字
const char* name;
//java函数的签名信息
const char* signature;
//jni层对应函数的函数指针
void* fnPtr;
} JNINativeMethod;
可以简单看做一个映射关系,这样native的jni函数名就可以自定义了。
static const JNINativeMethod ANR_METHODS[] = {
{"nativeInitSignalAnrDetective", "(Ljava/lang/String;Ljava/lang/String;)V", (void *) nativeInitSignalAnrDetective},
{"nativeFreeSignalAnrDetective", "()V", (void *) nativeFreeSignalAnrDetective},
{"nativePrintTrace", "()V", (void *) nativePrintTrace},
};
实际来看,由于一个so里通常有许多native调用,这里matrix注册了三个函数的映射。
处理声明映射关系外,我们还需要将这个映射关系通知给虚拟机,在JNI_OnLoad方法中,进行如下调用即可:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
jclass anrDetectiveCls = env->FindClass("com/tencent/matrix/trace/tracer/SignalAnrTracer");
if (!anrDetectiveCls)
return -1;
if (env->RegisterNatives(
anrDetectiveCls, ANR_METHODS, static_cast<jint>(NELEM(ANR_METHODS))) != 0)
return -1;
}
RegisterNatives函数的第一个参数为对应的java类名。
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着就会查找该库中JNI_OnLoad函数,这样就完成了动态注册。
tips:函数签名
做动态注册映射时,除了传入java层方法名和native方法名外,还需要传入java层的方面签名。
方法签名的格式为(参数1类型参数2类型...参数n类型)返回值类型
这里给出参数类型映射表
| 类型标识 | java类型 |
|---|---|
| Z | boolean |
| B | byte |
| C | char |
| S | short |
| I | int |
| J | long |
| F | float |
| D | double |
| L/java/land/string; | String |
| [I | int[] |
| [L/java/lang/object | Object[] |
上面说了这么多,但只是完成了java层方法到native层方法的转换,为了避免遗忘,把java层的方法和native层的方法再打出来:
//java层
private static native void nativeInitSignalAnrDetective(String anrPrintTraceFilePath, String printTraceFilePath);
//c++层
static void nativeInitSignalAnrDetective(JNIEnv *env, jclass, jstring anrTracePath, jstring printTracePath) {
const char* anrTracePathChar = env->GetStringUTFChars(anrTracePath, nullptr);
const char* printTracePathChar = env->GetStringUTFChars(printTracePath, nullptr);
anrTracePathString = std::string(anrTracePathChar);
printTracePathString = std::string(printTracePathChar);
sAnrDumper.emplace(anrTracePathChar, printTracePathChar);
}
这里可以发现,java函数只有2个参数,到native函数变成4个了,wtf
先看下后两个吧,看上去还是对的上的。
对于基本类型来说,java到native就是一一对应的关系。
对于引用数据类型来说,除了基本数据类型的数组,Class,String,Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。
再看下多出来的两个参数。
jclass表示Java层是静态函数,这个参数指的就是Java层的Class。
如果Java层是成员函数,那这个参数就变成了
jobject thiz
该类的实例,即在哪个对象上调用的该方法。
JNIEnv
这里我们着重介绍下native方法的第一个参数JNIEnv,因为它比较重要。
其实我们把它看做是虚拟机线程相关的一个上下文即可,它具有一些比较大的权限,如:
- 调用Java层函数,后面介绍。
- 操作jobject对象等。 相对应的,在进程有一个全局的权力较大的上下文对象:JavaVM,调用其AttachCurrentThread方法就可以获得当前线程的JNIEnv。
下面函数可以通过JNIEnv获得Java类的变量和函数:
jfiedlID getFieldID(jclass clazz, const char *name, const char *sig);
jmethodID getMethodID(jclass clazz, const char *name, const char *sig);
jfiedlID getStaticFieldID(jclass clazz, const char *name, const char *sig);
jmethodID getStaticMethodID(jclass clazz, const char *name, const char *sig);
看下matrix是怎么应用的
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
jclass anrDetectiveCls = env->FindClass("com/tencent/matrix/trace/tracer/SignalAnrTracer");
if (!anrDetectiveCls)
return -1;
gJ.AnrDetector_onANRDumped =
env->GetStaticMethodID(anrDetectiveCls, "onANRDumped", "()V");
}
这里将SignalAnrTracer的onANRDumped方法保存到gJ(StacktraceJNI)的AnrDetector_onANRDumped变量中,以备我们后面讲native调用java层时备用。
native调用Java层
有了jfieldID,jmethodID后,就可以使用JNIEnv提供的一系列方法,进行反向调用了。
//调用方法
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID,...)
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID,...)
//操作字段
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID)
void set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value)
看是看一下实战吧,我们看下matrix是怎么调用到SignalAnrTracer的onANRDumped方法的。
bool anrDumpCallback() {
JNIEnv *env = JniInvocation::getEnv();
if (!env) return false;
env->CallStaticVoidMethod(gJ.AnrDetective, gJ.AnrDetector_onANRDumped);
return true;
}
简单吧!
jstring
String应该是我们使用最高频的一个引用类型了,所以有必要着重介绍下JNI情况下字符串的一些用法。
native string转java string
jstring newStringUTF(JNIEnv *env, const char *unicodeChars)
这个方法用在native层调用java层方法,需要传递string参数时。
java string转native string
char* getStringUTFChars(JNIEnv *env, jstring str, void *)
还是请出例子老朋友:
static void nativeInitSignalAnrDetective(JNIEnv *env, jclass, jstring anrTracePath, jstring printTracePath) {
const char* anrTracePathChar = env->GetStringUTFChars(anrTracePath, nullptr);
const char* printTracePathChar = env->GetStringUTFChars(printTracePath, nullptr);
anrTracePathString = std::string(anrTracePathChar);
printTracePathString = std::string(printTracePathChar);
sAnrDumper.emplace(anrTracePathChar, printTracePathChar);
}
这里通过JNIEnv提供的转换方法,获取到了我们hook anr trace日志保存的文件位置,并将该变量传入我们的native代码逻辑中。
tips:java层gc对jni层的影响
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
gJ.AnrDetective = static_cast<jclass>(env->NewGlobalRef(anrDetectiveCls));
}
这里AnrDetective和anrDetectiveCls都是jclass,为什么还要调用NewGlobalRef呢。
这是因为在native层直接使用java层的一个对象,是不会增加该对象的引用计数,那如果在java层回收了该对象,native层保存的这个变量就变成野指针了。
这里我们使用NewGlobalRef,将其变为全局引用。
使用全局引用,虽然避免了调用方法返回,对象内存被回收的风险,但同时这个对象只能由我们手动释放,一般在保存该对象的析构函数中释放内存。
JNI还提供了一种弱全局引用的使用方式,使得对象的生命周期可以关联到jvm的gc能力,这使得我们能够更方便的在jni中使用java对象,不再需要手动释放内存。但由于对象可能被内存回收,需要在使用前判断:
env-> NewWeakGlobalRef(xxx)
if(env->IsSameObject(weakGlobalRef, NULL)) {
return false;
}
至此,我们基本掌握了jni这个工具的各种用法。掌握这个基本工具,再结合一些linux中断,信号处理,local socket,plt hook的基础知识后,就可以接着愉快的学习matrix anr相关性能优化模块的内容了。