声明:本文已授权微信公众号 YYGeeker 独家发布。博主原创文章,转载请注明出处:小嵩的博客

本系列传送门:
微信Tinker 热修复介绍及接入(一)
Tinker 原理深入理解(二)
Tinker 合并及加载补丁过程源码分析 (三)

前言

上篇文章我们讲了Tinker实现的主要原理,本篇文章主要对Tinker源码中补丁安装合并以及加载过程进行分析,本文分析基于Tinker 1.9.8 版本。主要内容有以下几点:

  1. 安装合并补丁包过程。
  2. 加载补丁过程分析。
  3. 加载补丁资源过程分析。
  4. 加载补丁SO文件分析。

一、安装合并补丁包过程分析

时序图如下:

大致流程:

1.1 在代理 Application 中初始化 Tinker 相关。

我们可以看到Tinker方案中,TinkerApplication继承自Application,也就是说它才是应用真正的Application。Tinker方法使用了ApplicationLike 来代理我们的Application。因此在代理类ApplicationLike 的实现类(demo中是SampleApplicationLike)中对Tinker进行了一些初始化操作。我们可以来看看代码:

TinkerManager中创建了几种Reporter 以及 UpgradePatch 对象。

1.2 调用入口(App主进程)。

当补丁包下发到本地,调用它开始补丁合成:

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");

1.3 获取监听器,并调用TinkerPatchService。

TinkerInstaller类中调用Tinker对象实例,然后获取Listener并调用onPatchReceived方法:


public class TinkerInstaller {//省略部分代码...public static void onReceiveUpgradePatch(Context context, String patchLocation) {Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);}...
}
public class DefaultPatchListener implements PatchListener {//省略部分代码...@Overridepublic int onPatchReceived(String path) {File patchFile = new File(path);int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));//校验Patch 合法性。if (returnCode == ShareConstants.ERROR_PATCH_OK) {TinkerPatchService.runPatchService(context, path);//开启Serive进程服务进行合并Patch} else {Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);}return returnCode;}//省略部分代码...
}

1.4 开启Service服务或者JobScheduler合并Patch。

public class TinkerPatchService {//省略部分代码...public static void runPatchService(final Context context, final String path) {try {if (Build.VERSION.SDK_INT < MIN_SDKVER_TO_USE_JOBSCHEDULER) {runPatchServiceByIntentService(context, path);} else {try {runPatchServiceByJobScheduler(context, path);} catch (Throwable ignored) {// ignored.}mHandler.postDelayed(new Runnable() {@Overridepublic void run() {TinkerLog.i(TAG, "check if patch service is running.");if (!TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {TinkerLog.w(TAG, "patch service is not running, retry with IntentService.");try {runPatchServiceByIntentService(context, path);TinkerLog.i(TAG, "successfully start patch service with IntentService.");} catch (Throwable thr) {TinkerLog.e(TAG, "failure to start patch service with IntentService. osver: %s, manu: %s, msg: %s", Build.VERSION.SDK_INT, Build.MANUFACTURER, thr.toString());}}}}, TimeUnit.SECONDS.toMillis(5));}} catch (Throwable throwable) {TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);}}}

我们通过源码可以看到,TinkerPatchService 是合并补丁时,比较重要的一个类。runPatchService 方法中,对于Android8.0以下版本使用了 IntentService,并提高服务优先级来避免Patch进程被系统kill掉。针对 Android 8.0 版本额外做了兼容,通过 JobScheduler 以及 Hanlder 循环来保证能够正常开启多进程服务进行补丁合并。

1.5 调用doApplyPatch方法执行合并及校验操作。

public class TinkerPatchService {//省略部分代码...
private static void doApplyPatch(Context context, Intent intent) {//前面这段代码做了一些校验和琐碎逻辑。PatchResult patchResult = new PatchResult();try {if (upgradePatchProcessor == null) {throw new TinkerRuntimeException("upgradePatchProcessor is null.");}result = upgradePatchProcessor.tryPatch(context, path, patchResult);} catch (Throwable throwable) {e = throwable;result = false;tinker.getPatchReporter().onPatchException(patchFile, e);}//省略部分代码,主要做了一些监听器的回调。}
}

可以看到,doApplyPatch 方法中调用了我们在1.1节所述,即 Tinker 初始化时所 new 出来的 UpgradePatch 对象。

1.6 调用UpgradePatch执行合并补丁逻辑。

public class UpgradePatch extends AbstractPatch {@Overridepublic boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {//省略部分代码,主要做了一些校验逻辑及文件拷贝删除。//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;}if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");return false;}if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");return false;}// check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpretedif (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");return false;}if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);return false;}TinkerLog.w(TAG, "UpgradePatch tryPatch: done, it is ok");return true;}

可以看到,我们的补丁包真正合并的逻辑都是在这里执行的,通过DexDiff、BsDiff、ResDiff 类分别去合并补丁包的Dex类文件,So文件,Resource资源。

1.7 补丁合并完成,回调告知主进程补丁合并结果。

public class TinkerPatchService {//省略部分代码...
private static void doApplyPatch(Context context, Intent intent) {//省略部分代码, 文章上述1.5节已标记。tinker.getPatchReporter().onPatchResult(patchFile, result, cost); //Reporter回调patchResult.isSuccess = result;patchResult.rawPatchFilePath = path;patchResult.costTime = cost;patchResult.e = e;//回调给ResulrSerive,通知合并结果AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));}
}

从上面的代码可知,Tinker在合并补丁完成后,会调用Reporter的onPatchResult方法以及ResultService的onPatchResult方法,来通知主进程合并结果。其中,代码中的 AbstractResultService.runResultService 开启的是 Intent 中 RESULT_CLASS_EXTRA 这个参数对应的 Service 类。它默认是在 Tinker 初始化的时候设置的,代码如下:

public class Tinker {//省略部分代码...public void install(Intent intentResult) {install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());}
}

总体的流程大家可以参考时序图,时序图更加直观易懂些。

二、加载补丁过程分析

由于 Tinker 属于 ClassLoader 方案, 启动时基础包的类已经被虚拟机加载,所以在合并完补丁后并不能及时生效,需要重启应用后补丁才能生效。接下来我们开始分析打完补丁包后,启动 App 时 Tinker 加载补丁的大致过程:

时序图如下:

2.1 代理类TinkerApplication入口。

从AndroidManifest.xml 文件可以看出 application 是SampleApplication,它继承于TinkerApplication这个抽象类。
应用启动后,入口是attachBaseContext方法,它调用了onBaseContextAttached方法。

   public abstract class TinkerApplication extends Application {//省略部分代码.../*** Hook for sub-classes to run logic after the {@link Application#attachBaseContext} has been* called but before the delegate is created. Implementors should be very careful what they do* here since {@link android.app.Application#onCreate} will not have yet been called.*/private void onBaseContextAttached(Context base) {applicationStartElapsedTime = SystemClock.elapsedRealtime();applicationStartMillisTime = System.currentTimeMillis();loadTinker();ensureDelegate();applicationLike.onBaseContextAttached(base);//reset save modeif (useSafeMode) {String processName = ShareTinkerInternals.getProcessName(this);String preferName = ShareConstants.TINKER_OWN_PREFERENCE_CONFIG + processName;SharedPreferences sp = getSharedPreferences(preferName, Context.MODE_PRIVATE);sp.edit().putInt(ShareConstants.TINKER_SAFE_MODE_COUNT, 0).commit();}}
}

从代码可以看到,调用了loadTinker来加载Tinker相关逻辑。然后loadTinker执行完毕后调用了applicationLike代理类对象的onBaseContextAttached方法,回调Application生命周期。

接下来在loadTinker 方法中通过反射去调用 TinkerLoader 的tryLoad方法:

private void loadTinker() {try {//reflect tinker loader, because loaderClass may be define by user!Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);Constructor<?> constructor = tinkerLoadClass.getConstructor();tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);} catch (Throwable e) {//has exception, put exception error codetinkerResultIntent = new Intent();ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);}}

2.2 TinkerLoader类去加载patch。

    /*** only main process can handle patch version change or incomplete*/@Overridepublic Intent tryLoad(TinkerApplication app) {Intent resultIntent = new Intent();long begin = SystemClock.elapsedRealtime();tryLoadPatchFilesInternal(app, resultIntent);long cost = SystemClock.elapsedRealtime() - begin;ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);return resultIntent;}

这里逻辑相对较简单。记录了加载补丁起始时间戳,然后调用tryLoadPatchFilesInternal方法执行加载补丁的逻辑,计算出补丁加载耗时,将cost time添加到Intent里面去,最后返回Intent。

2.3 执行一系列校验逻辑。

tryLoadPatchFilesInternal方法里面逻辑比较多,考虑篇幅问题这里就不贴代码了,可以自行阅读。

2.4 加载补丁dex 以及 resource。

tryLoadPatchFilesInternal方法中,在校验完成后,执行真正的加载逻辑:

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {//省略代码,一系列校验逻辑...//now we can load patch jarif (isEnabledForDex) {boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);//省略部分代码,系统 OTA操作处理...}//now we can load patch resourceif (isEnabledForResource) {boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);if (!loadTinkerResources) {Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");return;}}//省略部分代码...}

可以看出,最终真正的执行加载补丁类逻辑就是TinkerDexLoader.loadTinkerJars()方法。这里我们顺着loadTinkerJars方法继续往下探索:

/*** Load tinker JARs and add them to* the Application ClassLoader.** @param application The application.*/@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {...PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();String dexPath = directory + "/" + DEX_PATH + "/";ArrayList<File> legalFiles = new ArrayList<>();for (ShareDexDiffPatchInfo info : loadDexList) {//for循环 读取patch包dex文件...legalFiles.add(file);}// verify merge classN.apkif (isVmArt && !classNDexInfo.isEmpty()) {File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);long start = System.currentTimeMillis();...legalFiles.add(classNFile);}//SystemOTA//省略代码... optimizeAlltry {// 加载dex到dexElements去SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);} catch (Throwable e) {...}return true;}

SystemClassLoaderAdder 根据系统版本区分加载patch文件

 @SuppressLint("NewApi")public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)throws Throwable {Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());if (!files.isEmpty()) {files = createSortedAdditionalPathEntries(files);ClassLoader classLoader = loader;if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {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);}}}

由于Android系统不同版本的差异性,可以看到这里是区分不同系统版本来进行加载的。以API 23 为例:

 private static final class V23 {private static void install(ClassLoader loader, List<File> additionalClassPathEntries,File optimizedDirectory)throws IllegalArgumentException, IllegalAccessException,NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {/* The patched class loader is expected to be a descendant of* dalvik.system.BaseDexClassLoader. We modify its* dalvik.system.DexPathList pathList field to append additional DEX* file entries.*/Field pathListField = ShareReflectUtil.findField(loader, "pathList");Object dexPathList = pathListField.get(loader);ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));if (suppressedExceptions.size() > 0) {for (IOException e : suppressedExceptions) {Log.w(TAG, "Exception in makePathElement", e);throw e;}}}
}

小结

追踪到这里,我们终于可以知道,在合并完补丁生成补丁dex后,Tinker是把dex单独存放在本地目录下,在启动时通过反射去获取Application的ClassLoader中的 pathList 字段,然后将 patch 包里面的 dex与基础包的 dexElements 进行合并,最终以此达到类修复手段。

此外,通过上述源码分析我们知道它是将合成后的dex放在本地目录下,那么顺理推舟,很明显就能明白tinekr清除补丁的逻辑,则是将本地目录下的补丁dex清除以达到回退目的。

三、加载补丁资源过程分析

Tinker的资源更新采用的是类似InstantRun的资源补丁方式,通过新建一个AssetManager 来实现功能。加载Resource资源的过程,可以参考“二、加载补丁过程分析”的内容(2.1 - 2.4小节),以及时序图。

具体过程大致可分为六个步骤:

3.1 检查资源文件(res/resources.apk)是否存在

首先先根据res_meta.xml文件中记载的信息检查文件(res/resources.apk)是否存在,若不存在则return。判断逻辑实现在TinkerResourceLoader.checkComplete() 方法。

3.2 判断是否支持反射更新资源

checkComplete方法里面,调用了TinkerResourcePatcher.isResourceCanPatch(context); 来判断是否支持反射更新资源,并存储了反射获取的一些字段。源代码如下:

class TinkerResourcePatcher {public static void isResourceCanPatch(Context context) throws Throwable {//   - Replace mResDir to point to the external resource file instead of the .apk. This is//     used as the asset path for new Resources objects.//   - Set Application#mLoadedApk to the found LoadedApk instance// Find the ActivityThread instance for the current threadClass<?> activityThread = Class.forName("android.app.ActivityThread");currentActivityThread = ShareReflectUtil.getActivityThread(context, activityThread);// API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.Class<?> loadedApkClass;try {loadedApkClass = Class.forName("android.app.LoadedApk");} catch (ClassNotFoundException e) {loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");}resDir = findField(loadedApkClass, "mResDir");packagesFiled = findField(activityThread, "mPackages");if (Build.VERSION.SDK_INT < 27) {resourcePackagesFiled = findField(activityThread, "mResourcePackages");}// Create a new AssetManager instance and point it to the resourcesfinal AssetManager assets = context.getAssets();addAssetPathMethod = findMethod(assets, "addAssetPath", String.class);// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm// in L, so we do it unconditionally.try {stringBlocksField = findField(assets, "mStringBlocks");ensureStringBlocksMethod = findMethod(assets, "ensureStringBlocks");} catch (Throwable ignored) {// Ignored.}// Use class fetched from instance to avoid some ROMs that use customized AssetManager// class. (e.g. Baidu OS)newAssetManager = (AssetManager) findConstructor(assets).newInstance();// Iterate over all known Resources objectsif (SDK_INT >= KITKAT) {//pre-N// Find the singleton instance of ResourcesManagerfinal Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");final Method mGetInstance = findMethod(resourcesManagerClass, "getInstance");final Object resourcesManager = mGetInstance.invoke(null);try {Field fMActiveResources = findField(resourcesManagerClass, "mActiveResources");final ArrayMap<?, WeakReference<Resources>> activeResources19 =(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);references = activeResources19.values();} catch (NoSuchFieldException ignore) {// N moved the resources to mResourceReferencesfinal Field mResourceReferences = findField(resourcesManagerClass, "mResourceReferences");references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);}} else {final Field fMActiveResources = findField(activityThread, "mActiveResources");final HashMap<?, WeakReference<Resources>> activeResources7 =(HashMap<?, WeakReference<Resources>>) fMActiveResources.get(currentActivityThread);references = activeResources7.values();}// check resourceif (references == null) {throw new IllegalStateException("resource references is null");}final Resources resources = context.getResources();// fix jianGuo pro has private field 'mAssets' with Resource// try use mResourcesImpl firstif (SDK_INT >= 24) {try {// N moved the mAssets inside an mResourcesImpl fieldresourcesImplFiled = findField(resources, "mResourcesImpl");} catch (Throwable ignore) {// for safetyassetsFiled = findField(resources, "mAssets");}} else {assetsFiled = findField(resources, "mAssets");}try {publicSourceDirField = findField(ApplicationInfo.class, "publicSourceDir");} catch (NoSuchFieldException ignore) {// Ignored.}}...
}

流程大致如下:

  1. 获取 ActivityThread 实例,并反射获取mResDir、mPackages等字段。
  2. 出于兼容目的(如百度自定义了BaiduAssetManager),新建一个AssetManager对象,拿到其中的addAssetPath方法的反射addAssetPathMethod。
  3. 拿到ensureStringBlocks的反射,然后区分版本拿到Resources的集合。
  4. SDK >= 19,从ResourcesManager中拿到mActiveResources变量,是个持有Resources的ArrayMap,赋值给references,Android N中该变量叫做mResourceReferences.
  5. SDK < 19,从ActivityThread中获取mActiveResources,是个HashMap持有Resources,赋值给references.
  6. 如果references为空,说明该系统不支持资源补丁,throw 一个IllegalStateException被上层调用catch。然后查找系统resources的对应字段将它们存储起来。

3.3 调用TinkerResourceLoader.loadTinkerResources方法

在isResourceCanPatch 方法反射并校验的过程中没有出现异常,checkComplete返回true之后,会调用loadTinkerResources方法,该方法里面主要做了两件事:先验证Patch的MD5,然后调用monkeyPatchExistingResources方法。

 public static boolean loadTinkerResources(TinkerApplication application, String directory, Intent intentResult) {if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {return true;}String resourceString = directory + "/" + RESOURCE_PATH +  "/" + RESOURCE_FILE;File resourceFile = new File(resourceString);long start = System.currentTimeMillis();// 1. 校验Resource资源及MD5值是否合法if (application.isTinkerLoadVerifyFlag()) {if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5);ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);return false;}Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));}//2.执行加载补丁资源文件逻辑。try {TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start));} catch (Throwable e) {Log.e(TAG, "install resources failed");//remove patch dex if resource is installed failedtry {SystemClassLoaderAdder.uninstallPatchDex(application.getClassLoader());} catch (Throwable throwable) {Log.e(TAG, "uninstallPatchDex failed", e);}intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);return false;}return true;}

3.4. monkeyPatchExistingResources方法

这个方法的名字跟InstantRun的资源补丁方法名是一样的,将补丁资源路径(res/resources.apk)传递进去,然后执行资源替换加载逻辑。

class TinkerResourcePatcher {public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {if (externalResourceFile == null) {return;}final ApplicationInfo appInfo = context.getApplicationInfo();final Field[] packagesFields;if (Build.VERSION.SDK_INT < 27) {packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};} else {packagesFields = new Field[]{packagesFiled};}for (Field field : packagesFields) {final Object value = field.get(currentActivityThread);for (Map.Entry<String, WeakReference<?>> entry: ((Map<String, WeakReference<?>>) value).entrySet()) {final Object loadedApk = entry.getValue().get();if (loadedApk == null) {continue;}final String resDirPath = (String) resDir.get(loadedApk);if (appInfo.sourceDir.equals(resDirPath)) {resDir.set(loadedApk, externalResourceFile);}}}// Create a new AssetManager instance and point it to the resources installed underif (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {throw new IllegalStateException("Could not create new AssetManager");}// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm// in L, so we do it unconditionally.if (stringBlocksField != null && ensureStringBlocksMethod != null) {stringBlocksField.set(newAssetManager, null);ensureStringBlocksMethod.invoke(newAssetManager);}for (WeakReference<Resources> wr : references) {final Resources resources = wr.get();if (resources == null) {continue;}// Set the AssetManager of the Resources instance to our brand new onetry {//pre-NassetsFiled.set(resources, newAssetManager);} catch (Throwable ignore) {// Nfinal Object resourceImpl = resourcesImplFiled.get(resources);// for Huawei HwResourcesImplfinal Field implAssets = findField(resourceImpl, "mAssets");implAssets.set(resourceImpl, newAssetManager);}// 省略代码···}}   ...
}

简单描述一下:

  1. 利用反射调用新建的AssetManager的addAssetPath将路径传进去。
  2. 主动调用ensureStringBlocksMethod方法确保资源的字符串索引创建出来。
  3. 循环遍历持有Resources对象的references集合,然后依次替换其中的AssetManager为我们新建的AssetManager对象。

3.5. 调用clearPreloadTypedArrayIssue清除TypeArray以兼容MIUI机型

class TinkerResourcePatcher {/*** Why must I do these?* Resource has mTypedArrayPool field, which just like Message Poll to reduce gc* MiuiResource change TypedArray to MiuiTypedArray, but it get string block from offset instead of assetManager*/private static void clearPreloadTypedArrayIssue(Resources resources) {// Perform this trick not only in Miui system since we can't predict if any other// manufacturer would do the same modification to Android.// if (!isMiuiSystem) {//     return;// }Log.w(TAG, "try to clear typedArray cache!");// Clear typedArray cache.try {final Field typedArrayPoolField = findField(Resources.class, "mTypedArrayPool");final Object origTypedArrayPool = typedArrayPoolField.get(resources);final Method acquireMethod = findMethod(origTypedArrayPool, "acquire");while (true) {if (acquireMethod.invoke(origTypedArrayPool) == null) {break;}}} catch (Throwable ignored) {Log.e(TAG, "clearPreloadTypedArrayIssue failed, ignore error: " + ignored);}}

这里看方法注释即可,主要用于兼容MIUI等系统,就不多阐述了。

3.6. 更新资源状态

最后通过调用resources.updateConfiguration方法,将Resources对象的配置信息更新到最新状态,完成整个资源替换的过程。

目前来看InstantRun的资源更新方式最简便而且兼容性也是比较好的,市面上大多数的热补丁框架都采用这套方案。Tinker的这套方案虽然也采用全量的替换,但是在下发patch中,通过采用差量资源的方式获取到差分包,下发到手机后再合成全量的资源文件,有效控制了补丁文件的大小。

四、加载补丁SO文件分析

Tinker提供了两个入口加载so补丁,分别是TinkerLoadLibrary和TinkerApplicationHelper。TinkerApplicationHelper 是可以在Tinker 尚未 install 的时候就能调用;TinkerLoadLibrary 是多种调用Native Library的都提供了。还有一个TinkerInstaller原先也是提供了一个加载SO补丁的入口,目前从最新源码来看,TinkerInstaller的入口方法已经去掉了,现在一般是用TinkerLoadLibrary来加载。

4.1 TinkerLoadLibrary

    /*** sample usage for native library** @param context* @param relativePath such as lib/armeabi* @param libName      for the lib libTest.so, you can pass Test or libTest, or libTest.so* @return boolean* @throws UnsatisfiedLinkError*/public static boolean loadLibraryFromTinker(Context context, String relativePath, String libName) throws UnsatisfiedLinkError {final Tinker tinker = Tinker.with(context);libName = libName.startsWith("lib") ? libName : "lib" + libName;libName = libName.endsWith(".so") ? libName : libName + ".so";String relativeLibPath = relativePath + "/" + libName;//TODO we should add cpu abi, and the real path laterif (tinker.isEnabledForNativeLib() && tinker.isTinkerLoaded()) {TinkerLoadResult loadResult = tinker.getTinkerLoadResultIfPresent();if (loadResult.libs != null) {for (String name : loadResult.libs.keySet()) {if (name.equals(relativeLibPath)) {String patchLibraryPath = loadResult.libraryDirectory + "/" + name;File library = new File(patchLibraryPath);if (library.exists()) {//whether we check md5 when loadboolean verifyMd5 = tinker.isTinkerLoadVerify();if (verifyMd5 && !SharePatchFileUtil.verifyFileMd5(library, loadResult.libs.get(name))) {tinker.getLoadReporter().onLoadFileMd5Mismatch(library, ShareConstants.TYPE_LIBRARY);} else {System.load(patchLibraryPath);TinkerLog.i(TAG, "loadLibraryFromTinker success:" + patchLibraryPath);return true;}}}}}}return false;}

4.2 TinkerApplicationHelper

 /*** you can use these api to load tinker library without tinker is installed!* same as {@code TinkerInstaller#loadLibraryFromTinker}** @param applicationLike* @param relativePath* @param libname* @return* @throws UnsatisfiedLinkError*/public static boolean loadLibraryFromTinker(ApplicationLike applicationLike, String relativePath, String libname) throws UnsatisfiedLinkError {libname = libname.startsWith("lib") ? libname : "lib" + libname;libname = libname.endsWith(".so") ? libname : libname + ".so";String relativeLibPath = relativePath + "/" + libname;//TODO we should add cpu abi, and the real path laterif (!TinkerApplicationHelper.isTinkerEnableForNativeLib(applicationLike)) {return false;}if (!TinkerApplicationHelper.isTinkerEnableForNativeLib(applicationLike)) {return false;}final HashMap<String, String> loadLibraries = TinkerApplicationHelper.getLoadLibraryAndMd5(applicationLike);if (loadLibraries == null) {return false;}final String currentVersion = TinkerApplicationHelper.getCurrentVersion(applicationLike);if (ShareTinkerInternals.isNullOrNil(currentVersion)) {return false;}final File patchDirectory = SharePatchFileUtil.getPatchDirectory(applicationLike.getApplication());if (patchDirectory == null) {return false;}final File patchVersionDirectory = new File(patchDirectory.getAbsolutePath() + "/" + SharePatchFileUtil.getPatchVersionDirectory(currentVersion));final String libPrePath = patchVersionDirectory.getAbsolutePath() + "/" + ShareConstants.SO_PATH;for (Map.Entry<String, String> libEntry : loadLibraries.entrySet()) {final String name = libEntry.getKey();if (!name.equals(relativeLibPath)) {continue;}final String patchLibraryPath = libPrePath + "/" + name;final File library = new File(patchLibraryPath);if (!library.exists()) {continue;}//whether we check md5 when loadfinal boolean verifyMd5 = applicationLike.getTinkerLoadVerifyFlag();if (verifyMd5 && !SharePatchFileUtil.verifyFileMd5(library, loadLibraries.get(name))) {//do not report, because tinker is not installTinkerLog.i(TAG, "loadLibraryFromTinker md5mismatch fail:" + patchLibraryPath);} else {System.load(patchLibraryPath);TinkerLog.i(TAG, "loadLibraryFromTinker success:" + patchLibraryPath);return true;}}return false;}

Tinker同样是根据so_meta.txt中的补丁信息,来校验so文件是否存在, 然后将so补丁列表存放在结果中libs的字段。从上面代码可以看出,两种方式最终其实都是调用了System.load(patchLibraryPath); 方法来实现更新so的。这是为什么呢?

因为so的更新方式跟dex、resource资源不太一样,系统直接给开发者提供了自定义so目录的选项。因此加载so的话,只需把so文件加载进去然后调用系统 api 即可:

public final class System {...public static void load(String pathName) {Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());}...
}

总结

以上就是Tinker合并以及加载补丁的主要过程,从中我们可以看出,由于Tinker是全量合成方案,因此一个patch包可能会包含多个dex补丁。所以合成时,需要全量合成多个dex文件,这样比较耗费手机性能增加开销。在合并补丁过程时,tinker是通过开启多进程来进行合并以降低合成耗时。此外通过源码分析,我们看到tinker是把dex进行拷贝,然后将全量合成后的产物单独放在自己的文件目录下,每次启动时会去加载该目录下的文件。这样不影响旧Apk的结构,可以做到补丁清除的功能。除此之外,tinker也支持Resource资源、so文件的更新。并且对各个版本,厂商都做了比较全面的兼容逻辑。

通过这篇文章我们深入探索了Tinker方案的合并及加载补丁的主要机制,并对它的源码具体实现逻辑进行了分析。若对该过程还有不太理解的地方,欢迎留言讨论。文章若有错误或表达不当之处,欢迎拍砖指正~

Tinker 合并及加载补丁过程源码分析 (三)相关推荐

  1. 这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析

    前言 package com.jvm.classloader;class Father2{public static String strFather="HelloJVM_Father&qu ...

  2. jQuery deferred应用dom加载完毕详细源码分析(三)

    我承认上章ajax部分写得不好,不要怪我,它的ajax代码太多了,而且跨越大,方法跳跃多,实在不好排版与讲解,但如果你真正想研究源码并且仔细读了得话,你的 收获应该会很大,至少你明白了js的ajax是 ...

  3. 描述一下JAVA的加载过程_JVM源码分析之Java类的加载过程

    简书 占小狼 转载请注明原创出处,谢谢! 趁着年轻,多学习 背景 最近对Java细节的底层实现比较感兴趣,比如Java类文件是如何加载到虚拟机的,类对象和方法是以什么数据结构存在于虚拟机中?虚方法.实 ...

  4. clamav --reload 加载病毒库源码分析

    基本流程可以参考clamav中clamdscan --version 不生效 我们直接从解析command开始.parse_command函数返回COMMAND_RELOAD类型.然后进入execut ...

  5. 下血本买的!Flutter中网络图片加载和缓存源码分析,看看这篇文章吧!

    目录 想要成为一名优秀的Android开发,你需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样. PagerAdapter 介绍 ViwePager 缓存策略 ViewPager 布局处 ...

  6. Android开发必会技术!Flutter中网络图片加载和缓存源码分析,完整PDF

    起因 事情是这样的. 4年前毕业那会,呆在公司的短视频项目,做 视频.那会做得比抖音还早,但是由于短视频太烧钱了,项目被公司关掉了.当时需要开发横竖屏直播/异步视频的场景,就研究下了市场上的 app, ...

  7. Flutter中网络图片加载和缓存源码分析,踩坑了

    关于Android的近况 大家都知道,今年移动开发不那么火热了,完全没有了前两年Android开发那种火热的势头,如此同时,AI热火朝天,很多言论都说Android不行了.其实不光是Android,i ...

  8. 2款不同样式的CSS3 Loading加载动画 附源码

    原文:2款不同样式的CSS3 Loading加载动画 附源码 我们经常看到的Loading加载很多都是转圈圈的那种,今天我们来换一种有创意的CSS3 Loading加载动画,一种是声波形状的动画,另一 ...

  9. 2.2 LayoutInflater 加载布局文件源码

    LayoutInflater 加载布局文件源码 LayoutInflater是一个用于将xml布局文件加载为View或者ViewGroup对象的工具,我们可以称之为布局加载器. 获取LayoutInf ...

  10. html动画爱心制作代码,CSS心形加载的动画源码的实现

    废话不多说上代码,代码很简答,研究一下就明白了,有不明白的可以问我. .heart-loading { margin-top: 120px; width: 200px; height: 200px; ...

最新文章

  1. zabbix 清空历史表
  2. 判断两棵树是否相等与使用二叉链表法建立二叉搜索树
  3. 惠普企业第三财季净利润23亿美元 同比增长914%
  4. 32岁前平凡无奇,鼓动同事创业,最终逆袭成硅谷首富
  5. ActiveMQ入门-ActiveMQ跟SpringBoot整合发送接收Topic
  6. 全国计算机等级考试题库二级C操作题100套(第72套)
  7. 关注 | 新冠病毒这次的突变毒株太可怕,与人受体亲和力提高了1000倍,传播提高70%!已经成为伦敦地区主要毒株...
  8. VC中对CString 的读写(ini文件)
  9. 网址发布收藏页源码自适应
  10. oracle导入字符集,Oracle导入字符集问题
  11. ironpython不想要可以卸载吗_IronPython的致命弱点
  12. python全栈开发 * 04 * 180604
  13. nginx中配置虚拟主机
  14. 百度蜘蛛IP功能说明初稿
  15. The 'mode' option has not been set, webpack will fallback to 'production' for th is value
  16. 台式计算机的安装顺序,台式电脑安装步骤教程
  17. win10笔记本电脑 QQ能发消息但是不能传输文件 微信断网 浏览器也打不开
  18. 如何破解招聘面试中暗藏的八大玄机?
  19. tablepc是什么平板电脑_平板电脑是什么
  20. 我们欺骗了活动主办方

热门文章

  1. ROS学习笔记---ros的通信机制
  2. 一壶浊酒尽余欢、今宵别梦寒!
  3. PHP即充宝v3.0实例
  4. 华硕主板刷机后不能进入Windows的解决办法
  5. 高德地图各种摄像头图标_汽车导航怎么看?高德地图各种符号图标图解大全
  6. ElasticSearch常用语法大全
  7. No rule to make target
  8. 《黑客秘笈——渗透测试实用指南(第2版)》—第2章2.3节外部或内部主动式信息搜集...
  9. 【OpenBMC 系列】D-Bus 调试器介绍 - D-Feet
  10. 个人微信公众号申请步骤