文章目录

  • 概念
  • 第三方框架
    • 阿里系
    • 腾讯系
    • 美团
  • 简单实现
    • 预期效果
    • 准备
      • dex分包
      • 实现原理
      • 分析ClassLoader相关源码
      • 生成dex文件
    • 代码实现

概念

我们先假设一个场景,如果一个已经上线的APP中,发现了一些bug,可能你会想直接升级版本就可以了,但是后面如果不断出现bug,难度要用户不断更新吗?这显然不太实际,用户也很难接受。这时“热修复”的概念就油然而生了,我们可以在用户未知或者影响极小的情况下把已知的bug修复,而不需要升级版本。

第三方框架

目前已经存在很多热修复的框架供我们使用了,这里就简单例举下。

阿里系

  • dexposed

  • AndFix

腾讯系

  • tinker

美团

  • Robust

至于如何选择使用第三方框架,在这不多设篇幅,这篇主要阐述一个热修复的概念和简单实现。

简单实现

了解了热修复的简单概念后,这里不适用第三方框架以一个例子来简单实现一下热修复。

预期效果

准备

dex分包

在Android中,calss文件全部打包进了dex中,程序通过ClassLoader从dex文件中加载需要的类。在用户安装好应用后,我们进行热修复把B.class重新打包进dex是不现实的,所以我们应该先将B.class打包成一个新的dex文件,让程序在加载B.class的时候,是从新的dex文件从加载,而不是从旧的dex文件加载。

因此需要dex分包,mutildex的配置可以使应用支持多包,同时也可以避免65536方法数上限。下面是mutildex的配置 :

app module中build.gradle


apply plugin: 'com.android.application'android {compileSdkVersion 23buildToolsVersion "22.0.1"defaultConfig {applicationId ......// Enabling multidex support.multiDexEnabled true}...dependencies {compile fileTree(dir: 'libs', include: ['*.jar'])compile 'com.android.support:multidex:1.0.0'...}
}

让自己的Application类继承MultidexApplication,也可以不继承:


public class App extends Application {@Overridepublic void onCreate() {super.onCreate();}@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);MultiDex.install(this);}
}

实现原理

正如我们所知,Java的类加载是通过ClassLoader进行的,在Android中也差不多,通过BaseDexClassLoader 加载classes.dex。需要实现热修复我们要用到两个BaseDexClassLoader 子类:PathClassLoaderDexClassLoader

分析ClassLoader相关源码

作为一个合格的程序员,当然要先看看这两个类加载器的源码啦?。我们可以直接下载离线的源码,也可以在线查看,我比较懒,选择在线看。附上传送门androidxref.com,这里可以看到所有版本的源码。这里我找的是6.0.0_r1的。

  • PathClassLoader

这个类很简单,就一个方法,并且还是调用父类方法,我就不贴上来了,英文好的可以自行看注释,大概意思就是这个类主要用来加载应用当前程序的dex。

  • DexClassLoader

这个类也很简单,也看不出什么,和PathClassLoader类似,不过这个可以加载指定dex文件,但需要是在当前应用程序的目录下。

  • BaseDexClassLoader

这个类是PathClassLoader和DexClassLoader共同的父类,通过查看这个类源码,我们可以发现 Class<?> findClass(String name) 这个方法,那我们可以猜想,如果 findClass(B) 时,把已经修复好bug的B.class替换有bug的那个返回回去不就实现了热修复了吗?bigo,来看看这个方法:

@Overrideprotected 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;}

很容易发现,有用的就只有这句 Class c = pathList.findClass(name, suppressedExceptions); ,那这个 pathList 是哪里来的?在看源码,可以知道这个是在构造方法中new的。至于它是干嘛的,那我们只有继续跟踪下去了。

  • DexPathList

接着在BaseDexClassLoader中使用到 pathList.findClass(name, suppressedExceptions) ,那就先来看这个方法吧。

/*** Finds the named class in one of the dex files pointed at by* this instance. This will find the one in the earliest listed* path element. If the class is found but has not yet been* defined, then this method will define it in the defining* context that this instance was constructed with.** @param name       of class to find* @param suppressed exceptions encountered whilst finding the class* @return the named class or {@code null} if the class is not* found in any of the dex files*/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 dex = element.dexFile; 这里指的是dex文件,那这里就很可能可以实现我们的目标了。当我们需要加载B.class时,加载已经修复好B.class的dex文件进行加载就ok了,所以该怎么进行判断呢?其实有个很简单的方法,不需要进行判断,因为后面一旦找到对应name的Class会return出去,并且dex文件是通过对“dexElements”对象遍历所得,所以我们只需要把新的dex文件插入"dexElements"的靠前位置即可。

生成dex文件

当我们使用Android Studio Build或者Run时都会生成相应的dex文件,或者解压apk文件也可以找到对应dex文件,但不太建议使用这类的dex文件,我们只把修复bug影响的类生成dex文件这样效率会高很多。所以问题来了,如何将指定的Class打包生成dex文件呢?

谷歌爸爸早就给我们准备好了,在 **ANDROIDSDK/build−tools/26.0.0/∗∗目录下存在∗∗dx.bat∗∗工具(其中{ANDROID_SDK}/build-tools/26.0.0/** 目录下存在 **dx.bat** 工具(其中ANDROIDS​DK/build−tools/26.0.0/∗∗目录下存在∗∗dx.bat∗∗工具(其中{ANDROID_SDK}是AndroidSDK的根目录;“26.0.0”是版本,可根据实际情况更改),通过这个工具,我们就能实现我们想要的了。


dx --dex --output=重新生成的.dex文件名 (空格) 需要打包进dex文件的Class文件或目录(目录下所有的Class文件)例:dx --dex --output=D:\classes2.dex D:\new_folder

这些 Class文件 可以自己手动一个一个通过 javac 命令收到编译生成;也可以通过 Android Studio 执行 Rebuild Project 操作,将会在 ${module_name}\build\intermediates\classes 文件夹下生成所有 Class文件 ,我们可以在这里Copy出来( 注意Copy出来的Class文件的目录结构必须完整

代码实现

分析了这么多,已经找到了“突破口”,我们只需“要对症下药”就行了。

前面提到,要在DexPathList类“dexElements”插入新的dex,这时很显然,得用上反射了。但是如果直接去修改dexElements数组的话有点麻烦,使用一个新的dexElements数组替换会方便一些。


private static void setField(Object obj,Class<?> cls, String field, Object value) throws Exception {Field localField = cls.getDeclaredField(field);localField.setAccessible(true);localField.set(obj,value);
}

显然可以通过 setField(dexPathListObj,dexPathListObj.getClass(),"dexElements",newDexElements); 来替换,那现在又有2个问题,dexPathListObj和newDexElements怎么来?

  • dexPathListObj

这个肯定也是通过反射了,不过先别急着去通过反射DexPathList的构造方法构造对象,延伸出DexPathList类的地方还记得吗?是通过BaseDexClassLoader类中findClass()方法里面有这样的调用 Class c = pathList.findClass(name, suppressedExceptions); ,说明BaseDexClassLoader中就已经有现成的DexPathList对象pathList,可以直接通过反射拿到这个对象。


private static Object getPathList(Object baseDexClassLoader) throws Exception {Class cls = Class.forName("dalvik.system.BaseDexClassLoader");Field localField = cls.getDeclaredField(""pathList"");localField.setAccessible(true);return localField.get(baseDexClassLoader);
}

通过 getPathList(baseDexClassLoader); 即可获得dexPathListObj,对于baseDexClassLoader我们可以调用 context.getClassLoader(); 得到。

  • newDexElements

我们要的“newDexElements”实际上是将原来的dexElements数组对象和新的dexElements数组对象合并所得,原来的dexElements数组对象我们可以通过反射得到,那新的dexElements数组对象该怎么来呢?
可以回到DexPathList类,很容易知道,dexElements对象是在DexPathList构造方法中创建的,那么我们可以猜想是不是在合适的地方new一个DexPathLis就可以了?这样也不用关心dexElements对象内部到底是怎么实现的了。先来看看DexPathLis构造方法:

/*** Constructs an instance.** @param definingContext the context in which any as-yet unresolved* classes should be defined* @param dexPath list of dex/resource path elements, separated by* {@code File.pathSeparator}* @param libraryPath list of native library directory path elements,* separated by {@code File.pathSeparator}* @param optimizedDirectory directory where optimized {@code .dex} files* should be found and written to, or {@code null} to use the default* system directory for same*/public DexPathList(ClassLoader definingContext, String dexPath,String libraryPath, File optimizedDirectory) {//....}

其中第一个参数需要一个ClassLoader,自然想到他的实现子类BaseDexClassLoader,并且在BaseDexClassLoader中已经new好了DexPathList对象。这里classloader和dex文件是要对应的,上面提到,PathClassLoader只能加载当前应用的dex文件,DexClassLoader可以加载指定的dex文件,对此,只需要想办法构造DexClassLoader对象即可反穿回去了。先看看DexClassLoader的构造方法:

/*** Creates a {@code DexClassLoader} that finds interpreted and native* code.  Interpreted classes are found in a set of DEX files contained* in Jar or APK files.* <p>* <p>The path lists are separated using the character specified by the* {@code path.separator} system property, which defaults to {@code :}.** @param dexPath            the list of jar/apk files containing classes and*                           resources, delimited by {@code File.pathSeparator}, which*                           defaults to {@code ":"} on Android*                           (需要加载的dex文件路径)* @param optimizedDirectory directory where optimized dex files*                           should be written; must not be {@code null}*                           (存放dex的解压目录,必须是当前应用程序下目录:/data/data/包名/...)* @param libraryPath        the list of directories containing native*                           libraries, delimited by {@code File.pathSeparator}; may be*                           {@code null}*                           (so库,可为null)* @param parent             the parent class loader (父类加载器)*/public DexClassLoader(String dexPath, String optimizedDirectory,String libraryPath, ClassLoader parent) {super(dexPath, new File(optimizedDirectory), libraryPath, parent);}

好了,接下来可以来得到需要的newDexElements了

  1. 构造DexClassLoader对象:

DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(),"...",null,pathLoader//这里可以用context.getClassLoader()获得的);
  1. 获取dexElements

private static Object getDexElements(Object pathListObj) throws Exception {Class cls = Class.forName("dalvik.system.DexPathList");Field localField = cls.getDeclaredField(""dexElements"");localField.setAccessible(true);return localField.get(pathListObj);
}
//原来的dexElements
Object dexElements = getDexElements(getPathList(baseDexClassLoader));
//新的dexElements
Object pathDexElements = getDexElements(getPathList(dexClassLoader));
  1. 合并,Object newDexElements = combineArray(pathDexElements,dexElements);
/*** 两个数组合并* @param arrayHead 合并后在前* @param arrayTail 合并后在后* @return*/private static Object combineArray(Object arrayHead, Object arrayTail) {Class<?> localClass = arrayHead.getClass().getComponentType();int i = Array.getLength(arrayHead);int j = i + Array.getLength(arrayTail);Object result = Array.newInstance(localClass, j);for (int k = 0; k < j; ++k) {if (k < i) {Array.set(result, k, Array.get(arrayHead, k));} else {Array.set(result, k, Array.get(arrayTail, k - i));}}return result;}

获取到dexPathListObj和newDexElements,通过 setField(dexPathListObj,dexPathListObj.getClass(),"dexElements",newDexElements); 即完成了替换,也实现了热修复功能。

感觉有点乱,打扰了。?

以上。

Android热修复原理及简单实现相关推荐

  1. Android 热修复原理篇及几大方案比较

    热修复说白了就是"即时无感打补丁",比如你们公司上线一个app,用户反应有重大bug,需要紧急修复.2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也 ...

  2. android热修复原理底层替换,Android 热修复 - 各框架原理学习及对比

    写在开头 从15年开始各技术大佬们开始研究热修复技术,并陆续开源了许多的热修复框架.如 Jasonross 的 Nuwa,美团的 Robust,阿里的 Andfix,腾讯的 Tinker 等等...均 ...

  3. 服务器中的热修复怎么做,Android 热修复(全网最简单的热修复讲解)

    首先!我们抛开网上的热修复框架不谈,我们来通过原理手动实现一个热修复工具,在撸码之前我们先通过一张图来了解热修复的流程. Android热修复 ACCCB328-AF5C-4BD9-AD08-6F7D ...

  4. 【大牛系列教学】Android热修复原理,满满干货指导

    优秀的战士需要出色的剑才能战斗.同样,在现代IT中,每个编码人员都需要最好的Android开发人员工具来提高他们的技能和效率.在Android应用程序开发这个残酷的竞争行业中,只有优秀的开发人员才能生 ...

  5. 热修复系列之一----Android 热修复原理篇及几大方案比较

    热修复说白了就是"即时无感打补丁",比如你们公司上线一个app,用户反应有重大bug,需要紧急修复.2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也 ...

  6. Android热修复原理(HotFix)初涉

    写在最前的话,一直听说热修复,不错,最近修复风靡,不明白原理都不行,明白原理了不会用也不行,故打算拿出一些时间去深入了解一番 翻阅众多资料 在此之前先感谢前人的资料提供, 好了 大家和我一起学习吧: ...

  7. android的热修复,Android热修复原理

    热修复框架技术主要有三类,代码修复,资源修复,动态链接库修复. 资源修复 很多资源修复的框架参考了Instant Run资源修复的原理,所以先了解一下Instant Run Instant Run I ...

  8. Android热修复原理,已整理成文档

    一. 开发背景 想要成为一名优秀的Android开发,你需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样. 1.知道它是什么.有什么用 相信很多人在学习的时候,一开始都会在网上找一整套资 ...

  9. Android Studio中使用Git——结合GitLab,Android热修复原理

    在GitLab网站点击导航条上的 "+" 即可进入创建项目的页面,然后根据提示填写相应信息,如下图: Project path:项目路径 Project name:项目名称 Pro ...

最新文章

  1. mysql 分区_搞懂MySQL分区
  2. 校园音乐点歌平台的设计与开发 微信小程序 点歌系统 java 开发
  3. mongoose常用方法(查询篇)
  4. AtCoder AGC036D Negative Cycle (图论、DP)
  5. 2016年第七届蓝桥杯C/C++ A组国赛 —— 第三题:打靶
  6. VUE 项目中引入 json 配置
  7. TensorFlow 2.0 mnist手写数字识别(CNN卷积神经网络)
  8. How to Leak a Context: Handlers Inner Classes
  9. ACM做题过程中的一些小技巧
  10. 利用cli.go来写命令行应用
  11. JS 阻止浮层弹窗下滚动
  12. 计算机硬件系统组装的论文,有关计算机硬件组装论文.docx
  13. CSS Flexbox布局
  14. 698. 划分为k个相等的子集:给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
  15. Google发展史 Google十三年
  16. 嵌入式和物联网有什么关系?一文教你搞明白。
  17. 教你如何正确反编译apk
  18. 生产制造业ERP管理系统对于制造企业的好处有哪些?
  19. 【生信分析】clusterProfiler: universal enrichment tool for functional and comparative study(3)
  20. 我在蚂蚁金服做SRE

热门文章

  1. python爬取图片并写入excel
  2. 在vue中使用mathjax渲染latex数学公式
  3. php .accdb,vb 与 .accdb 格式的access数据库的连接方法
  4. CAD使用天正打开可能引起索引色对应的RGB值变化
  5. 如何实现网页版滚动截图
  6. 中科大计算机机试题,中科大计算机考研2006-2012机试试题
  7. Python3下基于bs4和sqlalchemy的爬虫实现
  8. H323协议和sip协议
  9. l3119双闪_爱普生打印机两个指示灯一直在闪是为什么
  10. 12306订票助手java_GitHub - lifes/12306-hunter: 开源免费Java Swing C/S版本12306订票助手