查看原文
其他

Android 热修复 Tinker Gradle Plugin解析

鸿洋 鸿洋 2019-04-05
1
概述


前面写了两篇分析了tinker的loader部分源码以及dex diff/patch算法相关解析,那么为了保证完整性,最后一篇主要写tinker-patch-gradle-plugin相关了。


https://github.com/Tencent/tinker/tree/master/tinker-build/tinker-patch-gradle-plugin


(距离看的时候已经快两个月了,再不写就忘了,赶紧记录下来)

注意:本文基于1.7.7


前两篇文章分别为:

有兴趣的可以查看~


在介绍细节之前,我们可以先考虑下:通过一个命令生成一个patch文件,这个文件可以用于下发做热修复(可修复常规代码、资源等),那么第一反应是什么呢?


正常思维,需要设置oldApk,然后我这边build生成newApk,两者需要做diff,找出不同的代码、资源,通过特定的算法将diff出来的数据打成patch文件。


ok,的确是这样的,但是上述这个过程有什么需要注意的么?


  1. 我们在新增资源的时候,可能会因为我们新增的一个资源,导致非常多的资源id发生变化,如果这样直接进行diff,可能会导致资源错乱等(id指向了错误的图片)问题。所以应当保证,当资源改变或者新增、删除资源时,早已存在的资源的id不会发生变化。

  2. 我们在上线app的时候,会做代码混淆,如果没有做特殊的设置,每次混淆后的代码难以保证规则一致;所以,build过程中理论上需要设置混淆的mapping文件。

  3. 当项目比较大的时候,我们可能会遇到方法数超过65535的问题,我们很多时候会通过分包解决,这样就有主dex和其他dex的概念。集成了tinker之后,在应用的Application启动时会非常早的就去做tinker的load操作,所以就决定了load相关的类必须在主dex中。

  4. 在接入一些库的时候,往往还需要配置混淆,比如第三方库中哪些东西不能被混淆等(当然强制某些类在主dex中,也可能需要配置相对应的混淆规则)。


如果大家尝试过接入tinker并使用gradle的方式生成patch相关,会发现在需要在项目的build.gradle中,添加一些配置,这些配置中,会要求我们配置oldApk路径,资源的R.txt路径,混淆mapping文件路径、还有一些比较tinker相关的比较细致的配置信息等。


不过并没有要求我们显示去处理上述几个问题(并没有让你去keep混淆规则,主dex分包规则,以及apply mapping文件),所以上述的几个实际上都是tinker的gradle plugin 帮我们做了。


所以,本文将会以这些问题为线索来带大家走一圈plugin的代码(当然实际上tinker gradle plugin所做的事情远不止上述)。


其次,tinker gradle plugin也是非常好的gradle的学习资料~


2

寻找查看代码入口


下载tinker的代码,导入后,plugin的代码都在tinker-patch-gradle-plugin中,不过当然不能抱着代码一行一行去啃了,应该有个明确的入口,有条理的去阅读这些代码。


那么这个入口是什么呢?


其实很简单,我们在打patch的时候,需要执行tinkerPatchDebug(注:本篇博客基于debug模式讲解)。


当执行完后,将会看到执行过程包含以下流程:


:app:processDebugManifest :app:tinkerProcessDebugManifest(tinker) :app:tinkerProcessDebugResourceId (tinker) :app:processDebugResources :app:tinkerProguardConfigTask(tinker) :app:transformClassesAndResourcesWithProguard :app:tinkerProcessDebugMultidexKeep (tinker) :app:transformClassesWidthMultidexlistForDebug :app:assembleDebug :app:tinkerPatchDebug(tinker)


注:包含(tinker)的都是tinker plugin 所添加的task


可以看到部分task加入到了build的流程中,那么这些task是如何加入到build过程中的呢?


在我们接入tinker之后,build.gradle中有如下代码:


if (buildWithTinker()) {    apply plugin: 'com.tencent.tinker.patch'    tinkerPatch {} // 各种参数 }


如果开启了tinker,会apply一个plugincom.tencent.tinker.patch:



名称实际上就是properties文件的名字,该文件会对应具体的插件类。


对于gradle plugin不了解的,可以参考http://www.cnblogs.com/davenkin/p/gradle-learning-10.html,后面写会抽空单独写一篇详细讲gradle的文章。


下面看TinkerPatchPlugin,在apply方法中,里面大致有类似的代码:



可以看到它通过gradle Project API创建了5个task,通过dependsOn,mustRunAfter插入到了原本的流程中。


例如:


TinkerManifestTask manifestTask = ... manifestTask.mustRunAfter variantOutput.processManifest variantOutput.processResources.dependsOn manifestTask


TinkerManifestTask必须在processManifest之后执行,processResources在manifestTask后执行。


所以流程变为:

processManifest-> manifestTask-> processResources

其他同理。


ok,大致了解了这些task是如何注入的之后,接下来就看看每个task的具体作用吧。


注:如果我们有需求在build过程中搞事,可以参考上述task编写以及依赖方式的设置。


3

每个Task的具体行为


我们按照上述的流程来看,依次为:


TinkerManifestTask TinkerResourceIdTask TinkerProguardConfigTask TinkerMultidexConfigTask TinkerPatchSchemaTask


丢个图,对应下:


4

TinkerManifestTask



这里主要做了两件事:

  • writeManifestMeta主要就是解析AndroidManifest.xml,在<application>内部添加一个meta标签,value为tinkerValue。

例如:


<meta-data    android:name="TINKER_ID"    android:value="tinker_id_com.zhy.abc" />


这里不详细展开了,话说groovy解析XML真方便。


  • addApplicationToLoaderPattern主要是记录自己的application类名和tinker相关的一些load class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader中。


最后copy修改后的AndroidManifest.xml至build/intermediates/tinker_intermediates/AndroidManifest.xml。


这里我们需要想一下,在文初的分析中,并没有想到需要tinkerId这个东西,那么它到底是干嘛的呢?


看一下微信提供的参数说明,就明白了:


在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。


想一下,在非强制升级的情况下,线上一般分布着各个版本的app。但是。你打patch肯定是对应某个版本,所以你要保证这个patch下发下去只影响对应的版本,不会对其他版本造成影响,所以你需要tinkerId与具体的版本相对应。


ok,下一个TinkerResourceIdTask。


5

TinkerResourceIdTask


文初提到,打patch的过程实际上要控制已有的资源id不能发生变化,这个task所做的事就是为此。


如果保证已有资源的id保持不变呢?


实际上需要public.xml和ids.xml的参与,即预先在public.xml中的如下定义,在第二次打包之后可保持该资源对应的id值不变。


注:对xml文件的名称应该没有强要求。


<public type="id" name="search_button" id="0x7f0c0046" />


很多时候我们在搜索固化资源,一般都能看到通过public.xml去固化资源id,但是这里有个ids.xml是干嘛的呢?


下面这篇文章有个很好的解释~


http://blog.csdn.net/sbsujjbcy/article/details/52541803
首先需要生成public.xml,public.xml的生成通过aapt编译时添加-P参数生成。相关代码通过gradle插件去hook Task无缝加入该参数,有一点需要注意,通过appt生成的public.xml并不是可以直接用的,该文件中存在id类型的资源,生成patch时应用进去编译的时候会报resource is not defined,解决方法是将id类型型的资源单独记录到ids.xml文件中,相当于一个声明过程,编译的时候和public.xml一样,将ids.xml也参与编译即可。


ok,知道了public.xml和ids.xml的作用之后,需要再思考一下如何保证id不变?


首先我们在配置old apk的时候,会配置tinkerApplyResourcePath参数,该参数对应一个R.txt,里面的内容涵盖了所有old apk中资源对应的int值。


那么我们可以这么做,根据这个R.txt,把里面的数据写成public.xml不就能保证原本的资源对应的int值不变了么。


的确是这样的,不过tinker做了更多,不仅将old apk的中的资源信息写到public.xml,而且还干涉了新的资源,对新的资源按照资源id的生成规则,也分配的对应的int值,写到了public.xml,可以说该task包办了资源id的生成。


分析前的总结


好了,由于代码非常长,这里就直接写个总结了~~如果想要看代码的可以点击阅读原文(最好在PC上)继续学习。


首先将设置的old R.txt读取到内存中,转为:


  • 一个Map,key-value都代表一个具体资源信息;直接复用,不会生成新的资源信息。

  • 一个Map,key为资源类型,value为该类资源当前的最大int值;参与新的资源id的生成。


接下来遍历当前app中的资源,资源分为:


  • values文件夹下文件


对所有values相关文件夹下的文件已经处理完毕,大致的处理为:遍历文件中的节点,大致有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这些节点,将所有的节点按类型分类存储到rTypeResourceMap(key为资源类型,value为对应类型资源集合Set)中。


其中declare-styleable这个标签,主要读取其内部的attr标签,对attr标签对应的资源按上述处理。


  • res下非values文件夹


打开自己的项目有看一眼,除了values相关还有layout,anim,color等文件夹,主要分为两类:


一类是对 文件 即为资源,例如R.layout.xxx,R.drawable.xxx等;另一类为xml文档中以@+(去除@+android:id),其实就是找到我们自定义id节点,然后截取该节点的id值部分作为属性的名称(例如:@+id/tv,tv即为属性的名称)。


如果和设置的old apk中文件中相同name和type的节点不需要特殊处理,直接复用即可;如果不存在则需要生成新的typeId、resourceId等信息。


会将所有生成的资源都存到rTypeResourceMap中,最后写文件。

这样就基本收集到了所有的需要生成资源信息的所有的资源,最后写到public.xml即可。


总结性的语言难免有一些疏漏,实际以源码分析为标准。


6

TinkerProguardConfigTask


还记得文初说:


  1. 我们在上线app的时候,会做代码混淆,如果没有做特殊的设置,每次混淆后的代码差别应该非常巨大;所以,build过程中理论上需要设置混淆的mapping文件。

  2. 在接入一些库的时候,往往还需要配置混淆,比如第三方库中哪些东西不能被混淆等(当然强制某些类在主dex中,也可能需要配置相对应的混淆规则)。


这个task的作用很明显了。有时候为了确保一些类在main dex中,简单的做法也会对其在混淆配置中进行keep(避免由于混淆造成类名更改,而使main dex的keep失效)。


如果开启了proguard会执行该task。


这个就是主要去设置混淆的mapping文件,和keep一些必要的类了。



读取我们设置的mappingFile,设置


-applymapping applyMappingFile


然后设置一些默认需要keep的规则:



最后是keep住我们的application、com.tencent.tinker.loader.**以及我们设置的相关类。


TinkerManifestTask中:addApplicationToLoaderPattern主要是记录自己的application类名和tinker相关的一些load class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader。


7

TinkerMultidexConfigTask


对应文初:


当项目比较大的时候,我们可能会遇到方法数超过65535的问题,我们很多时候会通过分包解决,这样就有主dex和其他dex的概念。集成了tinker之后,在应用的Application启动时会非常早的就去做tinker的load操作,所以就决定了load相关的类必须在主dex中。


如果multiDexEnabled开启。


主要是让相关类必须在main dex。



相关类都在loader这个集合中,在TinkerManifestTask中设置的。


8

TinkerPatchSchemaTask


主要执行Runner.tinkerPatch



主要分为以下环节:


  • 生成patch

  • 生成meta-file和version-file,这里主要就是在assets目录下写一些键值对。(包含tinkerId以及配置中configField相关信息)

  • build patch


顾名思义就是两个apk比较去生成各类patch文件,那么从一个apk的组成来看,大致可以分为:


  • dex文件比对的patch文件

  • res文件比对的patch res文件

  • so文件比对生成的so patch文件


以及copy了一个test.dex文件至目标文件夹,该文件存储在tinker-patch-lib的resources文件夹下,主要用于在app上进行测试。


最后将生成的patch文件,以及相关的配置文件一起生成patch文件,以及压缩、签名等。


详细的源码分析可以阅读原文


总结


一直关注tinker的更新,也在项目中对tinker进行了使用与定制,tinker中包含了大量的可学习的知识,项目本身在也具有非常强的价值。


对于tinker的“技术的初心与坚持”一文感触颇深,希望tinker越来越好~


由于原文太长,几乎全部都是代码分析,实在不适合手机端阅读,所以特意写了个简版用于微信端阅读,大家可以通过微信端做一个简单的了解,需要详细学习时,查看博客原文即可。


如果你有想学习的文章直接留言,我会整理征稿。如果你有好的文章想和大家分享欢迎投稿,直接向我投递文章链接即可。


欢迎长按下图->识别图中二维码或者扫一扫关注我的公众号:

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存