为了账号安全,请及时绑定邮箱和手机立即绑定

Android热补丁之Tinker原理解析 上

标签:
Android

背景

在今年的MDCC大会上,微信开发团队宣布正式开源Tinker,在这之前微信团队已经发出过一些Tinker的相关文章,说实话在开源之前我们还是相当期待Tinker开源的,一方面是因为之前使用的热补丁一直存在一些兼容性问题,另一方面也好奇Tinker的实现方案。

在开源后我们团队第一时间着手研究Tinker,在详细阅读了源码之后,我们确定要在之后的一个版本集成Tinker上线,线上效果显示Tinker的修复效果果然牛逼,错误率明显下降的同时也没有报出兼容性的问题。附一张薄荷app使用Tinker修复前后的错误率对比。

从接入Tinker入手

想要深入某个框架,前提是要学会使用它。我们就从Tinker的接入入手一步一步解开它的实现原理。参照wiki我们做了如下操作。

实现一个Application


public class OneApplicationForTinker extends TinkerApplication {public OneApplicationForTinker() {super(//tinkerFlags, tinker支持的类型,dex,library,还是全部都支持!ShareConstants.TINKER_ENABLE_ALL,//ApplicationLike的实现类,只能传递字符串,不能使用class.getName()"com.boohee.one.MyApplication",//加载Tinker的主类名,对于特殊需求可能需要使用自己的加载类。需要注意的是://这个类以及它使用的类都是不能被补丁修改的,并且我们需要将它们加到dex.loader[]中。//一般来说,我们使用默认即可。"com.tencent.tinker.loader.TinkerLoader",//由于合成过程中我们已经校验了各个文件的Md5,并将它们存放在/data/data/..目录中。// 默认每次加载时我们并不会去校验tinker文件的Md5,但是你也可通过开启loadVerifyFlag强制每次加载时校验,// 但是这会带来一定的时间损耗。false);}}

其中的几个参数做了详细说明,Tinker其实提供了注解的方式生成该类,但是我们为了更清楚的了解Tinker的原理,所以并没有使用注解。

然后在AndroidManifest.xml中声明该类为application


...<applicationandroid:name=".tinker.OneApplicationForTinker"...</application>

那我们就知道了,app的入口Application就是该类,该类继承自TinkerApplication。然后我们项目中的MyApplication继承自ApplicationLike,其实看到这里,就大概猜到了OneApplicationForTinker可能是一个代理,App中的Application的真正实现还是MyApplication。

Application的替换

为了做分析前的铺垫,我们从最开始的接入入手,实现了OneApplicationForTinker,继承自TinkerApplication。我们继续往下看。
看下TinkerApplication的实现


public abstract class TinkerApplication extends Application {...protected TinkerApplication(int tinkerFlags, String delegateClassName,String loaderClassName, boolean tinkerLoadVerifyFlag) {this.tinkerFlags = tinkerFlags;this.delegateClassName = delegateClassName;this.loaderClassName = loaderClassName;this.tinkerLoadVerifyFlag = tinkerLoadVerifyFlag;}private Object createDelegate() {try {// Use reflection to create the delegate so it doesn't need to go into the primary dex.// And we can also patch itClass<?> delegateClass = Class.forName(delegateClassName, false, getClassLoader());Constructor<?> constructor = delegateClass.getConstructor(Application.class, int.class, boolean.class, long.class, long.class,Intent.class, Resources[].class, ClassLoader[].class, AssetManager[].class);return constructor.newInstance(this, tinkerFlags, tinkerLoadVerifyFlag,applicationStartElapsedTime, applicationStartMillisTime,tinkerResultIntent, resources, classLoader, assetManager);} catch (Throwable e) {throw new TinkerRuntimeException("createDelegate failed", e);}}private synchronized void ensureDelegate() {if (delegate == null) {delegate = createDelegate();}}/*** 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();try {Method method = ShareReflectUtil.findMethod(delegate, "onBaseContextAttached", Context.class);method.invoke(delegate, base);} catch (Throwable t) {throw new TinkerRuntimeException("onBaseContextAttached method not found", t);}//重置安全模式次数,大于等于三次会进入安全模式不再加载if (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();}}@Overrideprotected final void attachBaseContext(Context base) {super.attachBaseContext(base);onBaseContextAttached(base);}private void delegateMethod(String methodName) {if (delegate != null) {try {Method method = ShareReflectUtil.findMethod(delegate, methodName, new Class[0]);method.invoke(delegate, new Object[0]);} catch (Throwable t) {throw new TinkerRuntimeException(String.format("%s method not found", methodName), t);}}}@Overridepublic final void onCreate() {super.onCreate();ensureDelegate();delegateMethod("onCreate");}}

TinkerApplication继承自Application,说明它是正经的Application,而且在manifest文件中声明的也必须是它。然后在Application的各个声明周期方法中反射调用delegate同步Application的周期方法回调,其中的delegate是我们传过来的我们项目中的Application MyApplication

其中的loaderTinker()方法是Tinker的加载流程,我们稍后会讲到,在反射调用MyApplication的attachBaseContext之前,loaderTinker()已经被调用完成,也就是说,Tinker是在加载完整个流程之后才去调用的app中的Application的attachBaseContext开始真正的整个App的生命周期。说白了就是采用了代理。

看到这里,如果你看过我之前写的从Instant run谈Android替换Application和动态加载机制,就会发现跟这个好像。区别在于,InstantRun是在编译器修改manifest插入IncrementalClassLoader,运行时动态替换成项目中实际使用的MyApplication,进而替换了ClassLoader和资源等,开发者在毫不知情的情况下就完成了替换。

其中大量使用了反射,hook系统api,替换运行时系统中保有的Application的引用,最终完成替换,Tinker团队之前做过测试,100w人会有几十个在替换的时候出现问题,而且如果反射替换Application的问题,那么这个过程是不可逆的。Tinker为了兼容性问题考虑,采用了工程代理的方式,避免进入兼容性的坑。虽然可以用注解的方式生成,但是这种方式相比InstantRun的那一套接入成本还是增大不少,不过为了线上的稳定,这一切都是值得的。

还有一点需要注意的是,TinkerApplication是采用反射调用的MyApplication,为什么一定是反射,我们直接传过去MyApplication的引用直接调用不就好了吗?关于这一点,我们后面会详细说明。

补丁加载

在补丁加载之前,我们需要知道补丁文件现在已经下发到app中,并且通过dexDiff合成并且校验然后push到/data/data/package_name/tinker/下。大概的文件目录:


root@android:/data/data/tinker.sample.android/tinker # lsinfo.lockpatch-bc7c9396patch.inforoot@android:/data/data/tinker.sample.android/tinker/patch-bc7c9396 # lsdexodexpatch-bc7c9396.apkres

刚才讲到loadTinker()方法是实现Tinker加载补丁的关键,我们继续看下实现


private void loadTinker() {//disable tinker, not need to installif (tinkerFlags == TINKER_DISABLE) {return;}tinkerResultIntent = new Intent();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, int.class, boolean.class);Constructor<?> constructor = tinkerLoadClass.getConstructor();tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this, tinkerFlags, tinkerLoadVerifyFlag);} catch (Throwable e) {//has exception, put exception error codeShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);}}

其中的loaderClassName是我们传过来的"com.tencent.tinker.loader.TinkerLoader",反射调用TinkerLoader的tryLoad()方法拿到加载补丁结果,这里为什么也要用反射,是因为Tinker做了很多扩展性的工作,TinkerLoader只是默认实现,开发者完全可以自己定义加载器完成加载流程。


//TinkerLoader/*** only main process can handle patch version change or incomplete*/@Overridepublic 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()方法,然后计算消耗时间。


private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {......//tinker/patch.infoFile patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);//old = 641e634c5b8f1649c75caf73794acbdf//new = 2c150d8560334966952678930ba67fa8File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);String oldVersion = patchInfo.oldVersion;String newVersion = patchInfo.newVersion;resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);boolean mainProcess = ShareTinkerInternals.isInMainProcess(app);boolean versionChanged = !(oldVersion.equals(newVersion));String version = oldVersion;if (versionChanged && mainProcess) {version = newVersion;}//patch-641e634cString patchName = SharePatchFileUtil.getPatchVersionDirectory(version);//tinker/patch.info/patch-641e634cString patchVersionDirectory = patchDirectoryPath + "/" + patchName;File patchVersionDirectoryFile = new File(patchVersionDirectory);//tinker/patch.info/patch-641e634c/patch-641e634c.apkFile patchVersionFile = new File(patchVersionDirectoryFile.getAbsolutePath(), SharePatchFileUtil.getPatchVersionFile(version));ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);//校验签名和tinkerIdint returnCode = ShareTinkerInternals.checkSignatureAndTinkerID(app, patchVersionFile, securityCheck);resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);if (isEnabledForDex) {//tinker/patch.info/patch-641e634c/dexboolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);if (!dexCheck) {//file not found, do not load patchreturn;}}final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);if (isEnabledForNativeLib) {//tinker/patch.info/patch-641e634c/libboolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);if (!libCheck) {//file not found, do not load patchreturn;}}//check resourcefinal boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);Log.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);if (isEnabledForResource) {boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);if (!resourceCheck) {//file not found, do not load patchreturn;}}//we should first try rewrite patch info file, if there is a error, we can't load jarif (mainProcess && versionChanged) {patchInfo.oldVersion = version;//update old version to new...}//是否已经进入安全模式if (!checkSafeModeCount(app)) {...return;}//now we can load patch jarif (isEnabledForDex) {boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent);}//now we can load patch resourceif (isEnabledForResource) {boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent);}//all is ok!ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);Log.i(TAG, "tryLoadPatchFiles: load end, ok!");return;}

贴的代码省略了好多判空操作,会判断补丁是否存在,检查补丁信息中的数据是否有效,校验补丁签名以及tinkerId与基准包是否一致。在校验签名时,为了加速校验速度,Tinker只校验 *_meta.txt文件,然后再根据meta文件中的md5校验其他文件。
其中,meta文件有以下几种:

  • package_meta.txt 补丁包的基本信息

  • dex_meta.txt 补丁包中dex文件的信息

  • so_meta.txt 补丁包中so文件的信息

  • res_meta.txt 补丁包中资源文件的信息

然后根据开发者配置的Tinker可补丁类型判断是否可以加载dex,res,so。然后分别分发给TinkerDexLoader、TinkerSoLoader、TinkerResourceLoader分别进行校验是否符合加载条件进而进行加载。

加载补丁dex

在开始讲load dex之前,先说下Tinker的补丁方案,Tinker采用的是下发差分包,然后在手机端合成全量的dex文件进行加载。而在build.gradle配置中的tinkerPatch


dex.loader = ["com.tencent.tinker.loader.*","tinker.sample.android.app.SampleApplication","tinker.sample.android.app.BaseBuildInfo"]

这个配置中的类不会出现在任何全量补丁dex里,也就是说在合成后,这些类还在老的dex文件中,比如在补丁前dex顺序是这样的:oldDex1 -> oldDex2 -> oldDex3..,那么假如修改了dex1中的文件,那么补丁顺序是这样的newDex1 -> oldDex1 -> oldDex2...其中合成后的newDex1中的类是oldDex1中除了dex.loader中标明的类之外的所有类,dex.loader中的类依然在oldDex1中。

由于Tinker的方案是基于Multidex实现的修改dexElements的顺序实现的,所以最终还是要修改classLoder中dexPathList中dexElements的顺序。Android中有两种ClassLoader用于加载dex文件,BootClassLoader、PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader


public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) {super(parent);this.originalPath = dexPath;this.pathList =new DexPathList(this, dexPath, libraryPath, optimizedDirectory);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Class clazz = pathList.findClass(name);if (clazz == null) {throw new ClassNotFoundException(name);}return clazz;}//DexPathListpublic Class findClass(String name) {for (Element element : dexElements) {DexFile dex = element.dexFile;if (dex != null) {Class clazz = dex.loadClassBinaryName(name, definingContext);if (clazz != null) {return clazz;}}}return null;}

最终在DexPathList的findClass中遍历dexElements,谁在前面用谁。而这个dexElements是在方法makeDexElements中生成的,我们的目的就是hook这个方法把dex插入到dexElements的前面。


Android热补丁之Tinker原理解析 下

http://www.apkbus.com/blog-822721-76879.html

原文链接:http://www.apkbus.com/blog-822721-76877.html


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消