Android热修复升级探索——SO库修复方案

摘要: 通常情况下,大多数人希望android下热补丁方案能够做到补丁的全方位修复,包括类修复/资源修复/so库的修复。 这里主要介绍热补丁之so库修复思路。

一、前言
通常情况下,大多数人希望android下热补丁方案能够做到补丁的全方位修复,包括类修复/资源修复/so库的修复。 这里主要介绍热补丁之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库热部署实时生效可行性分析
1.动态注册native方法实时生效
前面我们分析过so库的加载原理, 我们知道动态注册的native方法调用一次JNI_OnLoad方法都会重新完成一次映射, 所以我们是否只要先加载原来的so库,,然后再加载补丁so库,就能完成Java层native方法到native层patch后的新方法映射, 这样就完成动态注册native方法的patch实时修复。一张图说明:

图片描述

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

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

首先来看下Dalvik虚拟机下面dlopen的实现, 源码在/bionic/linker/dlfcn.cpp文件, 方法调用链路:dlopen-> do_dlopen -> 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库的句柄, 然后执行补丁so库的JNI_OnLoad。

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

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方法就能够得到修复, 位置如果靠后就得不到修复。

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库的修复方案。

方案1. 接口调用替换
sdk提供接口替换System默认加载so库接口

图片描述

SOPatchManager.loadLibrary接口加载so库的时候优先尝试去加载sdk指定目录下的补丁so, 加载策略如下:
如果存在则加载补丁so库而不会去加载安装apk安装目录下的so库。
如果不存在补丁so, 那么调用System.loadLibrary去加载安装apk目录下的so库。

图片描述

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

优点:不需要对不同sdk版本进行兼容, 因为所有的sdk版本都有System.loadLibrary这个接口。
缺点: 调用方需要替换掉System默认加载so库接口为sdk提供的接口, 如果是已经编译混淆好的三方库的so库需要patch, 那么是很难做到接口的替换。
虽然这种方案实现简单, 同时不需要对不同sdk版本区分处理,但是有一定的局限性没法修复三方包的so库同时需要强制侵入接入方接口调用, 所以来看下方案2. 反射注入。

方案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数组, 如果找到了IoUtils.canOpenReadOnly(path)返回为true, 那么就直接返回该path, IoUtils.canOpenReadOnly(path)返回为true的前提肯定是需要path表示的so文件存在的。 那么我们可以采取类似类修复反射注入方式, 只要把我们的补丁so库的路径插入到nativeLibraryDirectories数组的最前面就能够达到加载so库的时候是补丁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文件修复实时生效的需求, 也是可以做到的,只是有些限制情况, 详见以上分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/523227.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中小企业搭建混合云,服务器如何选?

戳蓝字“CSDN云计算”关注我们哦&#xff01;经过一年多的发展Henry所在的NewStar公司的业务开始蒸蒸日上各种业务系统也越来越多、运营管理也开始越来越复杂公司所购买的“第一台服务器”逐渐力不从心业务中断时有发生Henry又回到了公司初期一筹莫展的状况中Henry脸上也是愁云…

org.xml.sax.SAXParseException; lineNumber: 11; columnNumber: 110; schema_reference.4: 无法读取方案文档

异常信息&#xff1a; org.xml.sax.SAXParseException; lineNumber: 11; columnNumber: 110; schema_reference.4: 无法读取方案文档 ‘http://www.springframework.org/schema/beans/spring-beans-4.0.xsd’, 原因为 1) 无法找到文档; 2) 无法读取文档; 3) 文档的根元素不是 x…

基于TableStore构建简易海量Topic消息队列

摘要&#xff1a; 前言 消息队列&#xff0c;通常有两种场景&#xff0c;一种是发布者订阅模式&#xff0c;一种是生产者消费者模式。发布者订阅模式&#xff0c;即发布者生产消息放入队列&#xff0c;多个监听的消费者都会收到同一份消息&#xff0c;也就是每个消费者收到的消…

com.android.tools.build:gradle:2.3.3,关于com.android.tools.build:gradle:3.4.2的构建问题

Android Studio Version&#xff1a;3.4.2Android Gradle Plugin Version&#xff1a;3.4.2Gradle Version&#xff1a;5.1.1根据以上IDE工具以及对应的插件版本&#xff0c;搭建了一个Android 项目&#xff0c;app的build.gradle如下&#xff1a;// Top-level build file wher…

苹果ID登陆第三方有漏洞?硬核!Gartner报告腾讯云数据库增速国内第一;“小米快递”商标注册,这是要入局物流领域?...

关注并标星星CSDN云计算极客头条&#xff1a;速递、最新、绝对有料。这里有企业新动、这里有业界要闻&#xff0c;打起十二分精神&#xff0c;紧跟fashion你可以的&#xff01;每周三次&#xff0c;打卡即read更快、更全了解泛云圈精彩newsgo go go 蔚来汽车&#xff08;图片来…

【阿里云MVP月度分享】SaaS服务商如何通过数加平台统计业务流量

摘要&#xff1a; 一、概述 因为自家公司是做B2B类Saas服务的&#xff0c;难免会产生精准计费的问题&#xff0c;所以在通过多套方案的选型及对比以后&#xff0c;我们最终确定了以下的方式进行自有业务平台的流量计算方案。因为涉及到具体的操作&#xff0c;所以阅读本文的前提…

android 机顶盒 view 焦点,AndroidTV/机顶盒 ListView获取焦点与点击事件问题处理方案...

AndroidTV/机顶盒 ListView获取焦点与点击事件问题处理方案本人大二&#xff0c;最近在写一个Android机顶盒的小项目&#xff0c;遇到了这样一个问题。由于App的布局复杂&#xff0c;导致ListView用遥控器获取其Item焦点时变得很卡很慢。需要不断点击上下键才能移动焦点。看了下…

【阿里云 MVP 月度分享】宋亚奇——应用MaxCompute实现电力设备监测数据的批量特征分析...

1 背景知识 电力设备在线监测指在不停电的情况下&#xff0c;对电力设备状况进行连续或周期性地自动监视检测&#xff0c;使用的技术包括&#xff1a;传感器技术、广域通信技术和信息处理技术。电力设备在线监测是实现电力设备状态运行检修管理、提升生产运行管理精益化水平的…

小企业的第一台服务器如何选?

戳蓝字“CSDN云计算”关注我们哦&#xff01;Henry是一家叫做NewStar的初创小企业的兼职IT管理员一天晚间&#xff0c;下班后的Henry正在忙着“吃鸡”突然&#xff0c;一阵急促的电话铃声响了起来原来是在外地出差的同事需要一份重要的文件&#xff0c;但却无法远程访问服务器H…

Linux 中文无法显示或显示方块

文章目录问题现象&#xff1a;字体需求一、查看字体列表1. 安装字体库2. 字体库中添加中文字体3. 进入字体目录复制需求字体4. 创建中文字体目录5. 上传需求字体6. 修改chinese目录的权限7. 安装ttmkfdir8. 修改字体配置文件9. 刷新内存中的字体缓存问题现象&#xff1a; Linu…

阿里云发布混合云数据存储和灾备方案

摘要&#xff1a; 12月7日&#xff0c;2017苏州云栖大会上&#xff0c;阿里云发布全新的混合云数据存储和灾备方案&#xff0c;此次发布的内容包括最新推出的混合云容灾服务HDR和混合云备份服务HBR&#xff0c;以及全面升级的混合云存储阵列CSA2000和CSA3000。 12月7日&#xf…

html 基本结构

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Document</title> </head> <body></body> </html>

打通钉钉+WebHook:日志服务告警升级

摘要&#xff1a; 用一个最最常用的案例&#xff08;Nginx日志分析&#xff09;来说明当前使用场景&#xff0c;告警要解决的3个问题&#xff1a;是否有错误&#xff1b;是否有性能问题&#xff1b;是否有流量急跌或暴涨 阿里云日志服务是针对实时数据一站式服务&#xff0c;用…

AliOS Things v1.1.1新特性

摘要&#xff1a; 今年杭州云栖大会上&#xff0c;AliOS Things正式发布&#xff0c;10/20在github上开源v1.1.0版本。经过AliOS Things团队及合作伙伴1个多月的努力&#xff0c;很高兴有些更新可以和大家分享。AliOS Things v1.1.1包含了ESP32支持&#xff0c;AT框架&#xff…

Spark精华问答 | spark性能优化方法

Hadoop再火&#xff0c;火得过Spark吗&#xff1f;今天我们继续关于Spark的精华问答吧。1Q&#xff1a;影响性能的主要因素是什么&#xff1f;A&#xff1a;网络传输开销大硬件资源利用率低同一资源的复用率低2Q&#xff1a;优化的方向有哪些&#xff1f;A&#xff1a;设置数据…

android 7.0 解锁亮屏,Android7.0亮屏流程分析

亮屏的本质是改变屏幕的电源状态&#xff0c;经过一系列的调用会来到PowerManagerService中的updatePowerStateLocked()1.PowerManagerService到DisplayPowerControllerprivate void updatePowerStateLocked() {if (!mSystemReady || mDirty 0) {return;}if (!Thread.holdsLoc…

concat函数显示小数点包括0

场景1&#xff1a;当小数点大于等于3位小于5位&#xff1a; select regexp_replace(concat(nvl(round(0.0001,4),0)*100,%),^.,0.)as rate from dual;例如&#xff1a;0.001-0.0001 场景2&#xff1a;当小数点小于等于2位&#xff1a; select concat(nvl(round(0.01,4),0)*1…

阿里云Elasticsearch的X-Pack:机器学习、安全保障和可视化

摘要&#xff1a; ELK是日志分析领域较为流行的技术选择&#xff0c;不少阿里云用户选择在ECS上搭建开源Elasticsearch。与自建开源Elastisearch相比&#xff0c;阿里云Elasticsearch做了性能优化&#xff0c;支持弹性扩容&#xff0c;并搭载了商业版组件X-Pack&#xff0c;为用…

Linux部署Web应用程序超链接下载中文名称文件404问题解决办法

超链接内容如下&#xff1a; <a href"jsp/plugin/用户手册.doc">用户手册</a>开发环境为Windows&#xff0c;Tomcat和WebSphere都用过&#xff0c;超链接都能正常下载 项目生产环境为Linux&#xff0c;由于Linux默认不支持中文&#xff0c;因此超链接下…