放了一个大长假,happy,先祝大家 2017 年笑口常开。
假期中一行代码没写,但是想着马上要上班了,赶紧写篇博客回顾下技能,于是便有了本文。
热修复这项技术,基本上已经成为项目比较重要的模块了。主要因为项目在上线之后,都难免会有各种问题,而依靠发版去修复问题,成本太高了。
现在热修复的技术基本上有阿里的 AndFix、QZone 的方案、美团提出的思想方案以及腾讯的 Tinker 等。
其中 AndFix 可能接入是最简单的一个(和 Tinker 命令行接入方式差不多),不过兼容性还是是有一定的问题的;QZone 方案对性能会有一定的影响,且在 Art 模式下出现内存错乱的问题 (其实这个问题我之前并不清楚,主要是 tinker 在 MDCC 上指出的); 美团提出的思想方案主要是基于 Instant Run 的原理,目前尚未开源,不过这个方案我还是蛮喜欢的,主要是兼容性好。
这么看来,如果选择开源方案,tinker 目前是最佳的选择,tinker 的介绍有这么一句:
Tinker 已运行在微信的数亿 Android 设备上,那么为什么你不使用 Tinker 呢?
好了,说了这么多,下面来看看 tinker 如何接入,以及 tinker 的大致的原理分析。希望通过本文可以实现帮助大家更好的接入 tinker,以及去了解 tinker 的一个大致的原理。
接入 tinker 目前给了两种方式,一种是基于命令行的方式,类似于 AndFix 的接入方式;一种就是 gradle 的方式。
考虑早期使用 Andfix 的 app 应该挺多的,以及很多人对 gradle 的相关配置还是觉得比较繁琐的,下面对两种方式都介绍下。
接入之前我们先考虑下,接入的话,正常需要的前提(开启混淆的状态)。
最后就是看看这个项目有没有需要配置混淆;
有了大致的概念,我们就基本了解命令行接入 tinker,大致需要哪些步骤了。
- dependencies {
- // ...
- //可选,用于生成application类
- provided('com.tencent.tinker:tinker-android-anno:1.7.7')
- //tinker的核心库
- compile('com.tencent.tinker:tinker-android-lib:1.7.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 true
- signingConfig signingConfigs.release
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- debug {
- debuggable true
- minifyEnabled true
- signingConfig signingConfigs.release
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- }
文末会有 demo 的下载地址,可以直接参考 build.gradle 文件,不用担心这些签名文件去哪找。
API 主要就是初始化和 loadPacth。
正常情况下,我们会考虑在 Application 的 onCreate 中去初始化,不过 tinker 推荐下面的写法:
- @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);
- }
- @Override
- public void onBaseContextAttached(Context base) {
- super.onBaseContextAttached(base);
- }
- @Override
- public void onCreate() {
- super.onCreate();
- TinkerInstaller.install(this);
- }
- }
ApplicationLike 通过名字你可能会猜,并非是 Application 的子类,而是一个类似 Application 的类。
tinker 建议编写一个 ApplicationLike 的子类,你可以当成 Application 去使用,注意顶部的注解:
,其 application 属性,会在编译期生成一个
- @DefaultLifeCycle
类。
- SimpleTinkerInApplication
所以,虽然我们这么写了,但是实际上 Application 会在编译期生成,所以
中是这样的:
- AndroidManifest.xml
- <application
- android:name=".SimpleTinkerInApplication"
- .../>
编写如果报红,可以 build 下。
这样其实也能猜出来,这个注解背后有个 Annotation Processor 在做处理,如果你没了解过,可以看下:
通过该文会对一个编译时注解的运行流程和基本 API 有一定的掌握,文中也会对 tinker 该部分的源码做解析。
上述,就完成了 tinker 的初始化,那么调用 loadPatch 的时机,我们直接在 Activity 中添加一个 Button 设置:
- public class MainActivity extends AppCompatActivity {
- @Override
- protected 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");
- }
- }
我们会将 patch 文件直接 push 到 sdcard 根目录;
所以一定要注意:添加 SDCard 权限,如果你是 6.x 以上的系统,自己添加上授权代码,或者手动在设置页面打开 SDCard 读写权限。
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
除以以外,有个特殊的地方就是 tinker 需要在
中指定 TINKER_ID。
- AndroidManifest.xml
- <application>
- <meta-data
- android:name="TINKER_ID"
- android:value="tinker_id_6235657" />
- //...
- </application>
到此 API 相关的就结束了,剩下的就是考虑 patch 如何生成。
tinker 提供了 patch 生成的工具,源码见: ,打成一个 jar 就可以使用,并且提供了命令行相关的参数以及文件。
命令行如下:
- java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output
需要注意的就是
,里面包含 tinker 的配置,例如签名文件等。
- tinker_config.xml
这里我们直接使用 tinker 提供的签名文件,所以不需要做修改,不过里面有个 Application 的 item 修改为与本例一致:
- <loader value="com.zhy.tinkersimplein.SimpleTinkerInApplication"/>
大致的文件结构如下:
可以在 中提取,或者直接下载文末的例子。
上述介绍了 patch 生成的命令,最后需要注意的就是,在第一次打出 apk 的时候,保留下生成的 mapping 文件,在
。
- /build/outputs/mapping/release/mapping.txt
可以 copy 到与
同目录,同时在第二次打修复包的时候,在
- proguard-rules.pro
中添加上:
- proguard-rules.pro
- -applymapping mapping.txt
保证后续的打包与线上包使用的是同一个 mapping 文件。
tinker 本身的混淆相关配置,可以参考:
如果,你对该部分描述不了解,可以直接查看源码即可。
首先随便生成一个 apk(API、混淆相关已经按照上述引入),安装到手机或者模拟器上。
然后,copy 出 mapping.txt 文件,设置
,修改代码,再次打包,生成 new.apk。
- applymapping
两次的 apk,可以通过命令行指令去生成 patch 文件。
如果你下载本例,命令需要在 [该目录] 下执行。
最终会在 output 文件夹中生成产物:
我们直接将 patch_signed.apk push 到 sdcard,点击 loadpatch,一定要观察命令行是否成功。
本例修改了 title。
点击 loadPatch,观察 log,如果成功,应用默认为重启,然后再次启动即可达到修复效果。
到这里命令行的方式就介绍完了,和 Andfix 的接入的方式基本上是一样的。
值得注意的是: 该例仅展示了基本的接入,对于 tinker 的各种配置信息,还是需要去读 tinker 的文档(如果你确定要使用) 。
gradle 接入的方式应该算是主流的方式,所以 tinker 也直接给出了例子,单独将该 以 project 方式引入即可。
引入之后,可以查看其接入 API 的方式,以及相关配置。
在你每次 build 时,会在
下生成本地打包的 apk,R 文件,以及 mapping 文件。
- build/bakApk
如果你需要生成 patch 文件,可以通过:
- ./gradlew tinkerPatchRelease // 或者 ./gradlew tinkerPatchDebug
生成。
生成目录为:
- build/outputs/tinkerPatch
需要注意的是,需要在 app/build.gradle 中设置相比较的 apk(即 old.apk,本次为 new.apk),
- ext {
- tinkerEnabled = true
- //old apk file to build patch apk
- tinkerOldApkPath = "${bakPath}/old.apk"
- //proguard mapping file to build patch apk
- tinkerApplyMappingPath = "${bakPath}/old-mapping.txt"
- }
提供的例子,基本上展示了 tinker 的自定义扩展的方式,具体还可以参考:
所以,如果你使用命令行方式接入,也不要忘了学习下其支持哪些扩展。
从注释和命名上看:
- //可选,用于生成application类
- provided('com.tencent.tinker:tinker-android-anno:1.7.7')
明显是该库,其结构如下:
典型的编译时注解的项目,源码见 。
入口为
,可以在该
- com.tencent.tinker.anno.AnnotationProcessor
文件中找到处理类全路径。
- services/javax.annotation.processing.Processor
再次建议,如果你不了解,简单阅读下 该文。
直接看
方法:
- AnnotationProcessor的process
- @Override
- public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
- processDefaultLifeCycle(roundEnv.getElementsAnnotatedWith(DefaultLifeCycle.class));
- return true;
- }
直接调用了 processDefaultLifeCycle:
- private void processDefaultLifeCycle(Set<? extends Element> 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.tmpl
- final 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();
- }
- }
代码比较简单,可以分三部分理解:
对象,获取该注解中声明属性的值。
- @DefaultLifeCycle
我们看一眼模板文件:
- 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%);
- }
- }
对应我们的
,
- SimpleTinkerInApplicationLike
- @DefaultLifeCycle(application = ".SimpleTinkerInApplication",
- flags = ShareConstants.TINKER_ENABLE_ALL,
- loadVerifyFlag = false)
- public class SimpleTinkerInApplicationLike extends ApplicationLike {}
主要就几个占位符:
的 loaderClass 属性,默认值为
- @DefaultLifeCycle
- com.tencent.tinker.loader.TinkerLoader
于是最终生成的代码为:
- /**
- *
- * 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);
- }
- }
tinker 这么做的目的,文档上是这么说的:
为了减少错误的出现,推荐使用 Annotation 生成 Application 类。
这样大致了解了 Application 是如何生成的。
接下来我们大致看一下 tinker 的原理。
来源于:
tinker 贴了一张大致的原理图。
可以看出:
tinker 将 old.apk 和 new.apk 做了 diff,拿到 patch.dex,然后将 patch.dex 与本机中 apk 的 classes.dex 做了合并,生成新的 classes.dex,运行时通过反射将合并后的 dex 文件放置在加载的 dexElements 数组的前面。
运行时替代的原理,其实和 Qzone 的方案差不多,都是去反射修改 dexElements。
两者的差异是:Qzone 是直接将 patch.dex 插到数组的前面;而 tinker 是将 patch.dex 与 app 中的 classes.dex 合并后的全量 dex 插在数组的前面。
tinker 这么做的目的还是因为 Qzone 方案中提到的
的解决方案存在问题;而 tinker 相当于换个思路解决了该问题。
- CLASS_ISPREVERIFIED
接下来我们就从代码中去验证该原理。
本片文章源码分析的两条线:
加载的代码实际上在生成的 Application 中调用的,其父类为 TinkerApplication,在其 attachBaseContext 中辗转会调用到 loadTinker() 方法,在该方法内部,反射调用了 TinkerLoader 的 tryLoad 方法。
- @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;
- }
tryLoadPatchFilesInternal 中会调用到
方法:
- loadTinkerJars
- private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {
- // 省略大量安全性校验代码
- if (isEnabledForDex) {
- //tinker/patch.info/patch-641e634c/dex
- boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
- if (!dexCheck) {
- //file not found, do not load patch
- Log.w(TAG, "tryLoadPatchFiles:dex check fail");
- return;
- }
- }
- //now we can load patch jar
- if (isEnabledForDex) {
- boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);
- if (!loadTinkerJars) {
- Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
- return;
- }
- }
- }
TinkerDexLoader.checkComplete 主要是用于检查下发的 meta 文件中记录的 dex 信息(meta 文件,可以查看生成 patch 的产物,在 assets/dex-meta.txt),检查 meta 文件中记录的 dex 文件信息对应的 dex 文件是否存在,并把值存在 TinkerDexLoader 的静态变量 dexList 中。
TinkerDexLoader.loadTinkerJars 传入四个参数,分别为 application,tinkerLoadVerifyFlag(注解上声明的值,传入为 false),patchVersionDirectory 当前 version 的 patch 文件夹,intent,当前 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<File> legalFiles = new ArrayList<>();
- final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();
- for (ShareDexDiffPatchInfo info : dexList) {
- //for dalvik, ignore art support dex
- if (isJustArtSupportDex(info)) {
- continue;
- }
- String path = dexPath + info.realName;
- File file = new File(path);
- legalFiles.add(file);
- }
- // just for art
- if (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;
- }
找出仅支持 art 的 dex,且当前 patch 是否仅适用于 art 时,并行去 loadDex。
关键是最后的 installDexes:
- @SuppressLint("NewApi")
- public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> 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 dex2opt
- if (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 done
- sPatchDexCount = files.size();
- Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
- if (!checkDexInstall(classLoader)) {
- //reset patch dex
- SystemClassLoaderAdder.uninstallPatchDex(classLoader);
- throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
- }
- }
- }
这里实际上就是根据不同的系统版本,去反射处理 dexElements。
我们看一下 V19 的实现(主要我看了下本机只有个 22 的源码~):
- private static final class V19 {
- private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
- File optimizedDirectory)
- throws IllegalArgumentException, IllegalAccessException,
- NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
- Field pathListField = ShareReflectUtil.findField(loader, "pathList");
- Object dexPathList = pathListField.get(loader);
- ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
- ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
- new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
- suppressedExceptions));
- if (suppressedExceptions.size() > 0) {
- for (IOException e : suppressedExceptions) {
- Log.w(TAG, "Exception in makeDexElement", e);
- throw e;
- }
- }
- }
- }
这里其实和 Qzone 的提出的方案基本是一致的。如果你以前未了解过 Qzone 的方案,可以参考此文:
这里的入口为:
- TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
- Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");
上述代码会调用 DefaultPatchListener 中的 onPatchReceived 方法:
- # 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;
- }
首先对 tinker 的相关配置(isEnable)以及 patch 的合法性进行检测,如果合法,则调用
。
- TinkerPatchService.runPatchService(context, path);
TinkerPatchService 是 IntentService 的子类,这里通过 intent 设置了两个参数,一个是 patch 的路径,一个是 resultServiceClass,该值是调用 Tinker.install 的时候设置的,默认为
- 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);
- }
- }
。由于是 IntentService,直接看 onHandleIntent 即可,如果你对 IntentService 陌生,可以查看此文: 。
- DefaultTinkerResultService.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));
- }
比较清晰,主要关注 upgradePatchProcessor.tryPatch 方法,调用的是 UpgradePatch.tryPatch。ps: 这里有个有意思的地方 increasingPriority(),其内部实现为:
- 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 InnerService
- startService(new Intent(this, InnerService.class));
- }
- } catch (Throwable e) {
- TinkerLog.i(TAG, "try to increase patch process priority error:" + e);
- }
- }
如果你对 "保活" 这个话题比较关注,那么对这段代码一定不陌生,主要是利用系统的一个漏洞来启动一个前台 Service。如果有兴趣,可以参考此文: 。
下面继续回到 tryPatch 方法:
- # 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 exist
- SharePatchInfo oldInfo = manager.getTinkerLoadResultIfPresent().patchInfo;
- String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
- //use md5 as version
- patchResult.patchVersion = patchMd5;
- SharePatchInfo newInfo;
- //already have patch
- if (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 patch
- final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
- final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);
- final String patchVersionDirectory = patchDirectory + "/" + patchName;
- //copy file
- File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));
- // check md5 first
- if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {
- SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
- }
- //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
- if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory,
- destPatchFile)) {
- TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
- return false;
- }
- return true;
- }
拷贝 patch 文件拷贝至私有目录,然后调用
:
- DexDiffPatchInternal.tryRecoverDexFiles
- 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;
- }
直接看 patchDexExtractViaDexDiff
- 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.loadDex
- return true;
- }
核心代码主要在 extractDexDiffInternals 中:
- private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
- //parse meta
- ArrayList<ShareDexDiffPatchInfo> patchList = new ArrayList<>();
- ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
- File directory = new File(dir);
- //I think it is better to extract the raw files from apk
- Tinker manager = Tinker.with(context);
- ZipFile apk = null;
- ZipFile patch = null;
- ApplicationInfo applicationInfo = context.getApplicationInfo();
- String apkPath = applicationInfo.sourceDir; //base.apk
- apk = 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;
- }
这里的代码比较关键了,可以看出首先解析了 meta 里面的信息,meta 中包含了 patch 中每个 dex 的相关数据。然后通过 Application 拿到 sourceDir,其实就是本机 apk 的路径以及 patch 文件;根据 mate 中的信息开始遍历,其实就是取出对应的 dex 文件,最后通过 patchDexFile 对两个 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);
- }
通过 ZipFile 拿到其内部文件的 InputStream,其实就是读取本地 apk 对应的 dex 文件,以及 patch 中对应 dex 文件,对二者的通过 executeAndSaveTo 方法进行合并至 patchedDexFile,即 patch 的目标私有目录。
至于合并算法,这里其实才是 tinker 比较核心的地方,这个算法跟 dex 文件格式紧密关联,如果有机会,然后我又能看懂的话,后面会单独写篇博客介绍。此外 dodola 已经有篇博客进行了介绍:
感兴趣的可以阅读下。
好了,到此我们就大致了解了 tinker 热修复的原理~~
测试 demo 地址:
当然这里只分析了代码了热修复,后续考虑分析资源以及 So 的热修、核心的 diff 算法、以及 gradle 插件等相关知识~
最后欢迎关注我的公众号~
我的微信公众号:hongyangAndroid
(可以给我留言你想学习的文章,支持投稿)
来源: