ART虚拟机 _ JNI静态注册和动态注册(1)

// - 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方法在调用过程中却会多出一步。 JNI跳板函数.png

  • 首先进入一个跳板函数,其中会处理Java参数到Native参数的转换以及线程状态切换等过程。
  • 在跳板函数内部调用Native世界中实现的JNI函数。

这样一来,不用存储字节码入口信息的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函数必须由JNIEXPORTJNICALL进行修饰。

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_的值有两种可能:

  1. 由于跳板函数主要负责参数转换,因此对于不同的native方法,只要它们的参数个数和类型一致,就可以使用同一个跳板函数。这些跳板函数只在AOT编译条件下才会生成,因此纯解释执行时quick_code == nullptr。
  2. quick_code == nullptr时,为entry_point_from_quick_compiled_code_设置的值是art_quick_generic_jni_trampoline函数指针。它相当于一个通用的跳板函数,在执行过程中动态进行参数转换。

data_的值(不考虑CriticalNative)只有一种可能:

  1. 设置为art_jni_dlsym_lookup_stub函数指针。该函数在执行时根据静态转换规则找到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_字段中。

// See section 11.3 “Linking Native Methods” of the JNI spec.
void* FindNativeMethod(Thread* self, ArtMethod* m, std::string& detail)
REQUIRES(!Locks::jni_libraries_lock_)
REQUIRES_SHARED(Locks::mutator_lock_) {
std::string jni_short_name(m->JniShortName());
std::string jni_long_name(m->JniLongName());

{
// Go to suspended since dlsym may block for a long time if other threads are using dlopen.
ScopedThreadSuspension sts(self, kNative);
void* native_code = FindNativeMethodInternal(self,
declaring_class_loader_allocator,
shorty,
jni_short_name,
jni_long_name);
if (native_code != nullptr) {
return native_code;
}
}

}

FindCodeForNativeMethod内部调用FindNativeMethod,创建两个字符串,一个是jni_short_name,另一个是jni_long_name。其实二者反映的就是之前所说的两种映射规则。

std::string GetJniShortName(const std::string& class_descriptor, const std::string& method) {
// Remove the leading ‘L’ and trailing ‘;’…
std::string class_name(class_descriptor);
CHECK_EQ(class_name[0], ‘L’) << class_name;
CHECK_EQ(class_name[class_name.size() - 1], ‘;’) << class_name;
class_name.erase(0, 1);
class_name.erase(class_name.size() - 1, 1);

std::string short_name;
short_name += “Java_”;
short_name += MangleForJni(class_name);
short_name += “_”;
short_name += MangleForJni(method);
return short_name;
}

std::string ArtMethod::JniLongName() {
std::string long_name;
long_name += JniShortName();
long_name += “__”;

std::string signature(GetSignature().ToString());
signature.erase(0, 1);
signature.erase(signature.begin() + signature.find(‘)’), signature.end());

long_name += MangleForJni(signature);

return long_name;
}

Short name是不考虑重载的映射规则,long name则增加了参数信息用于区分不同方法。寻找符号时,先找short name,再找long name。

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门**

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值