Android 热修复其实很简单

    xiaoxiao2021-03-25  157

    转载自:邓志勇博客 http://blog.csdn.net/qq_31530015/

    一、什么是热修复

    热修复说白了就是”打补丁”,比如你们公司上线一个app,用户反应有重大bug,需要紧急修复。如果按照通  常做法,那就是程序猿加班搞定bug,然后测试,重新打包并发布。这样带来的问题就是成本高,效率低。于是,热  修复就应运而生.一般通过事先设定的接口从网上下载无Bug的代码来替换有Bug的代码。这样就省事多了,用  户体验也好。

    二、热修复的原理

    1.Android的类加载机制

    Android的类加载器分为两种,PathClassLoader和DexClassLoader,两者都继承自BaseDexClassLoader

    PathClassLoader代码位于libcore\dalvik\src\main\Java\dalvik\system\PathClassLoader.java  DexClassLoader代码位于libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java  BaseDexClassLoader代码位于libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java

    PathClassLoader

    用来加载系统类和应用类

    DexClassLoader

    用来加载jar、apk、dex文件.加载jar、apk也是最终抽取里面的Dex文件进行加载.

    2.热修复机制

    看下PathClassLoader代码

    public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); } } 1234567891011 1234567891011

    DexClassLoader代码

    public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } } 123456 123456

    两个ClassLoader就两三行代码,只是调用了父类的构造函数.

    public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } @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; } 12345678910111213141516171819202122 12345678910111213141516171819202122

    在BaseDexClassLoader 构造函数中创建一个DexPathList类的实例,这个DexPathList的构造函数会创建一个dexElements 数组

    public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { ... this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); //创建一个数组 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); ... } 12345678 12345678

    然后BaseDexClassLoader 重写了findClass方法,调用了pathList.findClass,跳到DexPathList类中.

    /* package */final class DexPathList { ... public Class findClass(String name, List<Throwable> suppressed) { //遍历该数组 for (Element element : dexElements) { //初始化DexFile DexFile dex = element.dexFile; if (dex != null) { //调用DexFile类的loadClassBinaryName方法返回Class实例 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } return null; } ... } 1234567891011121314151617181920 1234567891011121314151617181920

    会遍历这个数组,然后初始化DexFile,如果DexFile不为空那么调用DexFile类的loadClassBinaryName方法返回Class实例.  归纳上面的话就是:ClassLoader会遍历这个数组,然后加载这个数组中的dex文件.  而ClassLoader在加载到正确的类之后,就不会再去加载有Bug的那个类了,我们把这个正确的类放在Dex文件中,让这个Dex文件排在dexElements数组前面即可.

    这里有个问题,可参考QQ空间团队的 安卓App热补丁动态修复技术介绍  概括来讲:如果引用者和被引用者的类(直接引用关系)在同一个Dex时,那么在虚拟机启动时,被引用类就会被打上CLASS_ISPREVERIFIED标志,这样被引用的类就不能进行热修复操作了.  那么我们就要阻止被引用类打上CLASS_ISPREVERIFIED标志.QQ空间的方法是在所有引用到该类的构造函数中插入一段代码,代码引用到别的类.

    三、热修复的例子

    我用的是阿里开源的热修复框架AndFix热修复框架地址

    其实它的原理也是动态加载class文件,然后调用反射完成修复.可参考我上一篇写的  Java的ClassLoader加载机制

    AndFix是 “Android Hot-Fix”的缩写。它支持Android 2.3到6.0版本,并且支持arm与X86系统架构的设备。完美支持Dalvik与ART的Runtime。AndFix 的补丁文件是以 .apatch 结尾的文件。

    我这是用eclipse写的Demo.

    1.把AndFix抽取成library依赖的形式

    2.新建一个AndFixDemo项目,依赖AndFix这个library

    2.1

    新建一个MyApplication继承Application

    public class MyApplication extends Application { private static final String TAG = "MyApplication"; /** * apatch文件 */ private static final String APATCH_PATH = "/Dennis.apatch"; private PatchManager mPatchManager; @Override public void onCreate() { super.onCreate(); // 初始化 mPatchManager = new PatchManager(this); mPatchManager.init("1.0"); // 版本号 // 加载 apatch mPatchManager.loadPatch(); //apatch文件的目录 String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH; File apatchPath = new File(patchFileString); if (apatchPath.exists()) { Log.i(TAG, "补丁文件存在"); try { //添加apatch文件 mPatchManager.addPatch(patchFileString); } catch (IOException e) { Log.i(TAG, "打补丁出错了"); e.printStackTrace(); } } else { Log.i(TAG, "补丁文件不存在"); } } } 12345678910111213141516171819202122232425262728293031323334353637383940 12345678910111213141516171819202122232425262728293031323334353637383940

    实际当中肯定是通过网络接口下载apatch文件,我这里为了方便演示就放在了SD卡根目录

    2.2

    在MainActivity用一个按钮弹出吐司,上面是有Bug的代码,下面是修正后的代码

    分别打包成Bug.apk和NoBug.apk

    2.3

    然后要用到一个生成补丁的工具apkpatch

    解压

    _MACOSX是给OSX系统用的  .bat是给window系统用的

    我用得是.bat

    把之前生成的Bug.apkNoBug.apk,还有打包所使用的keystore文件放到apkpatch-1.0.3目录下  打开cmd,进入到apkpatch-1.0.3目录下,输入如下指令

    apkpatch.bat -f NoBug.apk -t Bug.apk -o Dennis -k keystore -p 111111 -a 111111 -e 111111

    每个参数含义如下

    -f 新版本的apk  -t 旧版本的apk  -o 输出apatch文件的文件夹,可以随意命名  -k 打包的keystore文件名  -p keystore的密码  -a keystore 用户别名  -e keystore 用户别名的密码

    如果出现add modified …….就表示成功了,去apkpatch-1.0.3目录看下,新增了Dennis目录

    我把这个文件改为Dennis.apatch

    2.4

    手机装上Bug.apk运行起来

    然后把Dennis.apatch 放到SD卡根目录,退出app,再进入,按下按钮

    最后附上Demo还有apk和apatch 文件 打开链接

    转载请注明原文地址: https://ju.6miu.com/read-2125.html

    最新回复(0)