Android线上Bug热修复分析

发表于:2016-7-05 13:23

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:keep_thinking    来源:51Testing软件测试网采编

分享:
  Android的classLoader机制
  Android的Dalvik/ART虚拟机如同标准JAVA的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此可以利用这一点,在程序运行时手动加载Class,从而达到代码中动态加载可执行文件的目的。
  
Android的ClassLoader体系.png
  在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。
  此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。
  下面实际验证看看:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClassLoader classLoader = getClassLoader();
Log.i("ClassLoader" , "classLoader " + classLoader.toString());
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
if (classLoader != null) {
Log.i("ClassLoader", "classLoaderParent " + classLoader.toString());
}
}
}
  输出结果为:
  I/ClassLoader: classLoader dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sunteng.classloader-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
  I/ClassLoader: classLoaderParent java.lang.BootClassLoader@2d0a3af7
  可以看见有2个Classloader实例,一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader(应用启动时创建的,用于加载当前已安装app里面的类)。
  Android经常使用的是PathClassLoader和DexClassLoader
  PathClassLoader
  官方注释:一个简单的ClassLoader的实现,工作在本地文件系统中的文件和目录的列表上,但不尝试从网络加载类。 Android使用这个类为它的系统类加载器和应用类加载器。
  可以看出,Android是使用这个类作为其系统类和应用类的加载器。并且对于这个类呢,只能去加载已经安装到Android系统中的apk文件。
  DexClassLoader
  官方注释:一个ClassLoader的实现,从.jar和.apk文件内部加载classes.dex。这可以用于执行非安装程序作为已安装应用程序的一部分的代码。
  也就是说可以加载比如sd目录下的dex文件,获取到不是已安装app里面的类。
  Android中使用PathClassLoader类作为Android的默认的类加载器,PathClassLoade本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。
#BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
  看源码可知,BaseDexClassLoader将findClass方法委托给了pathList对象的findClass方法,pathList对象是在BaseDexClassLoader的构造函数中new出来的,它的类型是DexPathList。看下DexPathList.findClass源码是如何做的:
#DexPathList
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
  直接就是遍历dexElements列表,然后通过调用element.dexFile对象上的loadClassBinaryName方法来加载类,如果返回值不是null,就表示加载类成功,会将这个Class对象返回。而且dexElements对象是在DexPathList类的构造函数中完成初始化的。
  this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
  makeDexElements所做的事情就是遍历我们传递来的dexPath,然后一次加载每个dex文件。可以看出,BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的集合dexElements,而对于类加载,就是遍历这个集合,通过DexFile去寻找。
  这样的话,我们可以在这个dexElements中去做一些事情,比如在这个数组的第一个元素放置我们的patch.jar,里面包含修复过的类。当遍历findClass的时候,修复的类就会被查找到,从而替代有bug的类。
  一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找
  标准JVM中,ClassLoader是用defineClass加载类的,而Android中defineClass被弃用了,改用了loadClass方法,而且加载类的过程也挪到了DexFile中,在DexFile中加载类的具体方法也叫defineClass
  ClassLoader特性
  使用ClassLoader的一个特点就是,当ClassLoader在成功加载某个类之后,会把得到类的实例缓存起来。下次再请求加载该类的时候,ClassLoader会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,如果程序不重新启动,加载过一次的类就无法重新加载。
  如果使用ClassLoader来动态升级APP或者动态修复BUG,都需要重新启动APP才能生效。
  除了使用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)不同。
  Android中context可以给程序提供组件需要用到的功能,也可以提供一些主题、Res等资源,而现在的各种Android动态加载框架中,核心要解决的东西也正是如何给外部的新类提供上下文环境的问题。
  希望最终的效果
  能够简单地集成热修复sdk,开发者修改代码后能轻松地完成向用户发Patch操作,在用户无感知的情况下修复bug。
  技术选型
  对开发者友好,使用热修复要简单直接,能尽快解决问题;
  对用户友好,尽量减少用户感知;
  减小bug的影响,尽量扩大修复时覆盖的用户范围。
  就一个理念:只有适合当前情况的才是最好的。
  插件化和热修复
  前面关于Android中ClassLoader的介绍,Android使用PathClassLoader作为其类加载器,DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件。
  如果大家对于插件化有所了解,其实Android应用的插件化,就可以利用DexClassLoader来动态加载非安装应用的类来实现,当然也就可以做到只有单用户点击相应插件模块,才会从网络获取相应插件文件,再通过DexClassLoader实现类加载。
  而热修复可以利用BaseDexClassLoader中的pathList对象,pathList中包含一个DexFile的集合dexElements,我们可以在这个dexElements中去做一些事情,比如在这个数组的第一个元素放置我们的patch.jar,里面包含修复过的类。
  这样的话,当遍历findClass的时候,我们修复的类就会被查找到,从而替代有bug的类。不过这样处理还存在一个CLASS_ISPREVERIFIED的问题安卓App热补丁动态修复技术介绍。
  热修复具体实施
  上面分析了Android中的类的加载的流程,可以看出:
  DexPathList对象中的dexElements列表是类加载的一个核心,一个类如果能被成功加载,那么它的dex一定会出现在dexElements所对应的dex文件中。
  exElements中出现的顺序也很重要,在dexElements前面出现的dex会被优先加载,一旦Class被加载成功,就会立即返回。
  我们的如果想做hot fix,一定要保证我们的pacth dex文件出现在dexElements列表的前面。
  要实现热修复,就需要我们在运行时去更改PathClassLoader.pathList.dexElements,由于这些属性都是private的,因此需要通过反射来修改。
  另外,构造我们自己的dex文件所对应的dexElements数组的时候,我们也可以采取一个比较取巧的方式:
  通过构造一个DexClassLoader对象来加载我们的dex文件
  调用一次dexClassLoader.loadClass(dummyClassName)方法
  这样dexClassLoader.pathList.dexElements中就会包含我们的dex
  通过把dexClassLoader.pathList.dexElements插入到系统默认的classLoader.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。
  总体的思路就是这样,至于具体的实现,就有很多环节需要细化的,因为Android本身也有很多自身的特性。
  接下来就是考虑实际编码实现了。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号