Android线上Bug热修复分析
Hot Fix技术,简单来说就是针对线上已发布app出现了bug,在不推送新版本的情况下通过发布修复补丁进行修复。通常是刚上线的app,需要快速线上修复bug,类似的技术就叫做热修复或热补丁。
让app具有了上线后被修复的可能性,增加事故风险可控性;
避免为修复bug而快速增发新版本,让用户“无感”,提升体验;
推送新版本app修复时,用户升级覆盖面无法保证;
避免增发修复版本的复杂流程,减少发布新版本app成本;
目前,从技术解决方案上来说,有以下几种思路:
Dexposed
来自阿里手淘团队,白衣(花名)基于Xposed实现了Dexposed,在此基础上手淘团队推出了HotPatch二方库。
AndFix
出自阿里支付宝技术团队,同样是对方法的hook,但未基于Dexposed去实现,避免了在art上运行时存在兼容性问题。
基于ClassLoader
QQ空间终端开发团队提供了技术思路,目前基于此实现的热门的开源项目有Nuwa,HotFix,DroidFix,这三种方案的原理却徊然不同,各有优缺点。
热修复 == 动态替换 == 动态加载
得出上面的等式,是因为热修复一般来说就是增发patch文件,避免用户调用错误代码,并不是直接修改了原来的代码。这相当于是对问题文件做了动态替换,而要实现动态替换就是避免默认的加载,改变成动态地加载替换文件。
动态加载的基础是ClassLoader
Java程序在运行时加载对应的类是通过ClassLoader来实现的, Java 类可以被动态加载到 Java 虚拟机中并执行。所以ClassLoader所做的工作实质就是把类文件从硬盘读取到内存中。
AndFix示例图
ClassLoader
类加载器的树状结构:在JVM中,所有类加载器实例按树状结构组织,根结点为引导类加载器。除根结点外的所有类加载器都有一个非空的父类加载器,从而构成树状结构;
双亲委托(代理)模型:当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程;
类的判等:即使类完全相同(名称相同、字节码相同),不同类加载器实例加载的类对象也是不相等的;
类的垃圾回收:只有当类加载器可被作为垃圾回收的前提下,其加载的类才有可能被回收;
Android的Dalvik/ART虚拟机如同标准JAVA的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此可以利用这一点,在程序运行时手动加载Class,从而达到代码中动态加载可执行文件的目的。
Android的ClassLoader体系
在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。
此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。
下面实际验证看看:
输出结果为:
可以看见有2个Classloader实例,一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader(应用启动时创建的,用于加载当前已安装app里面的类)。
Android经常使用的是PathClassLoader和DexClassLoader
PathClassLoader
可以看出,Android是使用这个类作为其系统类和应用类的加载器。并且对于这个类呢,只能去加载已经安装到Android系统中的apk文件。
DexClassLoader
也就是说可以加载比如sd目录下的dex文件,获取到不是已安装app里面的类。
Android中使用PathClassLoader类作为Android的默认的类加载器,PathClassLoade本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。
看源码可知,BaseDexClassLoader将findClass方法委托给了pathList对象的findClass方法,pathList对象是在BaseDexClassLoader的构造函数中new出来的,它的类型是DexPathList。看下DexPathList.findClass源码是如何做的:
直接就是遍历dexElements列表,然后通过调用element.dexFile对象上的loadClassBinaryName方法来加载类,如果返回值不是null,就表示加载类成功,会将这个Class对象返回。而且dexElements对象是在DexPathList类的构造函数中完成初始化的。
makeDexElements所做的事情就是遍历我们传递来的dexPath,然后一次加载每个dex文件。可以看出,BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的集合dexElements,而对于类加载,就是遍历这个集合,通过DexFile去寻找。
这样的话,我们可以在这个dexElements中去做一些事情,比如在这个数组的第一个元素放置我们的patch.jar,里面包含修复过的类。当遍历findClass的时候,修复的类就会被查找到,从而替代有bug的类。
标准JVM中,ClassLoader是用defineClass加载类的,而Android中defineClass被弃用了,改用了loadClass方法,而且加载类的过程也挪到了DexFile中,在DexFile中加载类的具体方法也叫defineClass
使用ClassLoader的一个特点就是,当ClassLoader在成功加载某个类之后,会把得到类的实例缓存起来。下次再请求加载该类的时候,ClassLoader会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,如果程序不重新启动,加载过一次的类就无法重新加载。
除了使用ClassLoader外,还可以使用jni hook的方式修改程序的执行代码。后者做的已经是Native层级的工作了,直接修改应用运行时的内存地址,所以使用jni hook的方式时,不用重新应用就能生效。
而阿里的dexposed和AndFix采用了jni hook方案
Android程序比起一般Java程序在使用动态加载时麻烦在哪里
使用ClassLoader动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的Java程序,在Android程序中使用动态加载主要有两个麻烦的问题:
Android中许多组件类(如Activity、Service等)是需要在Manifest文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作;
Res资源是Android开发中经常用到的,而Android是把这些资源用对应的R.id注册好,运行时通过这些ID从Resource实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到R.id的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的Resource实例中保存的资源ID对不上;
说到底,一个Android程序和标准的Java程序最大的区别就在于他们的上下文环境(Context)不同。
能够简单地集成热修复sdk,开发者修改代码后能轻松地完成向用户发Patch操作,在用户无感知的情况下修复bug。
对开发者友好,使用热修复要简单直接,能尽快解决问题;
对用户友好,尽量减少用户感知;
减小bug的影响,尽量扩大修复时覆盖的用户范围。
就一个理念:只有适合当前情况的才是最好的。
前面关于Android中ClassLoader的介绍,Android使用PathClassLoader作为其类加载器,DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件。
如果大家对于插件化有所了解,其实Android应用的插件化,就可以利用DexClassLoader来动态加载非安装应用的类来实现,当然也就可以做到只有单用户点击相应插件模块,才会从网络获取相应插件文件,再通过DexClassLoader实现类加载。
而热修复可以利用BaseDexClassLoader中的pathList对象,pathList中包含一个DexFile的集合dexElements,我们可以在这个dexElements中去做一些事情,比如在这个数组的第一个元素放置我们的patch.jar,里面包含修复过的类。
上面分析了Android中的类的加载的流程,可以看出:
DexPathList对象中的dexElements列表是类加载的一个核心,一个类如果能被成功加载,那么它的dex一定会出现在dexElements所对应的dex文件中。
exElements中出现的顺序也很重要,在dexElements前面出现的dex会被优先加载,一旦Class被加载成功,就会立即返回。
我们的如果想做hot fix,一定要保证我们的pacth dex文件出现在dexElements列表的前面。
另外,构造我们自己的dex文件所对应的dexElements数组的时候,我们也可以采取一个比较取巧的方式:
通过构造一个DexClassLoader对象来加载我们的dex文件
调用一次dexClassLoader.loadClass(dummyClassName)方法
这样dexClassLoader.pathList.dexElements中就会包含我们的dex
通过分析三者的差异化对比,以及思考到底什么才是合适的,通过hook方法的方式实现起来确实最直接,但是问题却也很明显,首先成功覆盖率和稳定性是个问题,而且操作起来复杂性比较高。
而通过classloader考虑的是从系统动态加载的特性入手,所以理所当然以局限于系统的特性,比如由于对于已经加载的类,类加载器不会再调用loadClass方法,所以想要修复要等到下次启动程序才行。
Android项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种:
1.动态加载so库;
2.动态加载dex/jar/apk文件(通常都是这种)
1.动态调用外部的Dex文件则是完全没有问题的。
2.在APK文件中往往有一个或者多个Dex文件,我们写的每一句代码都会被编译到这些文件里面。
3.Android应用运行的时候就是通过执行这些Dex文件完成应用的功能的。
4.虽然一个APK一旦构建出来,我们是无法更换里面的Dex文件,但是我们可以通过加载外部的Dex文件来实现。
因此最极端的情况就是,直接把APK自身带有的Dex文件当做空壳,只是作为一个程序的入口,所有的功能都通过从服务器下载最新的Dex文件完成。
当然,一般来说只要利用Android动态加载技术,通过动态加载新的dex的方式,完成对有bug类的“替换”,来达到避免调用存在bug的代码,这也就是所谓的Hot Fix。
本文是由 石先 ,源码地址:
https://github.com/baishixian
有兴趣的同学可查看Github 源码欢迎 star & fork,如果您觉得不错,可以分享给小伙伴哦,支持小编也可以在下方+1,投稿及有疑问或者问题的小伙伴可以在下方留言,小编会第一时间与您联系!
Android线上Bug热修复分析的更多相关文章
- 听说”双11”是这么解决线上bug的
听说"双11"是这么解决线上bug的 --Android线上热修复的使用与原理 预备知识和开发环境 Android NDK编程 AndFix浅析 Android线上热修复的原理大同 ...
- 线上bug或故障界定及填写规范
[线上故障与线上Bug界定] 一.线上故障: 1. 故障参照公司规范稍做调整: a) 1级故障:资讯首页或主App首页无法打开:多条业务线同时不可用:超过15分钟: b) ...
- 线上bug的解决方案--带来的全新架构设计
缘由 本人从事游戏开发很多年一直都是游戏服务器端开发. 因为个人原因吧,一直在小型公司,或者叫创业型团队工作吧.这样的环境下不得不逼迫我需要什么都会,什么做. 但是自我感觉好像什么都不精通..... ...
- 程序员如何描述清楚线上bug
案例 一个管理后台的bug,把操作记录中的操作员姓名,写成了该操作员的id.原因是修改了一个返回操作人姓名的函数,返回了操作人的id.但是还有其他地方也用这个函数,导致其他地方把姓名字段填写成了操作员 ...
- 线上bug分析
昨天下午大神把组内几十号人召集在一起开Online bug分析大会,主要是针对近期线上事故从事故原因和解决方案两个维度来分析. 对金融软件来说,每一次的线上事故都有可能给公司带来重大的损失,少扣了用户 ...
- 记一次线上bug排查-quartz线程调度相关
记一次线上bug排查,与各位共同探讨. 概述:使用quartz做的定时任务,正式生产环境有个任务延迟了1小时之久才触发.在这一小时里各种排查找不出问题,直到延迟时间结束了,该任务才珊珊触发.原因主要就 ...
- 关于线上bug
之所以想写下线上bug,因为发觉有些公司对线上bug的处理是比较严格甚至是很苛刻,涉及到的相关人可能会因此而背黑锅. 之所以会存在这样情况,因为公司各部门都有关联,特别是用户.老板的投诉,也给公司会造 ...
- 记录一次线上bug
记录一次线上bug,总的来说就是弱网和重复点击.特殊值校验的问题. 测试场景一: 在3g网络或者使页面加载速度需要两秒左右的时候,输入学号,提交学生的缴费项目,提交完一个 学生的缴费后, ...
- 「日常开发」记一次因使用Date引起的线上BUG处理
生活中,我们需要掌控自己的时间,减少加班,提高效率:日常开发中,我们需要操作时间API,保证效率.安全.稳定.现在都2020年了,了解如何在JDK8及以后的版本中更好地操控时间就很有必要,尤其是一次线 ...
随机推荐
- CSS继承、层叠和特殊性
1.继承 (1)样式应用于某个特定的HTML标签元素,而且应用于其后代. (2)但某些标签不适用,如border: (3)例子:p{color:red;}设置了颜色 <p class=" ...
- linux服务器配置pyspark解决py4j报错等问题
1.下载spark,python包 略 2.环境变量配置 打开 ~/.bashrc配置文件 如图添加下列环境变量及path 3.退出配置文件,输入 source ~/.bashrc 来执行你添加的一些 ...
- bzoj1193: [HNOI2006]马步距离
1193: [HNOI2006]马步距离 Time Limit: 10 Sec Memory Limit: 162 MB Description 在国际象棋和中国象棋中,马的移动规则相同,都是走&q ...
- spring之构造注入
第一种:通过构造name和value属性(不常用) <!-- userAction --> <bean id="userAction" class="c ...
- 使用FileReader实现前端预览所选图片
需求描述 在浏览器环境下进开发网站(也就是B/S架构的应用),获取到当前设备上的图片后,希望可以在上传到服务器前简单预览一下图片内容. 具体实现 主要代码 html 部分 <input type ...
- BZOJ 1434: [ZJOI2009]染色游戏
一开始想这不$SG$裸题...然后发现100组数据...然后发现连通块是任意的求$SG$貌似要暴力枚举.... 然后想了一下1维,手动打表,每次就是队当前所有异或后缀和求$mex$,好像就是$lowb ...
- 利用Needleman–Wunsch算法进行DNA序列全局比对
生物信息学原理作业第二弹:利用Needleman–Wunsch算法进行DNA序列全局比对. 具体原理:https://en.wikipedia.org/wiki/Needleman%E2%80%93W ...
- ES6中let和闭包
在开始本文之前我们先来看一段代码 for(var i=0;i<10;i++){ arr[i]=function(){ return i; } } console.log(arr[3]());// ...
- 【css3】旋转倒计时
很多答题的H5界面上有旋转倒计时的效果,一个不断旋转减少的动画,类似于下图的这样. 今天研究了下,可以通过border旋转得到.一般我们可以通过border得到一个四段圆. See the Pen c ...
- 整理的linux面试运维题
如何在非交互模式下把 /home/example/下所有.conf文件中的 192.168.0.2 改成 db01 ? find /home/example/ -type f -name &quo ...