大龄菜鸡的安卓jni学习日志

204 阅读8分钟

why

为什么学习jni,掌握这项技能,对于学习一些主流的三方框架,如性能优化等,或者了解安卓系统framework一些功能的具体实现,都是必不可少的。

what

当我们讲学jni,我们到底要学习它的什么。
了解其本质,它就是java调用c/c++的一套桥接框架,和jsbridge等工具的职责并无本质不同,区别在于其是虚拟机提供相应的支持能力。
那先确定一个目标,通过这一波时间投入,需要完全掌握的几个技能点:

  1. jni的相关使用语法。
  2. java层调用c/c++方法。
  3. c/c++调用java层方法。

how

怎么学,总体来说还是采用吸星大法,吸别人总结的知识,学会这个工具相关的使用即可,主打一个点到为止。

detail

JNI(Java Native Interface),中文为java本地调用。

对于jni的学习,最好结合一个实际存在的项目学,比如一些技术书籍中会参考一些aosp的系统服务来讲解,这里我们换个口味,参考腾讯的性能优化框架matrix来看,更实用一些哈哈,毕竟有些系统服务不一定能用上,学了白学。。。

这里我们参照matrix里的anr采集模块。
我们把代码分为3层来看:

  1. java层:SignalAnrTracer类
  2. jni层:MatrixTracer.cc
  3. 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类型
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L/java/land/string;String
[Iint[]
[L/java/lang/objectObject[]

上面说了这么多,但只是完成了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,因为它比较重要。
其实我们把它看做是虚拟机线程相关的一个上下文即可,它具有一些比较大的权限,如:

  1. 调用Java层函数,后面介绍。
  2. 操作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相关性能优化模块的内容了。