首页 >> 大全

热修复 tinker接入及源码分析

2023-10-24 大全 25 作者:考证青年

本文出自张鸿洋的博客

一、概述

放了一个大长假,happy,先祝大家2017年笑口常开。

假期中一行代码没写,但是想着马上要上班了,赶紧写篇博客回顾下技能,于是便有了本文。

热修复这项技术,基本上已经成为项目比较重要的模块了。主要因为项目在上线之后,都难免会有各种问题,而依靠发版去修复问题,成本太高了。

现在热修复的技术基本上有阿里的、QZone的方案、美团提出的思想方案以及腾讯的等。

其中可能接入是最简单的一个(和命令行接入方式差不多),不过兼容性还是是有一定的问题的;QZone方案对性能会有一定的影响,且在Art模式下出现内存错乱的问题(其实这个问题我之前并不清楚,主要是在MDCC上指出的);美团提出的思想方案主要是基于 Run的原理,目前尚未开源,不过这个方案我还是蛮喜欢的,主要是兼容性好。

这么看来,如果选择开源方案,目前是最佳的选择,的介绍有这么一句:

已运行在微信的数亿设备上,那么为什么你不使用呢?

好了,说了这么多,下面来看看如何接入,以及的大致的原理分析。希望通过本文可以实现帮助大家更好的接入,以及去了解的一个大致的原理。

二、接入

接入目前给了两种方式,一种是基于命令行的方式,类似于的接入方式;一种就是的方式。

考虑早期使用的app应该挺多的,以及很多人对的相关配置还是觉得比较繁琐的,下面对两种方式都介绍下。

(1)命令行接入

接入之前我们先考虑下,接入的话,正常需要的前提(开启混淆的状态)。

最后就是看看这个项目有没有需要配置混淆;

有了大致的概念,我们就基本了解命令行接入,大致需要哪些步骤了。

依赖引入

dependencies {// ...//可选,用于生成application类provided('com.tencent.tinker:tinker-android-anno:1.7.7')//tinker的核心库compile('com.tencent.tinker:tinker-android-lib:1.7.7')
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

顺便加一下签名的配置:

android{//...signingConfigs {release {try {storeFile file("release.keystore")storePassword "testres"keyAlias "testres"keyPassword "testres"} catch (ex) {throw new InvalidUserDataException(ex.toString())}}}buildTypes {release {minifyEnabled truesigningConfig signingConfigs.releaseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}debug {debuggable trueminifyEnabled truesigningConfig signingConfigs.releaseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

文末会有demo的下载地址,可以直接参考build.文件,不用担心这些签名文件去哪找。

API引入

API主要就是初始化和。

正常情况下,我们会考虑在的中去初始化,不过推荐下面的写法:

@DefaultLifeCycle(application = ".SimpleTinkerInApplication",flags = ShareConstants.TINKER_ENABLE_ALL,loadVerifyFlag = false)
public class SimpleTinkerInApplicationLike extends ApplicationLike {public SimpleTinkerInApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);}@Overridepublic void onBaseContextAttached(Context base) {super.onBaseContextAttached(base);}@Overridepublic void onCreate() {super.onCreate();TinkerInstaller.install(this);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

通过名字你可能会猜,并非是的子类,而是一个类似的类。

建议编写一个的子类,你可以当成去使用,注意顶部的注解:@,其属性,会在编译期生成一个ation类。

所以,虽然我们这么写了,但是实际上会在编译期生成,所以.xml中是这样的:

 ".SimpleTinkerInApplication".../>
  • 1
  • 2
  • 3
    • 1
    • 2
    • 3

编写如果报红,可以build下。

这样其实也能猜出来,这个注解背后有个 在做处理,如果你没了解过,可以看下:

通过该文会对一个编译时注解的运行流程和基本API有一定的掌握,文中也会对该部分的源码做解析。

上述,就完成了的初始化,那么调用的时机,我们直接在中添加一个设置:


public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}public void loadPatch(View view) {TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

我们会将patch文件直接push到根目录;

所以一定要注意:添加权限,如果你是6.x以上的系统,自己添加上授权代码,或者手动在设置页面打开读写权限。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • 1
    • 1

除以以外,有个特殊的地方就是需要在.xml中指定。

"TINKER_ID"android:value="tinker_id_6235657" />//...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

到此API相关的就结束了,剩下的就是考虑patch如何生成。

patch生成

提供了patch生成的工具,源码见:-patch-cli,打成一个jar就可以使用,并且提供了命令行相关的参数以及文件。

命令行如下:

java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output
  • 1
    • 1

需要注意的就是.xml,里面包含的配置,例如签名文件等。

这里我们直接使用提供的签名文件,所以不需要做修改,不过里面有个的item修改为与本例一致:

value="com.zhy.tinkersimplein.SimpleTinkerInApplication"/>
  • 1
    • 1

大致的文件结构如下:

热修复 tinker接入及源码分析_热修复 tinker接入及源码分析_

可以在-patch-cli中提取,或者直接下载文末的例子。

上述介绍了patch生成的命令,最后需要注意的就是,在第一次打出apk的时候,保留下生成的文件,在/build////.txt。

可以copy到与-rules.pro同目录,同时在第二次打修复包的时候,在-rules.pro中添加上:

-applymapping mapping.txt
  • 1
    • 1

热修复 tinker接入及源码分析__热修复 tinker接入及源码分析

保证后续的打包与线上包使用的是同一个文件。

本身的混淆相关配置,可以参考:

如果,你对该部分描述不了解,可以直接查看源码即可。

测试

首先随便生成一个apk(API、混淆相关已经按照上述引入),安装到手机或者模拟器上。

然后,copy出.txt文件,设置,修改代码,再次打包,生成new.apk。

两次的apk,可以通过命令行指令去生成patch文件。

如果你下载本例,命令需要在[该目录]下执行。

最终会在文件夹中生成产物:

热修复 tinker接入及源码分析_热修复 tinker接入及源码分析_

我们直接将.apk push到,点击,一定要观察命令行是否成功。

热修复 tinker接入及源码分析__热修复 tinker接入及源码分析

本例修改了title。

点击,观察log,如果成功,应用默认为重启,然后再次启动即可达到修复效果。

到这里命令行的方式就介绍完了,和的接入的方式基本上是一样的。

值得注意的是:该例仅展示了基本的接入,对于的各种配置信息,还是需要去读的文档(如果你确定要使用)-wiki。

(2)接入

接入的方式应该算是主流的方式,所以也直接给出了例子,单独将该--以方式引入即可。

引入之后,可以查看其接入API的方式,以及相关配置。

在你每次build时,会在build/下生成本地打包的apk,R文件,以及文件。

如果你需要生成patch文件,可以通过:

./gradlew tinkerPatchRelease  // 或者 ./gradlew tinkerPatchDebug
  • 1
    • 1

生成。

生成目录为:build//

需要注意的是,需要在app/build.中设置相比较的apk(即old.apk,本次为new.apk),

ext {tinkerEnabled = true//old apk file to build patch apktinkerOldApkPath = "${bakPath}/old.apk"//proguard mapping file to build patch apktinkerApplyMappingPath = "${bakPath}/old-mapping.txt"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

提供的例子,基本上展示了的自定义扩展的方式,具体还可以参考:

所以,如果你使用命令行方式接入,也不要忘了学习下其支持哪些扩展。

三、是如何编译时生成的

从注释和命名上看:

//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.7.7')
  • 1
  • 2
    • 1
    • 2

明显是该库,其结构如下:

_热修复 tinker接入及源码分析_热修复 tinker接入及源码分析

典型的编译时注解的项目,源码见--anno。

入口为com...anno.,可以在该/javax...文件中找到处理类全路径。

再次建议,如果你不了解,简单阅读下 如何编写基于编译时注解的项目该文。

直接看的方法:

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {processDefaultLifeCycle(roundEnv.getElementsAnnotatedWith(DefaultLifeCycle.class));return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
    • 1
    • 2
    • 3
    • 4
    • 5

直接调用了cle:

private void processDefaultLifeCycle(Set elements) {// 被注解DefaultLifeCycle标识的对象for (Element e : elements) {// 拿到DefaultLifeCycle注解对象DefaultLifeCycle ca = e.getAnnotation(DefaultLifeCycle.class);String lifeCycleClassName = ((TypeElement) e).getQualifiedName().toString();String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf('.'));lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf('.') + 1);String applicationClassName = ca.application();if (applicationClassName.startsWith(".")) {applicationClassName = lifeCyclePackageName + applicationClassName;}String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf('.'));applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf('.') + 1);String loaderClassName = ca.loaderClass();if (loaderClassName.startsWith(".")) {loaderClassName = lifeCyclePackageName + loaderClassName;}// /TinkerAnnoApplication.tmplfinal InputStream is = AnnotationProcessor.class.getResourceAsStream(APPLICATION_TEMPLATE_PATH);final Scanner scanner = new Scanner(is);final String template = scanner.useDelimiter("\\A").next();final String fileContent = template.replaceAll("%PACKAGE%", applicationPackageName).replaceAll("%APPLICATION%", applicationClassName).replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName).replaceAll("%TINKER_FLAGS%", "" + ca.flags()).replaceAll("%TINKER_LOADER_CLASS%", "" + loaderClassName).replaceAll("%TINKER_LOAD_VERIFY_FLAG%", "" + ca.loadVerifyFlag());JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName);processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Creating " + fileObject.toUri());Writer writer = fileObject.openWriter();PrintWriter pw = new PrintWriter(writer);pw.print(fileContent);pw.flush();writer.close();}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

代码比较简单,可以分三部分理解:

我们看一眼模板文件:

package %PACKAGE%;import com.tencent.tinker.loader.app.TinkerApplication;/**** Generated application for tinker life cycle**/
public class %APPLICATION% extends TinkerApplication {public %APPLICATION%() {super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

对应我们的,

@DefaultLifeCycle(application = ".SimpleTinkerInApplication",flags = ShareConstants.TINKER_ENABLE_ALL,loadVerifyFlag = false)
public class SimpleTinkerInApplicationLike extends ApplicationLike {}
  • 1
  • 2
  • 3
  • 4
    • 1
    • 2
    • 3
    • 4

主要就几个占位符:

于是最终生成的代码为:

/**** Generated application for tinker life cycle**/
public class SimpleTinkerInApplication extends TinkerApplication {public SimpleTinkerInApplication() {super(7, "com.zhy.tinkersimplein.SimpleTinkerInApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

这么做的目的,文档上是这么说的:

为了减少错误的出现,推荐使用生成类。

这样大致了解了是如何生成的。

接下来我们大致看一下的原理。

四、原理

热修复 tinker接入及源码分析__热修复 tinker接入及源码分析

来源于:

贴了一张大致的原理图。

可以看出:

将old.apk和new.apk做了diff,拿到patch.dex,然后将patch.dex与本机中apk的.dex做了合并,生成新的.dex,运行时通过反射将合并后的dex文件放置在加载的数组的前面。

运行时替代的原理,其实和Qzone的方案差不多,都是去反射修改。

两者的差异是:Qzone是直接将patch.dex插到数组的前面;而是将patch.dex与app中的.dex合并后的全量dex插在数组的前面。

这么做的目的还是因为Qzone方案中提到的的解决方案存在问题;而相当于换个思路解决了该问题。

接下来我们就从代码中去验证该原理。

本片文章源码分析的两条线:

五、源码分析 (1)加载patch

加载的代码实际上在生成的中调用的,其父类为,在其中辗转会调用到()方法,在该方法内部,反射调用了的方法。

@Override
public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {Intent resultIntent = new Intent();long begin = SystemClock.elapsedRealtime();tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);long cost = SystemClock.elapsedRealtime() - begin;ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);return resultIntent;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

ernal中会调用到方法:

private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {// 省略大量安全性校验代码if (isEnabledForDex) {//tinker/patch.info/patch-641e634c/dexboolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);if (!dexCheck) {//file not found, do not load patchLog.w(TAG, "tryLoadPatchFiles:dex check fail");return;}}//now we can load patch jarif (isEnabledForDex) {boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);if (!loadTinkerJars) {Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");return;}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

.主要是用于检查下发的meta文件中记录的dex信息(meta文件,可以查看生成patch的产物,在/dex-meta.txt),检查meta文件中记录的dex文件信息对应的dex文件是否存在,并把值存在的静态变量中。

.传入四个参数,分别为,(注解上声明的值,传入为false),y当前的patch文件夹,,当前patch是否仅适用于art。

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult, boolean isSystemOTA) {PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();String dexPath = directory + "/" + DEX_PATH + "/";File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH);ArrayList legalFiles = new ArrayList<>();final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();for (ShareDexDiffPatchInfo info : dexList) {//for dalvik, ignore art support dexif (isJustArtSupportDex(info)) {continue;}String path = dexPath + info.realName;File file = new File(path);legalFiles.add(file);}// just for artif (isSystemOTA) {parallelOTAResult = true;parallelOTAThrowable = null;Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");TinkerParallelDexOptimizer.optimizeAll(legalFiles, optimizeDir,new TinkerParallelDexOptimizer.ResultCallback() {});SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

找出仅支持art的dex,且当前patch是否仅适用于art时,并行去。

关键是最后的:

@SuppressLint("NewApi")
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List files)throws Throwable {if (!files.isEmpty()) {ClassLoader classLoader = loader;if (Build.VERSION.SDK_INT >= 24) {classLoader = AndroidNClassLoader.inject(loader, application);}//because in dalvik, if inner class is not the same classloader with it wrapper class.//it won't fail at dex2optif (Build.VERSION.SDK_INT >= 23) {V23.install(classLoader, files, dexOptDir);} else if (Build.VERSION.SDK_INT >= 19) {V19.install(classLoader, files, dexOptDir);} else if (Build.VERSION.SDK_INT >= 14) {V14.install(classLoader, files, dexOptDir);} else {V4.install(classLoader, files, dexOptDir);}//install donesPatchDexCount = files.size();Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);if (!checkDexInstall(classLoader)) {//reset patch dexSystemClassLoaderAdder.uninstallPatchDex(classLoader);throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);}}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

这里实际上就是根据不同的系统版本,去反射处理。

我们看一下V19的实现(主要我看了下本机只有个22的源码~):

private static final class V19 {private static void install(ClassLoader loader, List additionalClassPathEntries,File optimizedDirectory)throws IllegalArgumentException, IllegalAccessException,NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {Field pathListField = ShareReflectUtil.findField(loader, "pathList");Object dexPathList = pathListField.get(loader);ArrayList suppressedExceptions = new ArrayList();ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,new ArrayList(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));if (suppressedExceptions.size() > 0) {for (IOException e : suppressedExceptions) {Log.w(TAG, "Exception in makeDexElement", e);throw e;}}}
}        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

找到()对象中的对象根据对象找到其中的方法,传入patch相关的对应的实参,返回[]对象拿到对象中原本的方法步骤2与步骤3中的[]数组进行合并,将patch相关的dex放在数组的前面最后将合并后的数组,设置给

这里其实和Qzone的提出的方案基本是一致的。如果你以前未了解过Qzone的方案,可以参考此文:

(2)合成patch

这里的入口为:

 TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");
  • 1
  • 2
    • 1
    • 2

上述代码会调用中的方法:

# DefaultPatchListener
@Override
public int onPatchReceived(String path) {int returnCode = patchCheck(path);if (returnCode == ShareConstants.ERROR_PATCH_OK) {TinkerPatchService.runPatchService(context, path);} else {Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);}return returnCode;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

首先对的相关配置()以及patch的合法性进行检测,如果合法,则调用.(, path);。

public static void runPatchService(Context context, String path) {try {Intent intent = new Intent(context, TinkerPatchService.class);intent.putExtra(PATCH_PATH_EXTRA, path);intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());context.startService(intent);} catch (Throwable throwable) {TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

是的子类,这里通过设置了两个参数,一个是patch的路径,一个是,该值是调用.的时候设置的,默认为.class。由于是,直接看即可,如果你对陌生,可以查看此文: 完全解析 当遇到

@Override
protected void onHandleIntent(Intent intent) {final Context context = getApplicationContext();Tinker tinker = Tinker.with(context);String path = getPatchPathExtra(intent);File patchFile = new File(path);boolean result;increasingPriority();PatchResult patchResult = new PatchResult();result = upgradePatchProcessor.tryPatch(context, path, patchResult);patchResult.isSuccess = result;patchResult.rawPatchFilePath = path;patchResult.costTime = cost;patchResult.e = e;AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

比较清晰,主要关注r.方法,调用的是.。ps:这里有个有意思的地方(),其内部实现为:

private void increasingPriority() {TinkerLog.i(TAG, "try to increase patch process priority");try {Notification notification = new Notification();if (Build.VERSION.SDK_INT < 18) {startForeground(notificationId, notification);} else {startForeground(notificationId, notification);// start InnerServicestartService(new Intent(this, InnerService.class));}} catch (Throwable e) {TinkerLog.i(TAG, "try to increase patch process priority error:" + e);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

如果你对“保活”这个话题比较关注,那么对这段代码一定不陌生,主要是利用系统的一个漏洞来启动一个前台。如果有兴趣,可以参考此文:关于 进程保活,你所需要知道的一切。

下面继续回到方法:

# UpgradePatch
@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {Tinker manager = Tinker.with(context);final File patchFile = new File(tempPatchPath);//it is a new patch, so we should not find a existSharePatchInfo oldInfo = manager.getTinkerLoadResultIfPresent().patchInfo;String patchMd5 = SharePatchFileUtil.getMD5(patchFile);//use md5 as versionpatchResult.patchVersion = patchMd5;SharePatchInfo newInfo;//already have patchif (oldInfo != null) {newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, Build.FINGERPRINT);} else {newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT);}//check ok, we can real recover a new patchfinal String patchDirectory = manager.getPatchDirectory().getAbsolutePath();final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);final String patchVersionDirectory = patchDirectory + "/" + patchName;//copy fileFile destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));// check md5 firstif (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);}//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch processif (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");return false;}return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

拷贝patch文件拷贝至私有目录,然后调用.:

protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,String patchVersionDirectory, File patchFile) {String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

直接看xDiff

private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {String dir = patchVersionDirectory + "/" + DEX_PATH + "/";if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");return false;}final Tinker manager = Tinker.with(context);File dexFiles = new File(dir);File[] files = dexFiles.listFiles();...files遍历执行:DexFile.loadDexreturn true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

核心代码主要在als中:

private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {//parse metaArrayList patchList = new ArrayList<>();ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);File directory = new File(dir);//I think it is better to extract the raw files from apkTinker manager = Tinker.with(context);ZipFile apk = null;ZipFile patch = null;ApplicationInfo applicationInfo = context.getApplicationInfo();String apkPath = applicationInfo.sourceDir; //base.apkapk = new ZipFile(apkPath);patch = new ZipFile(patchFile);for (ShareDexDiffPatchInfo info : patchList) {final String infoPath = info.path;String patchRealPath;if (infoPath.equals("")) {patchRealPath = info.rawName;} else {patchRealPath = info.path + "/" + info.rawName;}File extractedFile = new File(dir + info.realName);ZipEntry patchFileEntry = patch.getEntry(patchRealPath);ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);}return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

这里的代码比较关键了,可以看出首先解析了meta里面的信息,meta中包含了patch中每个dex的相关数据。然后通过拿到,其实就是本机apk的路径以及patch文件;根据mate中的信息开始遍历,其实就是取出对应的dex文件,最后通过对两个dex文件做合并。

private static void patchDexFile(ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,ShareDexDiffPatchInfo patchInfo,  File patchedDexFile) throws IOException {InputStream oldDexStream = null;InputStream patchFileStream = null;oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

通过拿到其内部文件的,其实就是读取本地apk对应的dex文件,以及patch中对应dex文件,对二者的通过方法进行合并至,即patch的目标私有目录。

至于合并算法,这里其实才是比较核心的地方,这个算法跟dex文件格式紧密关联,如果有机会,然后我又能看懂的话,后面会单独写篇博客介绍。此外已经有篇博客进行了介绍:

感兴趣的可以阅读下。

好了,到此我们就大致了解了热修复的原理~~

测试demo地址:

当然这里只分析了代码了热修复,后续考虑分析资源以及So的热修、核心的diff算法、以及插件等相关知识~

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了