100

一个ClassLoader引起的JNI链接错误

 6 years ago
source link: https://mp.weixin.qq.com/s/VP1S-pYwriqhmTcEeU2s2w
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一个ClassLoader引起的JNI链接错误

Rayszhang QQ空间开发团队 2017-10-19 11:26 Posted on

Android插件化工程具有减少方法数和包大小,易于扩展等优势,深得大型工程的青睐,但同时插件化也会引起一些意想不到的麻烦。我们最近在做的插件工程就遇到了一个诡异的JNI链接错误。

我们的插件工程作为主工程的具体业务,主工程提供了基础的类库和工具,插件工程有自己的ClassLoader,并把主工程的ClassLoader设为自己的父ClassLoader,通过双亲委托,插件工程就可以访问主工程中的类。在主工程中有一个类库,有JNI方法,但为了减少主工程的包大小,so文件由插件在用到时自己下载和加载。

而这种加载方式,出现了诡异的UnsatisfiedLinkError错误。我们首先检查了System.load方法发现并没有出错,又查看了进程的内存映射信息,发现so文件确实已经加载,但调用JNI方法也确实一直出错。待排查了时序等相关情况后,还是不成功,于是我们只得求助于系统源码,希望能从源码中找到答案,以Android N为例,我们开始了源码分析过程。

so加载流程分析

so既然要先加载才能用,那我们就先来看so是怎么加载的,先来分析System.load方法

Image

方法很简单,直接调用了Runtime类的load方法,传入了so的名称和当前的ClassLoader,再来看这个方法

Image

可以看到,load校验了参数后调用了doLoad方法,doLoad取得ldLibraryPath和dexPath后调用了native层的nativeLoad函数。继续看nativeLoad函数

Image

还是很简单的函数,设置完LdLibraryPath后,调用JavaVM的LoadNtiveLibrary函数,继续看

Image

该函数较长,但逻辑还是很清晰的,我们只列出了关键代码,libraries保存了一个以so路径和SharedLibrary对象为记录的Map,保存了当前所有已经加载的so。首先从libraries中查找记录,如果有说明该so已经加载过,再判断和so关联的ClassLoader是不是当前的ClassLoader,如果不是,返回false,这说明同一个路径的so只能被一个ClassLoader加载,如果没找到记录,说明该so没有加载过,则通过dlopen打开该so,保存相关信息到SharedLibrary对象中,把SharedLibrary添加到libraries中,用dlsym查找JNI_OnLoad函数,如果找到了则执行该函数。 在看代码时第一反应是会不会isSameObject判断这里有问题,so已经被另一个ClassLoader给加载了,但转念一想,如果这里有问题那么load的时候会直接报错,而不是在执行的时候才报错。所以so的加载流程没有找到有问题的点,那么我们再看执行流程。

native方法执行流程分析

我们知道,在ART环境下,类的方法都会用ArtMethod表示,而ArtMethod的PtrSizedFields字段保存了该方法的跳转地址

Image

其中entry_point_from_jni_就是native函数执行时的跳转地址,那么这个地址是什么呢?其实这个地址是Class在加载的时候设置的,我们来看下代码

Image

ClassLinker负责在ART中加载Class,通过FindClass->DefineClass->LoadClass->LoadClassMembers,会解析出ArtMethod,最后通过LinkCode对ArtMethod的跳转地址进行赋值,这里我们只看native方法的情况,执行了UnregisterNative函数

Image

SetEntryPointFromJni就是对entry_point_from_jni_做了赋值,值是通过GetJniDlsymLookupStub()获得,就是一个artjnidlsymlookupstub函数地址,到这里我们知道类加载后其native方法地址被设置成了artjnidlsymlookupstub这个入口函数,当native方法被执行时,会调用这个入口函数执行,我们来看这个函数

Image

art_jni_dlsym_lookup_stub在汇编中定义,与平台相关,我们用arm64平台代码作为例子

Image

可以看到这个函数又跳转到了artFindNativeMethod函数

Image

该函数首先查询native函数的地址,查到后会通过RegisterNative设置给ArtMethod,这样以后就ArtMethod就可以直接跳转到native层的地址,而不用每次都经过该函数,RegisterNative同样调用了SetEntryPointFromJni来设置跳转地址,接下来看FindCodeForNativeMethod函数

Image

这里又看到了熟悉的libraries,前边分析so加载部分已经知道它保存了所有已经加载的so,所以这就是从已经加载的so里查找native函数,如果没找到,则抛出UnsatisfiedLinkError。我们再来看看FindNativeMethod

Image

FindSymbol就是调用dlsym获取native函数的地址,所以到此native函数的地址就真正的找到了,但是我们注意到了其中的一个判断,library->GetClassLoader()==declaring_class_loader,也就是和so关联的ClassLoader要和当前的ClassLoader是同一个才行,不然会放弃查找,到此我们的疑惑也就解开了,因为JAVA层的代码是在主工程的ClassLoader里,而加载so用的是插件的ClassLoader,两个ClassLoader不相等,所以在这里放弃了查找而抛出了异常。

知道了原因解决自然也就容易了,只要用同一个ClassLoader加载类和so就行了,因为Java层的ClassLoader是变不了的,所以我们就改变加载so的ClassLoader

1、使用主工程中的类来加载so

2、如果主工程不好添加代码的话,我们也可以在插件里改变Runtime.load()所使用的ClassLoader,但是Runtime的load方法只有一个参数的公开方法,传ClassLoader的方法是私有的,所以我们只能通过反射去传入主工程的ClassLoader

Image

通常我们只注意了Java类和ClassLoader的对应关系,JVM通过ClassLoader和类的全路径名来唯一的确定一个class,而忽略了so和ClassLoader也是有对应关系的,具有相同ClassLoader的Java类和JNI方法才能一一对应,ClassLoader其实也起到了类似命名空间的作用。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK