本文分析基于Android 11(R)
源码路径
注册的本质是建立(Java层)native方法和(Native/C++层)JNI函数之间的一对一关系。静态注册指的是映射规则预先设定,一个native方法名可以转换成一个唯一的JNI函数名。动态注册的映射规则由程序员自己设定,通过结构体将native方法和JNI函数指针绑定起来。
如果需要在Java中使用so库中的代码,那么首先要做的就是库加载。库加载一般放在static代码块中,保证类加载后第一时间执行。
package com.hangl.jni;
public class TestJNI {
static {
System.loadLibrary(“native”);
}
…
}
众所周知,库加载时会去调用库中的JNI_Onload
函数。在ART源码中,System.loadLibrary
最终会调用JavaVMExt::LoadNativeLibrary
。首先去库中寻找JNI_Onload
符号,将其转成函数指针进行调用。接着,JNI_Onload
函数返回该库所必需的JNI版本号,由虚拟机判断是否支持。
void* sym = library->FindSymbol(“JNI_OnLoad”, nullptr);
using JNI_OnLoadFn = int()(JavaVM, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
int version = (*jni_on_load)(this, nullptr);
…
if (version == JNI_ERR) {
StringAppendF(error_msg, “JNI_ERR returned from JNI_OnLoad in “%s””, path.c_str());
} else if (JavaVMExt::IsBadJniVersion(version)) {
StringAppendF(error_msg, “Bad JNI version returned from JNI_OnLoad in “%s”: %d”,
path.c_str(), version);
…
} else {
was_successful = true;
}
当前虚拟机只支持三个JNI版本,如下所示。一般库返回较多的是JNI_VERSION_1_4。与1_4相比,1_6版本的JNI只多了一个GetObjectRefType
函数[链接],如果我们的库用不到这个函数,只需要1_4即可。
bool JavaVMExt::IsBadJniVersion(int version) {
// We don’t support JNI_VERSION_1_1. These are the only other valid versions.
return version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && version != JNI_VERSION_1_6;
}
JNI_Onload
函数是我们进行动态注册的宝地,在其中调用RegisterNatives
即可进行注册。具体的方法和细节留到"动态注册"章节阐述。
通常而言,一个Java方法在虚拟机中可以找到两种执行方式,一种是解释执行,另一种是机器码执行。解释执行时,解释器会去寻找字节码的入口地址。而机器码执行时,虚拟机会去寻找机器指令的入口地址。考虑到每个Java方法在虚拟机中都由ArtMethod
对象表示,字节码的入口信息(间接)存在其data_
字段中,而机器码入口信息则存在entry_point_from_quick_compiled_code_
字段中。如下所示。
// Must be the last fields in the method.
struct PtrSizedFields {
// Depending on the method type, the data is
// - native method: pointer to the JNI function registered to this method
// or a function to resolve the JNI function,
// - resolution method: pointer to a function to resolve the method and
// the JNI function for @CriticalNative.
// - conflict method: ImtConflictTable,
// - abstract/interface method: the single-implementation if any,
// - proxy method: the original interface method or constructor,
// - other methods: during AOT the code item offset, at runtime a pointer
// to the code item.
void* data_;
// Method dispatch from quick compiled code invokes this pointer which may cause bridging into
// the interpreter.
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
不过对于native方法而言,它在Java世界中只有定义没有实现,因此不会有字节码信息。虽然不需要存储字节码的入口地址,但native方法在调用过程中却会多出一步。
这样一来,不用存储字节码入口信息的data_
字段就可以用来存储JNI函数的入口地址了。而entry_point_from_quick_compiled_code_
中存储的就是跳板函数的入口地址。具体可参考ART视角 | 为什么调用Native方法可以进入C++世界。
当我们不在JNI_Onload
中调用RegisterNatives
,或者压根不在so库中编写JNI_Onload
函数时,native方法的映射只能由静态注册完成。
虽然这个过程的名字叫"静态注册",但实际注册是在运行时按需动态完成的,只不过由于映射关系是事先确定的,所以才被叫做"静态"。
那么具体的映射规则是什么呢?
JNI实现了两套映射规则,一套是简化版,一套是为了解决方法重载的复杂版。最终转换出来的函数名按照如下规则顺次拼接。
Java_
/
转成_
_
__
连接参数签名。如果该方法没有被重载,则省略这一步。为了区分重载方法,字符串的末尾需要拼接参数签名,而签名中是有可能有[
、;
和_
字符的。为了不在函数名中出现这些特殊字符(或者为了不和之前的连接符_
混淆),转换时对这些字符做了特殊处理。
_
转换为_1
;
转换为_2
[
转换为_3
由于Java类名和方法名中都不可能以数字开头,所以这样的转换不会跟Java类名或方法名冲突。具体规则参考JNI文档。以下是一个转换示例。
package pkg;
class Cls {
native double f(int i, String s);
…
}
转换为:
JNIEXPORT jdouble JNICALL Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv env, / interface pointer */
jobject obj, /* “this” pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */ {
/* Obtain a C-copy of the Java string */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* process the string */
…
/* Now we are done with str */
(*env)->ReleaseStringUTFChars(env, s, str);
return …
}
不过要注意一点,静态注册的JNI函数必须由JNIEXPORT
和JNICALL
进行修饰。
JNIEXPORT
表明将函数名输出到动态符号表中,这样后续注册时调用dlsym才能找的到。默认情况下,所有的函数名都会输出到动态符号表中。但为了安全性,我们可以在编译时传入-fvisibility=hidden
来关闭这种输出(JNIEXPORT
修饰的依然会输出),防止别人知道so中定义了哪些函数。这一点对于商业软件尤为重要。
JNICALL
主要用于消除不同硬件平台调用规则的差异,对于AArch64而言,JNICALL
不执行任何动作。
规则介绍完毕,接下来就要深入注册的具体过程。
上文中提到,ArtMethod对象的data_
字段存储JNI函数的入口地址,而entry_point_from_quick_compiled_code_
存储跳板函数的入口地址。可是对静态注册而言,直到第一次方法调用时映射关系才建立。
一个方法被调用之前,首先要加载它所属的类。那么在类加载到方法第一次调用的这段时间里,data_
和entry_point_from_quick_compiled_code_
等于什么呢?
类加载时会调用LinkCode
函数,为ArtMethod对象设置entry_point_from_quick_compiled_code_
和data_
字段。
static void LinkCode(ClassLinker* class_linker,
ArtMethod* method,
const OatFile::OatClass* oat_class,
uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {
…
const void* quick_code = nullptr;
if (oat_class != nullptr) {
// Every kind of method should at least get an invoke stub from the oat_method.
// non-abstract methods also get their code pointers.
const OatFile::OatMethod oat_method = oat_class->GetOatMethod(class_def_method_index);
quick_code = oat_method.GetQuickCode();
}
…
if (quick_code == nullptr) {
method->SetEntryPointFromQuickCompiledCode( //set entry_point_from_quick_compiled_code_ 字段
method->IsNative() ? GetQuickGenericJniStub() : GetQuickToInterpreterBridge());
}
…
if (method->IsNative()) {
// Set up the dlsym lookup stub. Do not go through UnregisterNative()
// as the extra processing for @CriticalNative is not needed yet.
method->SetEntryPointFromJni( // set data_ 字段
method->IsCriticalNative() ? GetJniDlsymLookupCriticalStub() : GetJniDlsymLookupStub());
…
}
}
entry_point_from_quick_compiled_code_
的值有两种可能:
quick_code
== nullptr。quick_code
== nullptr时,为entry_point_from_quick_compiled_code_
设置的值是art_quick_generic_jni_trampoline
函数指针。它相当于一个通用的跳板函数,在执行过程中动态进行参数转换。data_
的值(不考虑CriticalNative)只有一种可能:
art_jni_dlsym_lookup_stub
函数指针。该函数在执行时根据静《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
态转换规则找到JNI函数,接着跳转过去。因此真正的注册发生在它里面。
接下来看看JNI函数的寻找过程。art_jni_dlsym_lookup_stub
是汇编代码,其内部会调用artFindNaitveMethod
找到JNI函数的指针,然后通过br x17
指令跳转到该JNI函数中。
ENTRY art_jni_dlsym_lookup_stub
…
// Call artFindNativeMethod() for normal native and artFindNativeMethodRunnable()
// for @FastNative or @CriticalNative.
…
b.ne .Llookup_stub_fast_or_critical_native
bl artFindNativeMethod
b .Llookup_stub_continue
.Llookup_stub_fast_or_critical_native:
bl artFindNativeMethodRunnable
.Llookup_stub_continue:
mov x17, x0 // store result in scratch reg.
…
cbz x17, 1f // is method code null ?
br x17 // if non-null, tail call to method’s code.
1:
ret // restore regs and return to caller to handle exception.
END art_jni_dlsym_lookup_stub
extern “C” const void* artFindNativeMethodRunnable(Thread* self)
REQUIRES_SHARED(Locks::mutator_lock_) {
…
const void* native_code = class_linker->GetRegisteredNative(self, method);
if (native_code != nullptr) {
return native_code;
}
…
JavaVMExt* vm = down_cast<JNIEnvExt*>(self->GetJniEnv())->GetVm();
native_code = vm->FindCodeForNativeMethod(method);
…
return class_linker->RegisterNative(self, method, native_code);
}
artFindNativeMethod
内部调用的是artFindNativeMethodRunnable
,它首先判断ArtMethod的data_
字段是不是已经注册过了,如果是则直接返回data_
存储的函数指针。否则调用FindCodeForNativeMethod
去寻找。最后将找到的函数指针写入data_
字段中。
…
return class_linker->RegisterNative(self, method, native_code);
}
artFindNativeMethod
内部调用的是artFindNativeMethodRunnable
,它首先判断ArtMethod的data_
字段是不是已经注册过了,如果是则直接返回data_
存储的函数指针。否则调用FindCodeForNativeMethod
去寻找。最后将找到的函数指针写入data_
字段中。