深入理解Android热修复技术原理之so库热修复技术

目录
  • 一、SO库加载原理
  • 二、SO库热部署实时生效可行性分析
    • 2.1、动态注册 native 方法实时生效
    • 2.2、静态注册 native 方法实时生效
    • 2.3、SO实时生效方案总结
  • 三、SO库冷部署重启生效实现方案
    • 3.1、接口调用替换方案
    • 3.2、反射注入方案
  • 四、如何正确复制补丁 SO库
  • 五、本章小结

一、SO库加载原理

Java Api 提供以下两个接口加载一个 so 库

  • System. loadLibrary (String libName):传进去的参数:so库名称, 表示的so 库文件,位于apk压缩文件中的 libs 目录,最后复制到 apk安装目录下。
  • System, load (String pathName):传进去的参数: so库在磁盘中的完整 路径。加载一个自定义外部 so库文件。

上述两种方式加载一个 so 库,实际上最后都调用 nativeLoad 这个 native方法去加载 so库,这个方法的 fileName:so 库在磁盘中的完整路径名。

代码+图文的方式简述 so 库加载原理,下面的代码示例,stringFromJNI -> Java_com_taobao_jni_MainActivity_stringFromJNI 静态注册的 native 方 法,test->test 动态注册的 native 方法。

我们知道 JNI 编程中,动态注册的 native 方法必须实现 JNI_OnLoad方法,同时实现一个JNINativeMethod [] 数组,静态注册的 native 方法必须是Java+类完整路径+方法名的格式。

总结下:

  • 动态注册的 native 方法映射通过加载 so 库过程中调用 JNI_onLoad 方法调用完成。
  • 静态注册的 native 方法映射是在该native方法第一次执行的时候才完成映射,当然前提是该 so 库已经 load 过。

二、SO库热部署实时生效可行性分析

2.1、动态注册 native 方法实时生效

前面我们分析过 so 库的加载原理,我们知道动态注册的 native方法调用一次 JNI_OnLoad 方法都会重新完成一次映射,所以我们是否只要先加载原来的 so库, 然后再加载补丁 so 库,就能完成Java层 native 方法到 native 层 patch后的新方法映射,这样就完成动态注册native 方法的 patch 实时修复。一张图说明

实测发现 art 下这样是可以做到实时生效的,但是 Dalvik下做不到实时生效,通 过代码测试我们发现,实际上Dalvik 下第二次 load补丁 so库,执行的仍然是原来so 库的 JNI_0nLoad方法,而不是补丁so 库的 JNI_OnLoad 方法,所以 Dalvik 下做不到实时生效。我们来简单分析下,既然拿到的是原来 so 库的 JNI_OnLoad方法,那么我们首先怀疑以下两个函数是否有问题。

  • dlopen() :返回给我们一个动态链接库的句柄
  • disym() :通过一个 dlopen 得到的动态连接库句柄,来查找一个 symbol

首先来看下 Dalvik 虚拟机下面 dlopen 的实现,源码在 /bionic/linker/dlfcn.cpp 文件,方法调用链路:dlopen -> do_d.lopen -> find_library -> find_library_internal

findloadedlibrary 方法判断 name 表示的 so库是否已经被加载过,如果加载过直接返回之前加载 so库的句柄,没有加载过,调用 load_library尝试加载 so库

看代码注释,也知道其实这是Dalvik虚拟机下的一个 bug,这里它是通过 basename 去做查找,传进来的参数 name 实际上是 so库所在磁盘的完整路径,比如此时修复后的so库的路径为 /data/data/com. taobao. jni/files/libnative-lib.so。但是此时是通过 bname : libnative-lib.so 作为 key 去查找, 我们知道第一次加载原来的 so库 System.loadLibrary ( "native-lib");实际上已经在solist表中存在了 native-lib 这个 key,所以 Dalvik下面加载修复后的补丁so拿到的还是原so库文件的句柄,所以执行的仍然是原来 so库的JNI_ OnLoad方法,Art下不存在这个问题,是因为Art下这个地方是以name作为key 去查找而不是bname,所以art 重新load —遍补丁 so库:拿到的是补丁 so库的句柄,然后执行补丁库的JNI OnLoad。

所以为了解决 Dalvik 下面的这个问题,那么如果尝试对补丁 so进行改名,比如 此处补丁so 库的完整路径修改之后变成 /data/data/com.taobao.jni/files/ libnative-lib-123333.so,后面一串数字是当前时间戳,确保这个 bname是全局唯一的,按照上面的分析,在solist 中查找的 key已经是唯一的,所以此时可以做到Dalvik 下面动态注册的 native 方法的实时生效。

2.2、静态注册 native 方法实时生效

上面通过尝试对补丁 so库进行重命名为全局唯一的名称可以确保第二次加载补丁so 库可以做到 Dalvik 下和 Art下动态注册方法的实时生效,但要做到静态注册 native 方法的实时生效还需要更多工作。

前面我们说过静态注册 native 方法的映射是在 native方法第一次执行的时候就完成了映射,所以如果native方法在加载补丁 so 库之前已经执行过了,那么是否这种时候这个静态注册的 native 方法一定得不到修复?幸运的是,系统 JNI API提供 了解注册的接口。

UnregisterNatives 函数会把 jclazz 所在类的所有 native 方法都重新指向为 dvmResolveNativeMethod,所以调用 UnregisterNatives 之后不管是静态注册还是动态注册的native方法之前是否执行过在加载补丁 so的时候都会重新去做映射。所以我们只需要以下调用。

这里有一个难点,因为 native 方法的修改是在 so库中,所以我们的补丁工具很难检测出到底是哪个Java 类需要解注册 native 方法。这个问题暂且放下。假设我们能知道哪个类需要解注册native方法,然后 load补丁 so库之后,再次执行该 native 方法,这样看起来是可以让该 native方法实时生效,但是测试发现,在补丁 so 库重命名的前提下,java 层 native 方法可能映射到原so库的方法,也可能映射到补丁 so 库的修复后的新方法。

首先静态注册的 native方法之前从未执行,首先尝试解析该方法。或者调用了 unregisterJNINativeMethods 解注册方法,那么该方法将指向 meth->nativeFunc = dvmResolveNativeMethod,那么真正运行该方法的时候,实际上执行的是dvmResolveNativeMethod 函数。这个函数主要完成 java 层 native方法和native 层方法的映射逻辑。

gDvm.nativeLibs 是一个全局变量,它是一个hashtable,存放着整个虚拟机加载 so库的 SharedLib 结构指针。然后该变量作为参数传递给 dvmHashForeach 函数进行 hashtable 遍历。执行 findMethodInLib 函数看是否找到对应的 native函 数指针,如果第一个找到就直接return,不在进行下次的查找。

这个结构很重要,在虚拟机中大量使用到了 hashtable 这个数据结构,hashtable 的实现源码在 dalvik/vm/Hash.h 和 dalvik/vm/Hash.cpp 文件中,有兴趣可以自行查看源码,这里不进行详细分析。hashtable的遍历和插入都是在 dvmHashTableLookup 方法中实现,简单说下 java.hashtable 和 c.hashtable 的异同点:

  • 共同点:两者实际上都是数组实现,hashtable容量如果超过默认值都会进行扩容,都是对key进行hash计算然后跟hashtable的长度进行取模作为 bucket。
  • 不同点:Dalvik 虚拟机下 hashtable put/get操作实现方法,实际上实现要 比java hashmap 的实现要简单一些,java hashmap 的 put实现需要处理 hash冲突的情况,一般情况下会通过在冲突节点上新增一个链表处理冲突, 然后get实现会遍历这个链表通过equals方法比较value是否一致进行查找,davlik 下 hashtable 的 put 实现上 (doAdd=true) 只是简单的把指针 下移直到下一个空节点。get 实现 (doAdd=false) 首先根据 hash值计算出 bucket 位置,然后通过 cmpFunc函数比较值是否一致,不一致,指针下移。 hashtable 的遍历实际就是数组遍历实现

知道了 davlik 下 hashtable的实现原理,那我们再来看下前面提到的:补丁 so库重命名的前提下,为什么 java 层 native 方法可能映射到原 so 库的方法也可能映射到补丁 so库的修复后的新方法。一张图说明情况

所以我们可以得到结论:

对补丁 so库进行重命名后,如果这个补丁 so库在hashtable中的位置比原 so库的位置靠前,那么这个静态注册native方法就能够得到修复,位置如果靠后就得不到修复。

2.3、SO实时生效方案总结

基于上面的分析,so库的实时生效必须满足以下几点:

  • so库为了兼容Dalvik虚拟机下动态注册native方法的实时生效,必须对so 文件进行改名。
  • 针对so库静态注册native方法的实时生效,首先需要解注册静态注册的 native方法,这个也是难点,因为我们很难知道so库中哪几个静态注册的 native方法发生了变更。假设就算我们知道如果静态注册的native方法需要解注册,重新load补丁 so库也有可能被修复也有可能不被修复。
  • 上面对补丁 so进行了第二次加载,那么肯定是多消耗了一次本地内存,如果 补丁 so库够大,补丁 so够多,那么JNI层的OOM也不是没可能
  • 另外一方面补丁 so如果新增了一个动态注册的方法而dex中没有相应方法, 直接去加载这个补丁 so文件会报NoSuchMethodError异常,具体逻辑在 dvmRegisterJNIMethod中。我们知道如果dex如果新增了—native 方法,那么走不了热部署只能冷启动重启生效,所以此时补丁so就不能第二 次load 了。这种情况下so库的修复严重依赖于dex的修复方案。

可以看到 so库实时生效方案,对于静态注册的native方法有一定的局限性, 不能满足一般的通用性,所以最后我们放弃了 so库的实时生效需求,转而求次实现 so库修复的冷部署重启生效方案。

三、SO库冷部署重启生效实现方案

为了更好的兼容通用性,我们尝试通过冷部署重启生效的角度分析下补丁 so库的修复方案。

3.1、接口调用替换方案

sdk提供接口替换System默认加载so库接口

SOPatchManager.loadLibrary(String libName) -> System.loadLibrary(String libName) 

SOPatchManager.loadLibrary接口加载 so库的时候优先尝试去加载sdk 指定目录下的补丁so,加载策略如下:

如果存在则加载补丁 so库而不会去加载安装apk安装目录下的so库

如果不存在补丁so,那么调用System.loadLibrary去加载安装apk目录下的 so库。

我们可以很清楚的看到这个方案的优缺点:

  • 优点:不需要对不同 sdk 版本进行兼容,因为所有的 sdk 版本都有 System.loadLibrary 这个接口。
  • 缺点:调用方需要替换掉 System 默认加载 so 库接口为 sdk提供的接口, 如果是已经编译混淆好的三方库的so 库需要 patch,那么是很难做到接口的替换。

虽然这种方案实现简单,同时不需要对不同 sdk版本区分处理,但是有一定的局限性没法修复三方包的so库同时需要强制侵入接入方接口调用,接着我们来看下反射注入方案。

3.2、反射注入方案

前面介绍过 System. loadLibrary ( "native-lib"); 加载 so库的原理,其实native-lib 这个 so 库最终传给 native 方法执行的参数是 so库在磁盘中的完整路径,比如:/data/app-lib/com.taobao.jni-2/libnative-lib.so, so库会在 DexPathList.nativeLibraryDirectories/nativeLibraryPathElements 变量所表示的目录下去遍历搜索。

sdk<23 DexPathList.findLibrary 实现如下

可以发现会遍历 nativeLibraryDirectories数组,如果找到了 loUtils.canOpenReadOnly (path)返回为 true, 那么就直接返回该 path, loUtils.canOpenReadOnly (path)返回为 true 的前提肯定是需要 path 表示的 so文件存 在的。那么我们可以采取类似类修复反射注入方式,只要把我们的补丁so库的路径插入到nativeLibraryDirectories数组的最前面就能够达到加载so库的时候是补丁 库而不是原来so库的目录,从而达到修复的目的。

sdk>=23 DexPathList.findLibrary 实现如下

sdk23 以上 findLibrary 实现已经发生了变化,如上所示,那么我们只需要把补丁so库的完整路径作为参数构建一个Element对象,然后再插入到nativeLibraryPathElements 数组的最前面就好了。

  • 优点:可以修复三方库的so库。同时接入方不需要像方案1 —样强制侵入用 户接口调用
  • 缺点:需要不断的对 sdk 进行适配,如上 sdk23 为分界线,findLibrary接口实现已经发生了变化。

我们知道在不管是在补丁包中还是 apk 中一个 so 库都存在多种 cpu 架构的 so 文件,比如"armeabi","arm64-v8a","x86"等。加载肯定是加载其中一个 so库文件的,如何选择机型对应的 so 库文件将是重点所在。

四、如何正确复制补丁 SO库

上面提到的一个问题,这里不打算详细介绍。有需要的参考文档:Android动态 链接库加载原理及HotFix方案介绍,这篇文档有些观点不尽正确,但是我也能知道虚拟机究竟选择哪个abis目录作为参数构建PathClassLoader对象,一张图简单了解下原理:

实际上补丁 so也存在类似的问题,我们的补丁 so库文件放到补丁包的libs目录下面,libs目录和.dex文件和res资源文件一起打包成一个压缩文件作为最后的补丁包,libs目录可能也包含多种abis目录。所以我们需要选择手机最合适的 primaryCpuAbi,然后从libs目录下面选择这个primaryCpuAbi子目录插入到 nativeLibraryDirectories/nativeLibraryPathElements 数组中。所以怎么选择primaryCpuAbi是关键,来看下我们sdk具体的实现

  • sdk>=21 时,直接反射拿到 Applicationinfo 对象的 primaryCpuAbi 即可
  • sdk<21 时,由于此时不支持 64 位,所以直接把Build.CPU_ABI, Build.CPU_ABI2 作为 primaryCpuAbi 即可

五、本章小结

对于 so库的修复方案目前更多采取的是接口调用替换方式,需要强制侵入用户 接口调用。目前我们的so文件修复方案采取的是反射注入的方案,重启生效。具有更好的普遍性。如果有so文件修复实时生效的需求,也是可以做到的,只是有些限制情况。

以上就是深入理解Android热修复技术原理之so库热修复技术的详细内容,更多关于Android so库热修复的资料请关注我们其它相关文章!

(0)

相关推荐

  • 深入理解Android热修复技术原理之代码热修复技术

    一.底层热替换原理 1.1.Andfix 回顾 我们先来看一下,为何唯独 Andfix 能够做到即时生效呢? 原因是这样的,在 app运行到一半的时候,所有需要发生变更的分类已经被加载过了,在Android 上是无法对一个分类进行卸载的.而腾讯系的方案,都是让 Classloader去加载新的类.如果不重启,原来的类还在虚拟机中,就无法加载新类.因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve 为新的类.从而达到热修复的目的. An

  • 深入理解Android热修复技术原理之资源热修复技术

    一.普遍的实现方式 目前市面上的很多资源热修复方案基本上都是参考了 Instant Run的实现. 简要说来,Instant Run中的资源热修复分为两步: 1.构造一个新的 AssetManager,并通过反射调用 addAssetPath,把这个完 整的新资源包加入到AssetManager中.这样就得到了一个含有所有新资源的 AssetManager. 2.找到所有之前引用到原有 AssetManager的地方,通过反射,把引用处替换 为 AssetManager. 一个 Android

  • Android热修复Tinker接入及源码解读

    一.概述 热修复这项技术,基本上已经成为项目比较重要的模块了.主要因为项目在上线之后,都难免会有各种问题,而依靠发版去修复问题,成本太高了. 现在热修复的技术基本上有阿里的AndFix.QZone的方案.美团提出的思想方案以及腾讯的Tinker等. 其中AndFix可能接入是最简单的一个(和Tinker命令行接入方式差不多),不过兼容性还是是有一定的问题的:QZone方案对性能会有一定的影响,且在Art模式下出现内存错乱的问题(其实这个问题我之前并不清楚,主要是tinker在MDCC上指出的);

  • 深入理解Android热修复技术原理之so库热修复技术

    目录 一.SO库加载原理 二.SO库热部署实时生效可行性分析 2.1.动态注册 native 方法实时生效 2.2.静态注册 native 方法实时生效 2.3.SO实时生效方案总结 三.SO库冷部署重启生效实现方案 3.1.接口调用替换方案 3.2.反射注入方案 四.如何正确复制补丁 SO库 五.本章小结 一.SO库加载原理 Java Api 提供以下两个接口加载一个 so 库 System. loadLibrary (String libName):传进去的参数:so库名称, 表示的so 库

  • 4种VPS主机技术原理及优缺点(VPS独享主机技术原理)

    VPS独享主机一直是中小企业和中高端站长用户的最佳建站选择,而且,随着云计算技术的应用和发展,VPS主机价格也愈来平民化,使得更多的人们接触到VPS主机,和经常使用VPS主机.同时,VPS独享主机.虚拟专用服务器的原理和相关技术也就被人们不断的了解,也不再那么神秘. VPS独享主机作为一种虚拟化方案,有全虚拟化.半虚拟化.操作系统虚拟化三种分类.VPS主机是通过虚拟化技术实现的虚拟主机,虚拟化是一个抽象层,它将物理硬件与操作系统分开,从而提供更高的IT资源利用率和灵活性. 4种VPS主机虚拟技术

  • 详解Android中实现热更新的原理

    这篇文章就来介绍一下Android中实现热更新的原理. 一.ClassLoader 我们知道Java在运行时加载对应的类是通过ClassLoader来实现的,ClassLoader本身是一个抽象来,Android中使用PathClassLoader类作为Android的默认的类加载器,PathClassLoader其实实现的就是简单的从文件系统中加载类文件.PathClassLoade本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法

  • Android、iOS和Windows Phone中的推送技术详解

    推送并不是什么新技术,这种技术在互联网时代就已经很流行了.只是随着进入移动互联网时代,推送技术显得更加重要.因为在智能手机中,推送从某种程度上,可以取代使用多年的短信,而且与短信相比,还可以向用户展示更多的信息(如图像.表格.声音等). 推送技术的实现通常会使用服务端向客户端推送消息的方式.也就是说客户端通过用户名.Key等ID注册到服务端后,在服务端就可以将消息向所有活动的客户端发送. 实际上,在很多移动操作系统中,官方都为其提供了推送方案,例如,Google的云推送.IOS.Windows

  • 一文理解Android系统中强指针的实现

    强指针和弱指针基础 android中的智能指针包括:轻量级指针.强指针.弱指针. 强指针:它主要是通过强引用计数来进行维护对象的生命周期. 弱指针:它主要是通过弱引用计数来进行维护所指向对象的生命周期. 如果在一个类中使用了强指针或者弱指针的技术,那么这个类就必须从RefBase这个类进行做继承,因为强指针和弱指针是通过RefBase这个类来提供实现的引用计数器. 强指针和弱指针关系相对于轻量级指针来说更加亲密,因此他们一般是相互配合使用的. 强指针原理分析 以下针对源码的分析都是来源于andr

  • 浅析JSONP技术原理及实现

    跨域问题一直是前端中常见的问题,每当说到跨域,第一浮现的技术必然就是JSONP JSONP在我的理解,它并不是ajax,它是在文档中插入一个script标签,创建_callback方法,通过服务器配合执行_callback方法,并传入一些参数 JSONP的局限就在于,因为是通过插入script标签,所以参数只能通过url传入,因此只能满足get请求,特别jQuery的ajax方法时,即使设置type: 'POST',但是只要设置了dataType: 'jsonp',在请求时,都会自动使用GET请

  • 深入解析php模板技术原理【一】

    1.模板的由来 在没有模板技术之前,使用PHP开发程序,通常都是php代码和html混编在一起.比如说新闻列表,很可能就是一个newslist.php页面,结构如下: <?  //从数据库中读取出要显示的新闻记录  ?>  <html>  <head>--..  </head>  <body>  <?   While ($news = mysql_fetch_array($result)) {  ?>  <!--输出新闻标题  

  • 深入理解jQuery()方法的构建原理

    前言 虽然JQuery相对简单,但要全面掌握,且快速灵活的使用它也并不那么容易,它提供了很多方法,包含了网页开发的各个知识面,所以要全面掌握这些知识点,个人认为还是需要对jquery有深入的理解,对这些知识点做分类整理记忆,这样你才能面对一些JQuery代码的时候不会感到迷惑,才会知道采用何种方式实现某个特效是最佳实践,才能快速的采用JQuery来进行项目开发. jQuery中最常用方法的就是jQuery( ) ,也即$( ) . jQuery( )是一个函数调用,调用的结果是返回了一个jQue

随机推荐