关于热更新如今面试也是基本上都会提及到的,我上一家公司用的是tinker,而这里准备研究的也是它的原理,而关于tinker的原理网上也是一大堆文章进行介绍,为了对它有个更加进一步的认识,所以自己动手来实现类似于tinker的效果,当然关于补丁这块是如何打的不在这次研究的范围,这里只研究最最核心的,能根据补丁动态进行修复,通过分析ClassLoader的源码来弄清整个修复的原理。

界面框架搭建:

先简单弄一个示例界面,供我们能看到热修复的效果,如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="12dp"
android:gravity="center_horizontal"
tools:context=".MainActivity"> <TextView
android:id="@+id/titleTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp" /> <Button
android:id="@+id/showTitleBt"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:text="Show Title"/> <Button
android:id="@+id/hotfixBt"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:text="Hot Fix"/> <Button
android:id="@+id/removeHotfixBt"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:text="Remove Hot Fix"/> <Button
android:id="@+id/killSelfBt"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="8dp"
android:text="Kill Self"/> </LinearLayout>

Activity中给这些按钮增加点击事件:

public class MainActivity extends AppCompatActivity {

    TextView titleTv;
Button showTitleBt;
Button hotfixBt;
Button removeHotfixBt;
Button killSelfBt; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); titleTv = findViewById(R.id.titleTv);
showTitleBt = findViewById(R.id.showTitleBt);
hotfixBt = findViewById(R.id.hotfixBt);
removeHotfixBt = findViewById(R.id.removeHotfixBt);
killSelfBt = findViewById(R.id.killSelfBt); View.OnClickListener onClickListener = new View.OnClickListener() {
@Override
public void onClick(final View v) {
switch (v.getId()) {
case R.id.showTitleBt:
Util util = new Util();
titleTv.setText(util.getTitle());
break;
case R.id.hotfixBt:
//TODO:进行热更新 Util util1 = new Util();
titleTv.setText(util1.getTitle());
break;
case R.id.removeHotfixBt:
break;
case R.id.killSelfBt:
android.os.Process.killProcess(android.os.Process.myPid());
break;
}
}
}; showTitleBt.setOnClickListener(onClickListener);
hotfixBt.setOnClickListener(onClickListener);
removeHotfixBt.setOnClickListener(onClickListener);
killSelfBt.setOnClickListener(onClickListener);
}
}

其中我们以热修复Util中的一个简单方法来例,来阐述如何通过热补丁达到程序的动态更新,先来看一下Util的定义:

思考:Android的类是如何被加载的?

如何达到热修复的效果呢?这里就得分类Util这个类的加载机制了,我们知道一个类都是由类加载器从字节码给加载到内存当中的,所以咱们先来看一下当前ClassLoader是哪个类,如下:

其实关于这个类在网上都有介绍过,这里当作纯小白,从0一点点去挖掘出来热修复的原理,那接下来就打开PathClassLoader源码瞅下呗:

从包名"dalvik.system"就可以得知,这是内核的代码,通常下载肯定是不会下载有它,接下来很显然得关联相应的源码了,看下面。

看系统源码的方式【重点是如何关联IDE的系统源码】:

方式一:通过androidxref.com在上去查阅【如果比较着急,但是阅读性比IDE的要差】

先打开它瞅一下:

其中包含了Android的所有版本,如下:

它跟github的镜像是一样的,但是支持类之间的链接,在线的体验还比较好,比如我们想看"8.1.0_r33"的PathClassLoader,点进去:

就会进行一个查询界面:

然后点"Search":

此时就可以点击进去查看该类的源码了:

所以如果实际中遇到想看的源码木有在SDK中,而且又非常着急的想看,那采用这种在线的方式是非常好的,不过网页版的怎么的还是没直接在IDE查看爽,所以下面重点来看一下如何在IDE中关联我们想要的任意版本的源码。

方式二:通过从Android的系统git仓库来关联IDE【推荐】

这里要用一个非常之方便,但是貌似不多人用过的方式【平常可能也就是通过IDE的提示直接下载源码到SDK中】,也就是直接将git仓库下载到本地,然后我们可以随意的切换任意的Android本,然后在IDE中任意的关联各个版本的代码,下面来看一下如何做?当然得找到我们所看源码的github的仓库才行,如何找呢?首先直接在google中这样搜“PathClassLoader source code”,如下:

点击进去:

然后光只关联这个类肯定不行,应该是关联整个系统的源码,PathClassLoader是属于libcore项目的,所以我们往回切到这个目录:

然后点击进去:

但是!!经过几分钟后会报一个超时异常。。

在网上搜了一下解决方法:https://blog.csdn.net/zzsfqiuyigui/article/details/8832544

所以按步骤试一下,先去谷歌注册一下:

接下来需要将它拷贝到~/.bash_profile文件中,如下:

保存,然后记得重新加载一下该bash文件:

最终发现clone代码还是有问题,那。。咋办,现在源码下不下来呀,当然能解决啦,不然也不可能有此篇博文的存在,经过不断的网上找寻,发现这篇博客最终解决了我的问题:https://blog.csdn.net/qq_33487412/article/details/78458000,居然是git下androidsource得设置一下本地翻墙的代理才行,也就是如博文所说:

好,接下来咱们来设置一下,我是自有梯子的哈,具体用的啥梯子就不透露了,根据个人的喜爱去选择,如下:

然后在本地瞅一下:

接下来就需要将我们本地下下来的源码跟IDE的进行关联,首先我们要查看的PathClassLoader的包名是:

所以先在已经下载的代码中找到要看的代码:

我们知道IDE要与Android源码关联其实就是要将我们已经下载的相关源码拷贝到SDK的指定目录才可以,所以进入我们的SDK目录:

进去之后则是各个SDK中已经下载了源码的目录,如下:

那我们到底要放到哪个版本下呢?很显然就得看工程中目前所使用的sdk版本是多少了,而我们工程目前使用的targetSdkVersion是28:

所以我们应该将代码放到这个文件夹里:

而我们要拷贝的目录应该是这:

另外我们得确保拷的代码确实是28这个版本的,既然已经下载下来了libcore的整个git仓库,那只要本地切换一下版本既可,怎么切,一般某个版本发布都会打tag的,所以可以通过git tag来查看我们想要的版本,如下:

28是9.0貌似太新了,暂时还是要8.0的代码,所以咱们切到8.0的分支:

再来查看一下当前分支:

嗯~~目前已经确保我们本地的源码是我们想要的版本了,接着就可以将其指定的目录拷贝到我们的SDK指定的版本当中了,如下:

好!!接下来在Android Studio再来看是否我们想看的PathClassLoader的源码已经能看了:

以后想要看任何看不到Android源代码的都非常之方便啦~~

通过分析ClassLoader的源码找出热修复思路【常说的类加载的双亲委托细节就在里面】:

解决IDE看源码的问题之后,接着就来详细分析PathClassLoader的源码了:

其实Android还有另一个DexClassLoader也是如此,调用了一下父类的:

所以转到父类BaseDexClassLoader来瞅一下:

我们知道类加载器加载类的核心方法叫loadClass(),发现在这个父类也没有此方法,那就还得继续往它的父类ClassLoader找了,如下:

然后会调用带个参数的重载方法,其中第二个参数为resolve:

不信可以瞅一下JDK中的这个方法resolve参数是否用到:

好,接下来就来分析一下Android中的ClassLoader.loadClass()方法的具体代码:

看一下它的细节:

所以再回到主方法上来继续分析:

接下来就是重点分析类没有被加载的情况了,类加载器的双亲委托模型就开始体现了,如下:

这样的话就会父级一层层往上委托,只要找到了则就直接返回了,假如说没有父亲,则就会执行:

好,假如说都没有找到,接着就到了我们重点要看的方法了,如下:

点击查看一下:

然后转到BaseDexClassLoader类中来看到此方法的实现:

此时就需要看一下pathList这个成员变量了:

看一下它里面的findClass()方法:

关键的思路就产生了,也就是网上都会说的:

由此萌发啥思路呢?如果说我们要加载的这个类是有BUG的,那么想办法在这个类所在的dexElements的位置之前插一个修复好的类,那。。是不是之后的查找就会用我们修复的类从而达到一个热修复的目的?接下来继续看下源码的细节,既然dexElements这么重要,有必要了解一下它是如何被初始化的,此时得回溯一下调用链,目前分析的整个类的调用关系总结如下:

接下来再回来挼一挼流程:

所以定位到该构造方法:

而接下来就看一下DexPathList里面的dexElements的实例化过程,因为最终寻找就是遍历dexElements,如下:

好,这是最后的一点硬骨头了,得啃一下里面的肉,这样有助于我们思路的定型,其中makeDexElements()的第一个参数其实就是对我们传过来的文件路径进行分割,比如我们传了“”这样的路径,以冒号进行分隔,则会生成多个File集合,进去简单瞅一眼,不是重点:

而我们在修复补丁时肯定只会有一个文件,所以经过这个方法之后其实也就是生成一个带有一个File的集合,仅此而已,接着来看具体的makeDexElements()的实现:

/**
* Makes an array of dex/resource path elements, one per element of
* the given array.
*/
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files up front.
*/
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
DexFile dex = null;
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/*
* IOException might get thrown "legitimately" by the DexFile constructor if
* the zip file turns out to be resource-only (that is, no classes.dex file
* in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
*/
suppressedExceptions.add(suppressed);
} if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}

下面简单过一下流程:

而如果是一个文件,则会判断是否是".dex"文件或者是“apk”的文件,如下:

如果是.dex文件,则会调用loadDexFile()方法加载成DexFile对象,然后再将它放到Element数组当中:

而如果是apk,也是类似的,是不是我们只要将Apk正在运行的DexPathList中的dexElements的内容给替换成我们想要既可,那如何替换呢,反射!!!

实现热修复:

先补丁本地修复,实现初步修复:

有了思路之后,接下来热修复的核心代码直接贴出,不难:

记得补丁的dexElements一定是要插到程序本身的dexElements之前,这是核心。好,接下来咱们生成一个补丁,这里是以修复Util.getTitle()为目的,修复一下代码,并打成一个apk补下,如下:

然后修复方法,这里当然就是简单的修改下文案来模拟了:

然后运行生成一个apk就当成是补丁:

好,此时将它拷贝到assert目录中:

好,记得将此代码还原,因为我们要基于一个有错误的程序来验证热修复:

最后,需要将热补丁从assets目录拷贝到app的私有目录,直接也贴代码比较简单:

好,下面来运行一下:

嗯,最最简单的热修复就实现了,当然有很多不完善的地方,重点是了解其原理。

优化一下代码:

1、咱们需要将热修复的代码提到Application中去弄,或者也可以放到Splash界面等,反正就是在我们要用之前肯定得提前加载,不然达不到热修复的效果,如下:

原因是由于我们热复的代码是写在了“HOT FIX”的点击事件上了,所以咱们将其修复代码往前移:

public class HotfixApplication extends Application {

    @Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base); File apk = new File(getCacheDir() + "/hotfix.apk");
if (apk.exists()) {
try {
//首先获取当前正在运行的apk中的dexElements数组
ClassLoader classLoader = getClassLoader();
Class loaderClass = BaseDexClassLoader.class;
Field pathListField = loaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObject = pathListField.get(classLoader);
Class pathListClass = pathListObject.getClass();
Field dexElementsField = pathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElementsObject = dexElementsField.get(pathListObject); //然后再获取指定apk中的dexElements对象 PathClassLoader newClassLoader = new PathClassLoader(apk.getPath(), classLoader.getParent());
Object newPathListObject = pathListField.get(newClassLoader);
Object newDexElementsObject = dexElementsField.get(newPathListObject); //最后再用我们自己生成的dexElements对象来替换掉当前应用的dexElements对象,其中自己生成的是包含当前的和要修复的dexElements
int oldLength = Array.getLength(dexElementsObject);
int newLength = Array.getLength(newDexElementsObject);
Object concatDexElementsObject = Array.newInstance(dexElementsObject.getClass().getComponentType(), oldLength + newLength);
for (int i = 0; i < newLength; i++) {
Array.set(concatDexElementsObject, i, Array.get(newDexElementsObject, i));
}
for (int i = 0; i < oldLength; i++) {
Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObject, i));
} dexElementsField.set(pathListObject, concatDexElementsObject);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}

这样的话,进app主界面就可以用了:

2、补丁可以换成dex,只包含我们修复的内容,而不是整个apk中包含不需要的东东做为补丁包。【传说中的增量补丁,不过不是成熟的增量补丁】

热修复除了能直接用apk来作为补丁,也能够用dex,所以我们需要利用SDK中的dex工具来将我们指定的类指成dex包,如下:

然后在SDK中找到打dex的工具:

平常我们打dex包通常用的是dx命令,其实是可以用d8的,如下:

然后再将它复制到assets中:

此时再修改一下文件名:

然后再运行:

将打补丁的过程gradle化:

在上面的演示中我们打一个补丁是手动通过SDK中的工具在命令行中对我们指定要打入补丁的类进行打补丁,很显然现实中不太可取,所以需要将其自动化,所以将其打补丁的过程和配置到gradle,这里就需要定义一个任务,不难,下面直接贴出具体配置代码:

def patchPath = 'com/tinkerdemo/test/Util'

task hotfix {
doLast {
exec {
commandLine 'rm', '-rf', './build/patch'
}
exec {
commandLine 'mkdir', './build/patch'
}
exec {
commandLine 'javac', "./src/main/java/${patchPath}.java", '-d', './build/patch'
}
exec {
commandLine '/Users/xiongwei/android-sdks/Android/sdk/build-tools/28.0.3/d8', "./build/patch/${patchPath}.class", '--output', './build/patch'
}
exec {
commandLine 'mv', "./build/patch/classes.dex", './build/patch/hotfix.dex'
}
}
}

其实对于gradle来执行具体的命令必须是每条命令的执行单独写一个exec,如图:

对于这个配置不难理解,也就是将我们执行的命令通过gradle来自动执行了,跟批处理类似,然后最后会在这个目录下生成补下:

好,咱们来玩一下,比如我们修改了类的BUG,还是之前的Util:

接下来走一波:

当然啦真实商用一般都会用成熟开源的,配置也可能比较麻烦,但是学习其本质是很有必要的。

手写热更新阐述tinker实现原理的更多相关文章

  1. 【腾讯Bugly干货分享】手游热更新方案xLua开源:Unity3D下Lua编程解决方案

    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/2bY7A6ihK9IMcA0bOFyB-Q 导语 xL ...

  2. 腾讯开源手游热更新方案,Unity3D下的Lua编程

    原文:http://www.sohu.com/a/123334175_355140 作者|车雄生 编辑|木环 腾讯最近在开源方面的动作不断:先是微信跨平台基础组件Mars宣布开源,腾讯手游又于近期开源 ...

  3. IOS热更新-JSPatch实现原理+Patch现场恢复

    关于HotfixPatch 在IOS开发领域,由于Apple严格的审核标准和低效率,IOS应用的发版速度极慢,稍微大型的app发版基本上都在一个月以上,所以代码热更新(HotfixPatch)对于IO ...

  4. 手游热更新方案--Unity3D下的CsToLua技术

    WeTest 导读 CsToLua工具将客户端 C#源码自动转换为Lua,实现热更新,本文以麻将项目为例介绍客户端技术细节. 麻将项目架构 其中ChinaMahjong-CSLua为C#工程,实现麻将 ...

  5. Android热更新技术——Tinker、nuwa、AndFix、Dexposed

    一.热修复技术作用 线上app BUG紧急修复,不重新发版,不重新安装,在线远程修复问题 二.局限性与适用场景 补丁只能针对单一客户端版本,随着版本差异变大补丁体积也会增大: 补丁不能支持所有的修改, ...

  6. 手游热更新方案xLua开源:Unity3D下Lua编程解决方案

    C#下Lua编程支持 xLua为Unity. .Net. Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用. xLua的突破 xLua在功能.性能.易用 ...

  7. 4.1 手写Java PriorityQueue 核心源码 - 原理篇

    本章先讲解优先级队列和二叉堆的结构.下一篇代码实现 从一个需求开始 假设有这样一个需求:在一个子线程中,不停的从一个队列中取出一个任务,执行这个任务,直到这个任务处理完毕,再取出下一个任务,再执行. ...

  8. Unity3D热更新之LuaFramework篇[08]--热更新原理及热更服务器搭建

    前言 前面铺垫了这么久,终于要开始写热更新了. Unity游戏热更新包含两个方面,一个是资源的更新,一个是脚本的更新. 资源更新是Unity本来就支持的,在各大平台也都能用.而脚本的热更新在iOS平台 ...

  9. 手把手教你实现热更新功能,带你了解 Arthas 热更新背后的原理

    文章来源:https://studyidea.cn/java-hotswap 一.前言 一天下午正在摸鱼的时候,测试小姐姐走了过来求助,说是需要改动测试环境 mock 应用.但是这个应用一时半会又找不 ...

随机推荐

  1. 基于 appium 的 UI 自动化测试

    其中主要的目录和文件为: /MPTestCases ----------- 存放测试用例 /errorScreenShot ------------ 用例执行失败生成的错误截图 startTest.p ...

  2. 滚动条mCustomScrollbar自定义

    mCustomScrollbar 是个基于 jQuery UI 的自定义滚动条插件,它可以让你灵活的通过 CSS 定义网页的滚动条,并且垂直和水平两个方向的滚动条都可以定义,它通过 Brandon A ...

  3. redhat7.6Linux安装Oracle19C完整版教程

    首先安装配置虚拟机,见博客https://www.cnblogs.com/xuzhaoyang/p/11264563.html 然后配置IP地址,见博客https://www.cnblogs.com/ ...

  4. time包 — 汇总

    time包学习 package main; import ( "time" "fmt" ) func main() { //time.Time代表一个纳秒精度的 ...

  5. Django之拾遗

    一.设计模式 1.1 MVC 模型(M)是数据的表述,非真正数据,而是数据接口. 视图(V)是你看到的界面,是模型的表现层,此外还提供了收集用户输入的接口. 控制器(C)控制模型和视图之间的信息流动. ...

  6. 有关java中的try{}catch(){}的讲解

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/qq_38225558/article/d ...

  7. 深度探索MySQL主从复制原理

    深度探索MySQL主从复制原理 一 .概要 MySQL Replication (MySQL 主从复制) 是什么? 为什么要主从复制以及它的实现原理是什么? 1.1 MySQL 主从复制概念 MySQ ...

  8. centos7,jdk8,tomcat8镜像推送到腾讯云

    目录 centos7 jdk tomcat centos7 创建一个mycentos7的文件 vim mycentos7 FROM centos:7 MAINTAINER qyp_mail@sohu. ...

  9. Python模拟知乎登录

    # -*- coding:utf-8 -*- import urllib import urllib2 import cookielib import time from PIL import Ima ...

  10. numpy模块之axis(转)

    转自:https://blog.csdn.net/fangjian1204/article/details/53055219