Tinker 合并及加载补丁过程源码分析 (三)
声明:本文已授权微信公众号 YYGeeker 独家发布。博主原创文章,转载请注明出处:小嵩的博客
本系列传送门:
微信Tinker 热修复介绍及接入(一)
Tinker 原理深入理解(二)
Tinker 合并及加载补丁过程源码分析 (三)
前言
上篇文章我们讲了Tinker实现的主要原理,本篇文章主要对Tinker源码中补丁安装合并以及加载过程进行分析,本文分析基于Tinker 1.9.8 版本。主要内容有以下几点:
- 安装合并补丁包过程。
- 加载补丁过程分析。
- 加载补丁资源过程分析。
- 加载补丁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.}}...
}
流程大致如下:
- 获取 ActivityThread 实例,并反射获取mResDir、mPackages等字段。
- 出于兼容目的(如百度自定义了BaiduAssetManager),新建一个AssetManager对象,拿到其中的addAssetPath方法的反射addAssetPathMethod。
- 拿到ensureStringBlocks的反射,然后区分版本拿到Resources的集合。
- SDK >= 19,从ResourcesManager中拿到mActiveResources变量,是个持有Resources的ArrayMap,赋值给references,Android N中该变量叫做mResourceReferences.
- SDK < 19,从ActivityThread中获取mActiveResources,是个HashMap持有Resources,赋值给references.
- 如果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);}// 省略代码···}} ...
}
简单描述一下:
- 利用反射调用新建的AssetManager的addAssetPath将路径传进去。
- 主动调用ensureStringBlocksMethod方法确保资源的字符串索引创建出来。
- 循环遍历持有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 合并及加载补丁过程源码分析 (三)相关推荐
- 这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析
前言 package com.jvm.classloader;class Father2{public static String strFather="HelloJVM_Father&qu ...
- jQuery deferred应用dom加载完毕详细源码分析(三)
我承认上章ajax部分写得不好,不要怪我,它的ajax代码太多了,而且跨越大,方法跳跃多,实在不好排版与讲解,但如果你真正想研究源码并且仔细读了得话,你的 收获应该会很大,至少你明白了js的ajax是 ...
- 描述一下JAVA的加载过程_JVM源码分析之Java类的加载过程
简书 占小狼 转载请注明原创出处,谢谢! 趁着年轻,多学习 背景 最近对Java细节的底层实现比较感兴趣,比如Java类文件是如何加载到虚拟机的,类对象和方法是以什么数据结构存在于虚拟机中?虚方法.实 ...
- clamav --reload 加载病毒库源码分析
基本流程可以参考clamav中clamdscan --version 不生效 我们直接从解析command开始.parse_command函数返回COMMAND_RELOAD类型.然后进入execut ...
- 下血本买的!Flutter中网络图片加载和缓存源码分析,看看这篇文章吧!
目录 想要成为一名优秀的Android开发,你需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样. PagerAdapter 介绍 ViwePager 缓存策略 ViewPager 布局处 ...
- Android开发必会技术!Flutter中网络图片加载和缓存源码分析,完整PDF
起因 事情是这样的. 4年前毕业那会,呆在公司的短视频项目,做 视频.那会做得比抖音还早,但是由于短视频太烧钱了,项目被公司关掉了.当时需要开发横竖屏直播/异步视频的场景,就研究下了市场上的 app, ...
- Flutter中网络图片加载和缓存源码分析,踩坑了
关于Android的近况 大家都知道,今年移动开发不那么火热了,完全没有了前两年Android开发那种火热的势头,如此同时,AI热火朝天,很多言论都说Android不行了.其实不光是Android,i ...
- 2款不同样式的CSS3 Loading加载动画 附源码
原文:2款不同样式的CSS3 Loading加载动画 附源码 我们经常看到的Loading加载很多都是转圈圈的那种,今天我们来换一种有创意的CSS3 Loading加载动画,一种是声波形状的动画,另一 ...
- 2.2 LayoutInflater 加载布局文件源码
LayoutInflater 加载布局文件源码 LayoutInflater是一个用于将xml布局文件加载为View或者ViewGroup对象的工具,我们可以称之为布局加载器. 获取LayoutInf ...
- html动画爱心制作代码,CSS心形加载的动画源码的实现
废话不多说上代码,代码很简答,研究一下就明白了,有不明白的可以问我. .heart-loading { margin-top: 120px; width: 200px; height: 200px; ...
最新文章
- zabbix 清空历史表
- 判断两棵树是否相等与使用二叉链表法建立二叉搜索树
- 惠普企业第三财季净利润23亿美元 同比增长914%
- 32岁前平凡无奇,鼓动同事创业,最终逆袭成硅谷首富
- ActiveMQ入门-ActiveMQ跟SpringBoot整合发送接收Topic
- 全国计算机等级考试题库二级C操作题100套(第72套)
- 关注 | 新冠病毒这次的突变毒株太可怕,与人受体亲和力提高了1000倍,传播提高70%!已经成为伦敦地区主要毒株...
- VC中对CString 的读写(ini文件)
- 网址发布收藏页源码自适应
- oracle导入字符集,Oracle导入字符集问题
- ironpython不想要可以卸载吗_IronPython的致命弱点
- python全栈开发 * 04 * 180604
- nginx中配置虚拟主机
- 百度蜘蛛IP功能说明初稿
- The 'mode' option has not been set, webpack will fallback to 'production' for th is value
- 台式计算机的安装顺序,台式电脑安装步骤教程
- win10笔记本电脑 QQ能发消息但是不能传输文件 微信断网 浏览器也打不开
- 如何破解招聘面试中暗藏的八大玄机?
- tablepc是什么平板电脑_平板电脑是什么
- 我们欺骗了活动主办方