摘要
深入探索Android热修复技术,揭开SO库热修复的神秘面纱。让我们一起感受技术的魅力,不超过50个字,留给读者自由想象。
正文
深层次探寻Android热修复技术性基本原理阅读笔记 —— so库热修复技术性
热修复系列产品文章内容:
深层次探寻Android热修复技术性基本原理阅读笔记 —— 热修复技术性详细介绍
深层次探寻Android热修复技术性基本原理阅读笔记 —— 编码热修复技术性
深层次探寻Android热修复技术性基本原理阅读笔记 —— 資源热修复技术性
1. SO库载入基本原理
Java Api 给予下列2个插口载入一个 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 过。
2. 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 方法,那麼大家最先猜疑下列2个涵数是不是有什么问题。
-
• dlopen() :回到给大家一个动态链接库的返回值
-
• disym() :根据一个 dlopen 获得的动态性联接库返回值,来搜索一个 symbol
最先看来下 Dalvik vm虚拟机下边 dlopen 的完成,源代码在 /bionic/linker/dlfcn.cpp 文档,方式 启用链接:dlopen -> do_d.lopen -> find_library -> find_library_internal
findloadedlibrary 方式 分辨 name 表明的 so 库是不是早已被载入过,假如载入过立即回到以前载入 so 库的返回值,沒有载入过,启用 load_library 试着加载 so 库
看代码注释,也了解实际上它是Dalvikvm虚拟机下的一个 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,储放着全部vm虚拟机载入 so 库的 SharedLib 构造表针。随后该自变量做为参数传递给 dvmHashForeach 涵数开展 hashtable 解析xml。实行 findMethodInLib 涵数看是不是寻找相匹配的 native 函 数表针,假如第一个寻找就立即 return,没有开展下一次的搜索。
这一构造很重要,在vm虚拟机中很多应用到 hashtable 这一算法设计,hashtable 的完成源代码在 dalvik/vm/Hash.h 和 dalvik/vm/Hash.cpp 文档中,有兴趣爱好能够 自主查询源代码,这儿不开展深入分析。hashtable 的解析xml和插进全是在 dvmHashTableLookup 方式 中完成,简易说下 java.hashtable 和 c.hashtable 的不同点点:
-
相同点:二者事实上全是二维数组完成,hashtable 容积假如超出初始值都是会进行扩充,全是对 key 开展 hash 测算随后跟 hashtable 的长短开展牙模型作为 bucket。
-
不同之处:Dalvik vm虚拟机下 hashtable put/get 实际操作完成方式 ,事实上完成要 比 java hashmap 的完成要简易一些,java hashmap 的 put 完成必须解决 hash矛盾的状况,一般状况下能根据在冲突连接点上增加一个链表解决矛盾, 随后get完成会解析xml这一链表根据 equals 方式 较为 value 是不是一致开展搜索,davlik 下 hashtable 的 put 完成上 (doAdd=true) 仅仅简易的把表针 下沉直至下一个空连接点。get 完成 (doAdd=false) 最先依据 hash 值测算出 bucket 部位,随后根据 cmpFunc 涵数较为值是不是一致,不一致,表针下沉。 hashtable 的解析xml具体便是数组遍历完成
知道 davlik 下 hashtable 的完成基本原理,那大家再看来下前边提及的:补丁包 so 库重新命名的前提条件下,为何 java 层 native 方式 很有可能投射到原 so 库的方式 也很有可能映射到补丁包 so 库的修补后的新方式 。一张图表明状况
因此 我们可以获得结果:
-
对补丁包 so库开展重新命名后,假如这一补丁包 so 库在 hashtable 中的部位比原 so 库的部位靠前,那麼这一静态数据申请注册 native 方式 就可以获得修补,部位如果靠后就无法得到修补。
2.3. SO 即时起效计划方案汇总
根据上边的剖析,so 库的即时起效务必达到以下几个方面:
-
so 库为了更好地兼容 Dalvik vm虚拟机下动态性申请注册 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库修补的冷布署重新启动起效计划方案。
3. 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 自变量所表明的文件目录下来解析xml检索。
sdk<23 DexPathList.findLibrary 完成以下
能够 发觉会解析xml 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 库文件将是关键所属。
4. 如何正确拷贝补丁包 SO库
上边提及的一个难题,这儿不准备详解。有必须的参照文本文档:Android动态性 链接库载入基本原理及HotFix计划方案详细介绍,这篇文本文档有一些见解不绝恰当,可是因为我能了解vm虚拟机到底选择哪一个 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 就可以
5. 此章总结
针对 so 库的修补计划方案现阶段大量采用的是插口启用更换方法,必须强制性入侵客户 插口启用。现阶段大家的 so 文件损坏修复计划方案采用的是反射面引入的计划方案,重新启动起效。具备更强的客观性。如果有 so 文件损坏修复即时起效的要求,也是能够 保证的,仅仅有一些限制状况。
关注不迷路
扫码下方二维码,关注宇凡盒子公众号,免费获取最新技术内幕!
评论0