本文已在我的公众号hongyangAndroid首发。
转载请标明出处:

http://blog.csdn.net/lmj623565791/article/details/54882693
本文出自张鸿洋的博客

一、概述

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

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

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

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

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

这么看来,假设选择开源方案,tinker眼下是最佳的选择,tinker的介绍有这么一句:

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

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

二、接入Tinker

接入tinker眼下给了两种方式。一种是基于命令行的方式。相似于AndFix的接入方式;一种就是gradle的方式。

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

(1)命令行接入

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

  • 对于API

    一般来说,我们接入热修库。会在Application#onCreate中进行一下初始化操作。然后在某个地方去调用相似loadPatch这种API去载入patch文件。

  • 对于patch的生成

    简单的方式就是通过两个apk做对照然后生成;须要注意的是:两个apk做对照,须要的前提条件,第二次打包混淆所使用的mapping文件应该和线上apk是一致的。

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

有了大致的概念,我们就基本了解命令行接入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引入

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去使用,注意顶部的注解:@DefaultLifeCycle,其application属性,会在编译期生成一个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须要在AndroidManifest.xml中指定TINKER_ID。

<application>
<meta-data
android:name="TINKER_ID"
android:value="tinker_id_6235657" />
//...
</application>

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

patch生成

tinker提供了patch生成的工具。源代码见:tinker-patch-cli,打成一个jar就能够使用,而且提供了命令行相关的參数以及文件。

命令行例如以下:

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

须要注意的就是tinker_config.xml,里面包括tinker的配置,比如签名文件等。

这里我们直接使用tinker提供的签名文件,所以不须要做改动,只是里面有个Application的item改动为与本例一致:

<loader value="com.zhy.tinkersimplein.SimpleTinkerInApplication"/>

大致的文件结构例如以下:

能够在tinker-patch-cli中提取,或者直接下载文末的样例。

上述介绍了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文件。设置applymapping。改动代码。再次打包,生成new.apk。

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

假设你下载本例,命令须要在[该文件夹]下执行。

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

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

本例改动了title。

点击loadPatch,观察log。假设成功,应用默觉得重新启动,然后再次启动就可以达到修复效果。

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

值得注意的是:该例仅展示了主要的接入。对于tinker的各种配置信息。还是须要去读tinker的文档(假设你确定要使用)tinker-wiki

(2)gradle接入

gradle接入的方式应该算是主流的方式,所以tinker也直接给出了样例,单独将该tinker-sample-android以project方式引入就可以。

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

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

假设你须要生成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是怎样编译时生成的

从凝视和命名上看:

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

明显是该库。其结构例如以下:

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

入口为com.tencent.tinker.anno.AnnotationProcessor,能够在该services/javax.annotation.processing.Processor文件里找到处理类全路径。

再次建议,假设你不了解。简单阅读下Android 怎样编写基于编译时注解的项目该文。

直接看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(); }
}

代码比較简单,能够分三部分理解:

  • 步骤1:首先找到被DefaultLifeCycle标识的Element(为类对象TypeElement),得到该对象的包名,类名等信息,然后通过该对象。拿到@DefaultLifeCycle对象,获取该注解中声明属性的值。

  • 步骤2:读取一个模板文件,读取为字符串。将各个占位符通过步骤1中的值替代。
  • 步骤3:通过JavaFileObject将替换完毕的字符串写文件。事实上就是本例中的Application对象。

我们看一眼模板文件:

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 {}

主要就几个占位符:

  • 包名,假设application属性值以点開始。则同包;否则则截取
  • 类名,application属性值中的类名
  • %TINKER_FLAGS%相应flags
  • %APPLICATION_LIFE_CYCLE%,编写的ApplicationLike的全路径
  • “%TINKER_LOADER_CLASS%”。这个值我们没有设置,实际上相应@DefaultLifeCycle的loaderClass属性,默认值为com.tencent.tinker.loader.TinkerLoader
  • %TINKER_LOAD_VERIFY_FLAG%相应loadVerifyFlag

于是终于生成的代码为:

/**
*
* 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的原理。

四、原理

来源于:https://github.com/Tencent/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方案中提到的CLASS_ISPREVERIFIED的解决方式存在问题;而tinker相当于换个思路攻克了该问题。

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

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

  • 应用启动时,从默认文件夹载入合并后的classes.dex
  • patch下发后,合成classes.dex至目标文件夹

五、源代码分析

(1)载入patch

载入的代码实际上在生成的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;
}
}
}
}
  1. 找到PathClassLoader(BaseDexClassLoader)对象中的pathList对象
  2. 依据pathList对象找到当中的makeDexElements方法,传入patch相关的相应的实參,返回Element[]对象
  3. 拿到pathList对象中原本的dexElements方法
  4. 步骤2与步骤3中的Element[]数组进行合并,将patch相关的dex放在数组的前面
  5. 最后将合并后的数组。设置给pathList

这里事实上和Qzone的提出的方案基本是一致的。假设你曾经未了解过Qzone的方案。能够參考此文:

(2)合成patch

这里的入口为:

 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);

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

TinkerPatchService是IntentService的子类,这里通过intent设置了两个參数。一个是patch的路径,一个是resultServiceClass,该值是调用Tinker.install的时候设置的。默觉得DefaultTinkerResultService.class。由于是IntentService。直接看onHandleIntent就可以,假设你对IntentService陌生,能够查看此文:Android IntentService全然解析 当Service遇到Handler

@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。

假设有兴趣,能够參考此文:关于 Android 进程保活。你所须要知道的一切

以下继续回到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

(能够给我留言你想学习的文章,支持投稿)

Android 热修复 Tinker接入及源代码浅析的更多相关文章

  1. Android 热修复 Tinker接入及源码浅析

    一.概述 放了一个大长假,happy,先祝大家2017年笑口常开. 假期中一行代码没写,但是想着马上要上班了,赶紧写篇博客回顾下技能,于是便有了本文. 热修复这项技术,基本上已经成为项目比较重要的模块 ...

  2. Android热修复——Tinker的集成

    前言 做前端开发的都知道,当我们项目做完了以后,都会把应用上传到应用市场上供用户下载使用,比如上传到应用宝啊,应用汇啊,360啊,小米,华为,魅族啊,等等但是,有时候我们会经常遇到一些很扯淡的事情,刚 ...

  3. Android 热修复 Tinker platform 中的坑,以及详细步骤(二)

    操作流程: 一.注册平台账号: http://www.tinkerpatch.com 二.查看操作文档: http://www.tinkerpatch.com/Docs/SDK 参考文档: https ...

  4. Android热修复之微信Tinker使用初探

      文章地址:Android热修复之微信Tinker使用初探 前几天,万众期待的微信团队的Android热修复框架tinker终于在GitHub上开源了. 地址:https://github.com/ ...

  5. Android 热修复方案Tinker

    转自:http://blog.csdn.net/l2show/article/details/53925543 Android 热修复方案Tinker(一) Application改造 Android ...

  6. Android 热修复方案Tinker(一) Application改造

    基于Tinker V1.7.5 Android 热修复方案Tinker(一) Application改造    Android 热修复方案Tinker(二) 补丁加载流程    Android 热修复 ...

  7. 深入探索Android热修复技术原理读书笔记 —— 代码热修复技术

    在前一篇文章 深入探索Android热修复技术原理读书笔记 -- 热修复技术介绍中,对热修复技术进行了介绍,下面将详细介绍其中的代码修复技术. 1 底层热替换原理 在各种 Android 热修复方案中 ...

  8. 全面了解Android热修复技术

    WeTest 导读 本文探讨了Android热修复技术的发展脉络,现状及其未来. 热修复技术概述 热修复技术在近年来飞速发展,尤其是在InstantRun方案推出之后,各种热修复技术竞相涌现.国内大部 ...

  9. Android热修复框架汇总整理(Hotfix)

      Android平台出现了一些优秀的热更新方案,主要可以分为两类:一类是基于multidex的热更新框架,包括Nuwa.Tinker等:另一类就是native hook方案,如阿里开源的Andfix ...

随机推荐

  1. mount挂载WINDOWS分区和目录

    转自:http://blog.163.com/sg_liao/blog/static/29577083200942811445981/ 一,挂载共享目录 sudo mount -t cifs  -o ...

  2. sharepoint admin svc must be running in order to create deployment timer job 若要创建计时器作业,必须执行SVC

    sharepoint admin svc must be running in order to create deployment timer job 若要创建计时器作业.必须执行SVC       ...

  3. 织梦dedecms修改include和plus重命名提高安全性防漏洞注入挂马

    织梦dedecms是新手站长使用得比较多的一个建站开源程序,正因如此,也是被被入侵挂马比较多的程序.下面就来跟大家说一下怎么重新命名dedecms的include文件夹以及plus文件夹来提高网站的安 ...

  4. oracle 查询 函数练习

    /*--以下代码是对emp表进行显示宽度设置col empno for 9999;col ename for a10;col job for a10;col mgr for 9999; col hir ...

  5. Vue Element Form表单时间验证控件使用

    如果直接使用Element做时间选择器,其规则(rules)不添加type:'date',会提示类型错误,处理这个需要规范值的类型为date. 时间格式化过滤器 import Vue from 'vu ...

  6. java基础讲解14-----IO

    package com.io; import java.io.File;import java.io.IOException; public class IoClass {        /**   ...

  7. adb 修改手机代理方式

    一.使用全局命令 设置代理: adb shell settings put global http_proxy 代理IP地址:端口号 如: adb shell settings put global ...

  8. Linux监控平台搭建

    Linux监控平台介绍 zabbix监控介绍 zabbix监控流程图 安装zabbix 准备两台主机: zabbix服务端:192.168.133.88 zabbix客户端:192.168.133.6 ...

  9. PHP:计算文件或数组中单词出现频率

    一:如果是小文件,可以一次性读入到数组中,使用方便的数组计数函数进行词频统计(假设文件中内容都是空格隔开的单词): <?php $str = file_get_contents("/p ...

  10. yum安装Apache Web Server后各个文件存放位置

    yum安装Apache Web Server后各个文件存放位置   用yum安装apache软件: yum -y install httpd 安装完成后,来查看理解yum安装软件的过程和安装路径.   ...