一个 ClassLoader 引起的 JNI 链接错误

作者:Rayszhang

前言

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

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

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

so加载流程分析

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

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

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

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

该函数较长,但逻辑还是很清晰的,我们只列出了关键代码,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字段保存了该方法的跳转地址

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

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

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

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

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

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

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

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

一点思考

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

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏PPV课数据科学社区

适用于 PHP 开发人员的 Python 基础知识

您是一名 PHP 开发人员。您在过去 五年(或更长时间)中可能一直都编写应用程序,您已经将许多想像变成了可能 — 电子商务系统、简单内容管理系统、Twitte...

39615
来自专栏祝威廉

Spark Tungsten in-heap / off-heap 内存管理机制前言

分析方式基本是自下而上,也就是我们分析的线路会从最基础内存申请到上层的使用。我们假设你对sun.misc.Unsafe 的API有一些最基本的了解。

1193
来自专栏精讲JAVA

11 个简单的 Java 性能调优技巧

大多数开发人员理所当然地以为性能优化很复杂,需要大量的经验和知识。好吧,不能说这是完全错误的。优化应用程序以获得最佳性能不是一件容易的事情。但是...

2128
来自专栏码神联盟

性能优化 | Java性能调优准则-攻略1

大家在写代码的时候是不是都只考虑了实现,没有考虑性能呢?如果说你只是做业务系统的增删改查并且业务量不大的话。这是毋庸置疑的。但是如果你在较大吞吐量和较小的资源的...

3697
来自专栏向治洪

Android热修复技术总结

插件化和热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高级开发中必须掌握的技能,插件化的知识可以查我我之前的介绍:Android插件化。本...

3717
来自专栏Java帮帮-微信公众号-技术文章全总结

【大牛经验】关于堆和栈的那些事

问题描述 编程语言书籍中经常解释值类型被创建在栈上,引用类型被创建在堆上,但是并没有本质上解释这堆和栈是什么。我仅有高级语言编程经验,没有看过对此更清晰的解释。...

3739
来自专栏mini188

学习笔记:内存,堆栈,到底为何物?

     在网上看到了一篇关于面试的博客文,突然发现自己对于这个博主而言简直差的是十万八千里,他提到的许多技术我尽然一个也答不上来。于是就开始反思,还是要抱一抱...

2416
来自专栏决胜机器学习

有趣的算法(十) ——归并排序思想解决大量用户数据清洗

有趣的算法(十)——归并排序思想解决用户数据清洗 (原创内容,转载请注明来源,谢谢) 一、问题阐述 近期工作中接触到一个很有趣的算法,在此进行分享。 当前有...

3599
来自专栏java一日一条

11 个简单的 Java 性能调优技巧

大多数开发人员理所当然地以为性能优化很复杂,需要大量的经验和知识。好吧,不能说这是完全错误的。优化应用程序以获得最佳性能不是一件容易的事情。但是,这并不意味着如...

1082
来自专栏韩伟的专栏

实用主义编程规范:JAVA篇

JAVA代码规范 1.规范说明 此规范包含:避免出现常见恶劣代码的禁令;指导编写合格代码的基本规则 此规范不包含:分析与设计出符合业务需求的代码; 2.基本原则...

4566

扫码关注云+社区

领取腾讯云代金券