Android热修复

一、什么是热修复

热修复说白了就是”打补丁”,比如你们公司上线一个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);
    }
} 
   
   

      DexClassLoader代码

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

      两个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;
          }
         
         

        在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);
                ... 
            }
           
           

          然后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;
              }
              ...
          } 

          会遍历这个数组,然后初始化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, “补丁文件不存在”);
                  }
              }
          }
          

          实际当中肯定是通过网络接口下载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 文件 打开链接

          • 0
            点赞
          • 0
            收藏
            觉得还不错? 一键收藏
          • 0
            评论
          评论
          添加红包

          请填写红包祝福语或标题

          红包个数最小为10个

          红包金额最低5元

          当前余额3.43前往充值 >
          需支付:10.00
          成就一亿技术人!
          领取后你会自动成为博主和红包主的粉丝 规则
          hope_wisdom
          发出的红包
          实付
          使用余额支付
          点击重新获取
          扫码支付
          钱包余额 0

          抵扣说明:

          1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
          2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

          余额充值