http://blog.csdn.net/lmj623565791/article/details/72667669

来源:互联网 发布:网络咨询医生话术 编辑:程序博客网 时间:2024/06/11 12:20

本文已在我的公众号hongyangAndroid原创首发。 
转载请标明出处: 
http://blog.csdn.net/lmj623565791/article/details/72667669 
本文出自张鸿洋的博客

一、概述

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

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

注意:

本文基于1.7.7

前两篇文章分别为:

  • Android 热修复 Tinker接入及源码浅析
  • Android 热修复 Tinker 源码分析之DexDiff / DexPatch

有兴趣的可以查看~

在介绍细节之前,我们可以先考虑下:通过一个命令生成一个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的学习资料~

二、寻找查看代码入口

下载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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

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

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

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

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

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

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

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

// ... 省略了一堆代码TinkerPatchSchemaTask tinkerPatchBuildTask         = project.tasks.create("tinkerPatch${variantName}", TinkerPatchSchemaTask)tinkerPatchBuildTask.dependsOn variant.assembleTinkerManifestTask manifestTask         = project.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask)manifestTask.mustRunAfter variantOutput.processManifestvariantOutput.processResources.dependsOn manifestTaskTinkerResourceIdTask applyResourceTask         = project.tasks.create("tinkerProcess${variantName}ResourceId", TinkerResourceIdTask)applyResourceTask.mustRunAfter manifestTaskvariantOutput.processResources.dependsOn applyResourceTaskif (proguardEnable) {    TinkerProguardConfigTask proguardConfigTask             = project.tasks.create("tinkerProcess${variantName}Proguard", TinkerProguardConfigTask)    proguardConfigTask.mustRunAfter manifestTask    def proguardTask = getProguardTask(project, variantName)    if (proguardTask != null) {        proguardTask.dependsOn proguardConfigTask    }}if (multiDexEnabled) {    TinkerMultidexConfigTask multidexConfigTask             = project.tasks.create("tinkerProcess${variantName}MultidexKeep", TinkerMultidexConfigTask)    multidexConfigTask.mustRunAfter manifestTask    def multidexTask = getMultiDexTask(project, variantName)    if (multidexTask != null) {        multidexTask.dependsOn multidexConfigTask    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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

例如:

TinkerManifestTask manifestTask = ...manifestTask.mustRunAfter variantOutput.processManifestvariantOutput.processResources.dependsOn manifestTask
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

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

所以流程变为:

processManifest-> manifestTask-> processResources
  • 1
  • 1

其他同理。

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

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

三、每个Task的具体行为

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

TinkerManifestTaskTinkerResourceIdTaskTinkerProguardConfigTaskTinkerMultidexConfigTaskTinkerPatchSchemaTask
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

丢个图,对应下:

四、TinkerManifestTask

#TinkerManifestTask@TaskActiondef updateManifest() {    // Parse the AndroidManifest.xml    String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId    tinkerValue = TINKER_ID_PREFIX + tinkerValue;//"tinker_id_"    // /build/intermediates/manifests/full/debug/AndroidManifest.xml    writeManifestMeta(manifestPath, TINKER_ID, tinkerValue)    addApplicationToLoaderPattern()    File manifestFile = new File(manifestPath)    if (manifestFile.exists()) {        FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这里主要做了两件事:

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

    例如:

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

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

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

最后copy修改后的AndroidManifest.xmlbuild/intermediates/tinker_intermediates/AndroidManifest.xml

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

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

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

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

ok,下一个TinkerResourceIdTask。

五、TinkerResourceIdTask

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

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

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

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

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

很多时候我们在搜索固化资源,一般都能看到通过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的生成。

分析前的总结

好了,由于代码非常长,我决定在这个地方先用总结性的语言总结下,如果没有耐心看代码的可以直接跳过源码分析阶段:

首先将设置的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即可。

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

开始源码分析

@TaskActiondef applyResourceId() {     // 资源mapping文件    String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping    // resDir /build/intermediates/res/merged/debug    String idsXml = resDir + "/values/ids.xml";    String publicXml = resDir + "/values/public.xml";    FileOperation.deleteFile(idsXml);    FileOperation.deleteFile(publicXml);    List<String> resourceDirectoryList = new ArrayList<String>();    // /build/intermediates/res/merged/debug    resourceDirectoryList.add(resDir);    project.logger.error("we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}");    project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true;    // 收集所有的资源,以type->type,name,id,int/int[]存储    Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile);    AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);    PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);    File publicFile = new File(publicXml);    if (publicFile.exists()) {        FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));        project.logger.error("tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}");    }    File idxFile = new File(idsXml);    if (idxFile.exists()) {        FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));        project.logger.error("tinker gen resource idx.xml in ${RESOURCE_IDX_XML}");    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

大体浏览下代码,可以看到首先检测是否设置了resource mapping文件,如果没有设置会直接跳过。并且最后的产物是public.xmlids.xml

因为生成patch时,需要保证两次打包已经存在的资源的id一致,需要public.xmlids.xml的参与。

首先清理已经存在的public.xmlids.xml,然后通过PatchUtil.readRTxt读取resourceMappingFile(参数中设置的),该文件记录的格式如下:

int anim abc_slide_in_bottom 0x7f050006int id useLogo 0x7f0b0012int[] styleable AppCompatImageView { 0x01010119, 0x7f010027 }int styleable AppCompatImageView_android_src 0int styleable AppCompatImageView_srcCompat 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

大概有两类,一类是int型各种资源;一类是int[]数组,代表styleable,其后面紧跟着它的item(熟悉自定义View的一定不陌生)。

PatchUtil.readRTxt的代码就不贴了,简单描述下:

首先正则按行匹配,每行分为四部分,即idType,rType,name,idValue(四个属性为RDotTxtEntry的成员变量)。

  • idType有两种INTINT_ARRAY
  • rType包含各种资源:

ANIM, ANIMATOR, ARRAY, ATTR, BOOL, COLOR, DIMEN, DRAWABLE, FRACTION, 
ID, INTEGER, INTERPOLATOR, LAYOUT, MENU, MIPMAP, PLURALS, RAW, 
STRING, STYLE, STYLEABLE, TRANSITION, XML

http://developer.android.com/reference/android/R.html

name和value就是普通的键值对了。

这里并没有对styleable做特殊处理。

最后按rType分类,存在一个Map中,即key为rType,value为一个RDotTxtEntry类型的Set集合。

回顾下剩下的代码:

//...省略前半部分     AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);    PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);    File publicFile = new File(publicXml);    if (publicFile.exists()) {        FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));        project.logger.error("tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}");    }    File idxFile = new File(idsXml);    if (idxFile.exists()) {        FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));        project.logger.error("tinker gen resource idx.xml in ${RESOURCE_IDX_XML}");    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

那么到了AaptUtil.collectResource方法,传入了resDir目录和我们刚才收集了资源信息的Map,返回了一个AaptResourceCollector对象,看名称是对aapt相关的资源的收集:

看代码:

public static AaptResourceCollector collectResource(List<String> resourceDirectoryList,                                                    Map<RType, Set<RDotTxtEntry>> rTypeResourceMap) {    AaptResourceCollector resourceCollector = new AaptResourceCollector(rTypeResourceMap);    List<com.tencent.tinker.build.aapt.RDotTxtEntry> references = new ArrayList<com.tencent.tinker.build.aapt.RDotTxtEntry>();    for (String resourceDirectory : resourceDirectoryList) {        try {            collectResources(resourceDirectory, resourceCollector);        } catch (Exception e) {            throw new RuntimeException(e);        }    }    for (String resourceDirectory : resourceDirectoryList) {        try {            processXmlFilesForIds(resourceDirectory, references, resourceCollector);        } catch (Exception e) {            throw new RuntimeException(e);        }    }    return resourceCollector;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

首先初始化了一个AaptResourceCollector对象,看其构造方法:

public AaptResourceCollector(Map<RType, Set<RDotTxtEntry>> rTypeResourceMap) {    this();    if (rTypeResourceMap != null) {        Iterator<Entry<RType, Set<RDotTxtEntry>>> iterator = rTypeResourceMap.entrySet().iterator();        while (iterator.hasNext()) {            Entry<RType, Set<RDotTxtEntry>> entry = iterator.next();            RType rType = entry.getKey();            Set<RDotTxtEntry> set = entry.getValue();            for (RDotTxtEntry rDotTxtEntry : set) {                originalResourceMap.put(rDotTxtEntry, rDotTxtEntry);                ResourceIdEnumerator resourceIdEnumerator = null;                    // ARRAY主要是styleable                if (!rDotTxtEntry.idType.equals(IdType.INT_ARRAY)) {                        // 获得resourceId                    int resourceId = Integer.decode(rDotTxtEntry.idValue.trim()).intValue();                    // 获得typeId                    int typeId = ((resourceId & 0x00FF0000) / 0x00010000);                    if (typeId >= currentTypeId) {                        currentTypeId = typeId + 1;                    }                        // type -> id的映射                    if (this.rTypeEnumeratorMap.containsKey(rType)) {                        resourceIdEnumerator = this.rTypeEnumeratorMap.get(rType);                        if (resourceIdEnumerator.currentId < resourceId) {                            resourceIdEnumerator.currentId = resourceId;                        }                    } else {                        resourceIdEnumerator = new ResourceIdEnumerator();                        resourceIdEnumerator.currentId = resourceId;                        this.rTypeEnumeratorMap.put(rType, resourceIdEnumerator);                    }                }            }        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

对rTypeResourceMap根据rType进行遍历,读取每个rType对应的Set集合;然后遍历每个rDotTxtEntry:

  1. 加入到originalResourceMap,key和value都是rDotTxtEntry对象
  2. 如果是int型资源,首先读取其typeId,并持续更新currentTypeId(保证其为遍历完成后的最大值+1)
  3. 初始化rTypeEnumeratorMap,key为rType,value为ResourceIdEnumerator,且ResourceIdEnumerator中的currentId保存着目前同类资源的最大的resouceId,也就是说rTypeEnumeratorMap中存储了各个rType对应的最大的资源Id。

结束完成构造方法,执行了

  1. 遍历了resourceDirectoryList,目前其中只有一个resDir,然后执行了collectResources方法;
  2. 遍历了resourceDirectoryList,执行了processXmlFilesForIds

分别读代码了:

collectResources

private static void collectResources(String resourceDirectory, AaptResourceCollector resourceCollector) throws Exception {    File resourceDirectoryFile = new File(resourceDirectory);    File[] fileArray = resourceDirectoryFile.listFiles();    if (fileArray != null) {        for (File file : fileArray) {            if (file.isDirectory()) {                String directoryName = file.getName();                if (directoryName.startsWith("values")) {                    if (!isAValuesDirectory(directoryName)) {                        throw new AaptUtilException("'" + directoryName + "' is not a valid values directory.");                    }                    processValues(file.getAbsolutePath(), resourceCollector);                } else {                    processFileNamesInDirectory(file.getAbsolutePath(), resourceCollector);                }            }        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

遍历我们的resDir中的所有文件夹

  • 如果是values相关文件夹,执行processValues
  • 非values相关文件夹则执行processFileNamesInDirectory

processValues处理values相关文件,会遍历每一个合法的values相关文件夹下的文件,执行processValuesFile(file.getAbsolutePath(), resourceCollector);

public static void processValuesFile(String valuesFullFilename,                                     AaptResourceCollector resourceCollector) throws Exception {    Document document = JavaXmlUtil.parse(valuesFullFilename);    String directoryName = new File(valuesFullFilename).getParentFile().getName();    Element root = document.getDocumentElement();    for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {        if (node.getNodeType() != Node.ELEMENT_NODE) {            continue;        }        String resourceType = node.getNodeName();        if (resourceType.equals(ITEM_TAG)) {            resourceType = node.getAttributes().getNamedItem("type").getNodeValue();            if (resourceType.equals("id")) {                resourceCollector.addIgnoreId(node.getAttributes().getNamedItem("name").getNodeValue());            }        }        if (IGNORED_TAGS.contains(resourceType)) {            continue;        }        if (!RESOURCE_TYPES.containsKey(resourceType)) {            throw new AaptUtilException("Invalid resource type '<" + resourceType + ">' in '" + valuesFullFilename + "'.");        }        RType rType = RESOURCE_TYPES.get(resourceType);        String resourceValue = null;        switch (rType) {            case STRING:            case COLOR:            case DIMEN:            case DRAWABLE:            case BOOL:            case INTEGER:                resourceValue = node.getTextContent().trim();                break;            case ARRAY://has sub item            case PLURALS://has sub item            case STYLE://has sub item            case STYLEABLE://has sub item                resourceValue = subNodeToString(node);                break;            case FRACTION://no sub item                resourceValue = nodeToString(node, true);                break;            case ATTR://no sub item                resourceValue = nodeToString(node, true);                break;        }        try {            addToResourceCollector(resourceCollector,                    new ResourceDirectory(directoryName, valuesFullFilename),                    node, rType, resourceValue);        } catch (Exception e) {            throw new AaptUtilException(e.getMessage() + ",Process file error:" + valuesFullFilename, e);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

values下相关的文件基本都是xml咯,所以遍历xml文件,遍历其内部的节点,(values的xml文件其内部一般为item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction等),每种类型的节点对应一个rType,根据不同类型的节点也会去获取节点的值,确定一个都会执行:

addToResourceCollector(resourceCollector,    new ResourceDirectory(directoryName, valuesFullFilename),    node, rType, resourceValue);
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

注:除此以外,这里在ignoreIdSet记录了声明的id资源,这些id是已经声明过的,所以最终在编写ids.xml时,可以过滤掉这些id。

下面继续看:addToResourceCollector

源码如下:

private static void addToResourceCollector(AaptResourceCollector resourceCollector,                                           ResourceDirectory resourceDirectory,                                           Node node, RType rType, String resourceValue) {    String resourceName = sanitizeName(rType, resourceCollector, extractNameAttribute(node));    if (rType.equals(RType.STYLEABLE)) {        int count = 0;        for (Node attrNode = node.getFirstChild(); attrNode != null; attrNode = attrNode.getNextSibling()) {            if (attrNode.getNodeType() != Node.ELEMENT_NODE || !attrNode.getNodeName().equals("attr")) {                continue;            }            String rawAttrName = extractNameAttribute(attrNode);            String attrName = sanitizeName(rType, resourceCollector, rawAttrName);            if (!rawAttrName.startsWith("android:")) {                resourceCollector.addIntResourceIfNotPresent(RType.ATTR, attrName);            }        }    } else {        resourceCollector.addIntResourceIfNotPresent(rType, resourceName);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

如果不是styleable的资源,则直接获取resourceName,然后调用resourceCollector.addIntResourceIfNotPresent(rType, resourceName)。

如果是styleable类型的资源,则会遍历找到其内部的attr节点,找出非android:开头的(因为android:开头的attr的id不需要我们去确定),设置rType为ATTR,value为attr属性的name,调用addIntResourceIfNotPresent。

public void addIntResourceIfNotPresent(RType rType, String name) { //, ResourceDirectory resourceDirectory) {    if (!rTypeEnumeratorMap.containsKey(rType)) {        if (rType.equals(RType.ATTR)) {            rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(1));        } else {            rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(currentTypeId++));        }    }    RDotTxtEntry entry = new FakeRDotTxtEntry(IdType.INT, rType, name);    Set<RDotTxtEntry> resourceSet = null;    if (this.rTypeResourceMap.containsKey(rType)) {        resourceSet = this.rTypeResourceMap.get(rType);    } else {        resourceSet = new HashSet<RDotTxtEntry>();        this.rTypeResourceMap.put(rType, resourceSet);    }    if (!resourceSet.contains(entry)) {        String idValue = String.format("0x%08x", rTypeEnumeratorMap.get(rType).next());        addResource(rType, IdType.INT, name, idValue); //, resourceDirectory);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

首先构建一个entry,然后判断当前的rTypeResourceMap中是否存在该资源实体,如果存在,则什么都不用做。

如果不存在,则需要构建一个entry,那么主要是id的构建。

关于id的构建:

还记得rTypeEnumeratorMap么,其内部包含了我们设置的”res mapping”文件,存储了每一类资源(rType)的资源的最大resourceId值。

那么首先判断就是是否已经有这种类型了,如果有的话,获取出该类型当前最大的resourceId,然后+1,最为传入资源的resourceId.

如果不存在当前这种类型,那么如果类型为ATTR则固定type为1;否则的话,新增一个typeId,为当前最大的type+1(currentTypeId中也是记录了目前最大的type值),有了类型就可以通过ResourceIdEnumerator.next()来获取id。

经过上述就可以构造出一个idValue了。

最后调用:

addResource(rType, IdType.INT, name, idValue);
  • 1
  • 1

查看代码:

public void addResource(RType rType, IdType idType, String name, String idValue) {    Set<RDotTxtEntry> resourceSet = null;    if (this.rTypeResourceMap.containsKey(rType)) {        resourceSet = this.rTypeResourceMap.get(rType);    } else {        resourceSet = new HashSet<RDotTxtEntry>();        this.rTypeResourceMap.put(rType, resourceSet);    }    RDotTxtEntry rDotTxtEntry = new RDotTxtEntry(idType, rType, name, idValue);    if (!resourceSet.contains(rDotTxtEntry)) {        if (this.originalResourceMap.containsKey(rDotTxtEntry)) {            this.rTypeEnumeratorMap.get(rType).previous();            rDotTxtEntry = this.originalResourceMap.get(rDotTxtEntry);        }         resourceSet.add(rDotTxtEntry);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

大体意思就是如果该资源不存在就添加到rTypeResourceMap。

首先构建出该资源实体,判断该类型对应的资源集合是否包含该资源实体(这里contains只比对name和type),如果不包含,判断是否在originalResourceMap中,如果存在(这里做了一个previous操作,其实与上面的代码的next操作对应,主要是针对资源存在我们的res map中这种情况)则取出该资源实体,最终将该资源实体加入到rTypeResourceMap中。

ok,到这里需要小节一下,我们刚才对所有values相关文件夹下的文件已经处理完毕,大致的处理为:遍历文件中的节点,大致有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这些节点,将所有的节点按类型分类存储到rTypeResourceMap中(如果和设置的”res map”文件中相同name和type的节点不需要特殊处理,直接复用即可;如果不存在则需要生成新的typeId、resourceId等信息)。

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

处理完成values相关文件夹之后,还需要处理一些res下的其他文件,比如layout、layout、anim等文件夹,该类资源也需要在R中生成对应的id值,这类值也需要固化。

processFileNamesInDirectory

public static void processFileNamesInDirectory(String resourceDirectory,                                               AaptResourceCollector resourceCollector) throws IOException {    File resourceDirectoryFile = new File(resourceDirectory);    String directoryName = resourceDirectoryFile.getName();    int dashIndex = directoryName.indexOf('-');    if (dashIndex != -1) {        directoryName = directoryName.substring(0, dashIndex);    }    if (!RESOURCE_TYPES.containsKey(directoryName)) {        throw new AaptUtilException(resourceDirectoryFile.getAbsolutePath() + " is not a valid resource sub-directory.");    }    File[] fileArray = resourceDirectoryFile.listFiles();    if (fileArray != null) {        for (File file : fileArray) {            if (file.isHidden()) {                continue;            }            String filename = file.getName();            int dotIndex = filename.indexOf('.');            String resourceName = dotIndex != -1 ? filename.substring(0, dotIndex) : filename;            RType rType = RESOURCE_TYPES.get(directoryName);            resourceCollector.addIntResourceIfNotPresent(rType, resourceName);            System.out.println("rType = " + rType + " , resName = " + resourceName);            ResourceDirectory resourceDirectoryBean = new ResourceDirectory(file.getParentFile().getName(), file.getAbsolutePath());            resourceCollector.addRTypeResourceName(rType, resourceName, null, resourceDirectoryBean);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

遍历res下所有文件夹,根据文件夹名称确定其对应的资源类型(例如:drawable-xhpi,则认为其内部的文件类型为drawable类型),然后遍历该文件夹下所有的文件,最终以文件名为资源的name,文件夹确定资源的type,最终调用:

resourceCollector.addIntResourceIfNotPresent(rType, resourceName);
  • 1
  • 2
  • 1
  • 2

processXmlFilesForIds

public static void processXmlFilesForIds(String resourceDirectory,                                         List<RDotTxtEntry> references, AaptResourceCollector resourceCollector) throws Exception {    List<String> xmlFullFilenameList = FileUtil            .findMatchFile(resourceDirectory, Constant.Symbol.DOT + Constant.File.XML);    if (xmlFullFilenameList != null) {        for (String xmlFullFilename : xmlFullFilenameList) {            File xmlFile = new File(xmlFullFilename);            String parentFullFilename = xmlFile.getParent();            File parentFile = new File(parentFullFilename);            if (isAValuesDirectory(parentFile.getName()) || parentFile.getName().startsWith("raw")) {                // Ignore files under values* directories and raw*.                continue;            }            processXmlFile(xmlFullFilename, references, resourceCollector);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

遍历除了raw*以及values*相关文件夹下的xml文件,执行processXmlFile。

public static void processXmlFile(String xmlFullFilename, List<RDotTxtEntry> references, AaptResourceCollector resourceCollector)        throws IOException, XPathExpressionException {    Document document = JavaXmlUtil.parse(xmlFullFilename);    NodeList nodesWithIds = (NodeList) ANDROID_ID_DEFINITION.evaluate(document, XPathConstants.NODESET);    for (int i = 0; i < nodesWithIds.getLength(); i++) {        String resourceName = nodesWithIds.item(i).getNodeValue();        if (!resourceName.startsWith(ID_DEFINITION_PREFIX)) {            throw new AaptUtilException("Invalid definition of a resource: '" + resourceName + "'");        }        resourceCollector.addIntResourceIfNotPresent(RType.ID, resourceName.substring(ID_DEFINITION_PREFIX.length()));    }    // 省略了无关代码}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

主要找xml文档中以@+(去除@+android:id),其实就是找到我们自定义id节点,然后截取该节点的id值部分作为属性的名称(例如:@+id/tv,tv即为属性的名称),最终调用:

resourceCollector    .addIntResourceIfNotPresent(RType.ID,         resourceName.substring(ID_DEFINITION_PREFIX.length()));
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

上述就完成了所有的资源的收集,那么剩下的就是写文件了:

public static void generatePublicResourceXml(AaptResourceCollector aaptResourceCollector,                                             String outputIdsXmlFullFilename,                                             String outputPublicXmlFullFilename) {    if (aaptResourceCollector == null) {        return;    }    FileUtil.createFile(outputIdsXmlFullFilename);    FileUtil.createFile(outputPublicXmlFullFilename);    PrintWriter idsWriter = null;    PrintWriter publicWriter = null;    try {        FileUtil.createFile(outputIdsXmlFullFilename);        FileUtil.createFile(outputPublicXmlFullFilename);        idsWriter = new PrintWriter(new File(outputIdsXmlFullFilename), "UTF-8");        publicWriter = new PrintWriter(new File(outputPublicXmlFullFilename), "UTF-8");        idsWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");        publicWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");        idsWriter.println("<resources>");        publicWriter.println("<resources>");        Map<RType, Set<RDotTxtEntry>> map = aaptResourceCollector.getRTypeResourceMap();        Iterator<Entry<RType, Set<RDotTxtEntry>>> iterator = map.entrySet().iterator();        while (iterator.hasNext()) {            Entry<RType, Set<RDotTxtEntry>> entry = iterator.next();            RType rType = entry.getKey();            if (!rType.equals(RType.STYLEABLE)) {                Set<RDotTxtEntry> set = entry.getValue();                for (RDotTxtEntry rDotTxtEntry : set) {                    String rawName = aaptResourceCollector.getRawName(rType, rDotTxtEntry.name);                    if (StringUtil.isBlank(rawName)) {                        rawName = rDotTxtEntry.name;                    }                    publicWriter.println("<public type=\"" + rType + "\" name=\"" + rawName + "\" id=\"" + rDotTxtEntry.idValue.trim() + "\" />");                          }                Set<String> ignoreIdSet = aaptResourceCollector.getIgnoreIdSet();                for (RDotTxtEntry rDotTxtEntry : set) {                    if (rType.equals(RType.ID) && !ignoreIdSet.contains(rDotTxtEntry.name)) {                        idsWriter.println("<item type=\"" + rType + "\" name=\"" + rDotTxtEntry.name + "\"/>");                    }                 }            }            idsWriter.flush();            publicWriter.flush();        }        idsWriter.println("</resources>");        publicWriter.println("</resources>");    } catch (Exception e) {        throw new PatchUtilException(e);    } finally {        if (idsWriter != null) {            idsWriter.flush();            idsWriter.close();        }        if (publicWriter != null) {            publicWriter.flush();            publicWriter.close();        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

主要就是遍历rTypeResourceMap,然后每个资源实体对应一条public标签记录写到public.xml中。

此外,如果发现该元素节点的type为Id,并且不在ignoreSet中,会写到ids.xml这个文件中。(这里有个ignoreSet,这里ignoreSet中记录了values下所有的<item type=id的资源,是直接在项目中已经声明过的,所以去除)。

六、TinkerProguardConfigTask

还记得文初说:

  1. 我们在上线app的时候,会做代码混淆,如果没有做特殊的设置,每次混淆后的代码差别应该非常巨大;所以,build过程中理论上需要设置混淆的mapping文件。
  2. 在接入一些库的时候,往往还需要配置混淆,比如第三方库中哪些东西不能被混淆等(当然强制某些类在主dex中,也可能需要配置相对应的混淆规则)。

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

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

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

@TaskActiondef updateTinkerProguardConfig() {    def file = project.file(PROGUARD_CONFIG_PATH)    project.logger.error("try update tinker proguard file with ${file}")    // Create the directory if it doesnt exist already    file.getParentFile().mkdirs()    // Write our recommended proguard settings to this file    FileWriter fr = new FileWriter(file.path)    String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping    //write applymapping    if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {        project.logger.error("try add applymapping ${applyMappingFile} to build the package")        fr.write("-applymapping " + applyMappingFile)        fr.write("\n")    } else {        project.logger.error("applymapping file ${applyMappingFile} is illegal, just ignore")    }    fr.write(PROGUARD_CONFIG_SETTINGS)    fr.write("#your dex.loader patterns here\n")    //they will removed when apply    Iterable<String> loader = project.extensions.tinkerPatch.dex.loader    for (String pattern : loader) {        if (pattern.endsWith("*") && !pattern.endsWith("**")) {            pattern += "*"        }        fr.write("-keep class " + pattern)        fr.write("\n")    }    fr.close()    // Add this proguard settings file to the list    applicationVariant.getBuildType().buildType.proguardFiles(file)    def files = applicationVariant.getBuildType().buildType.getProguardFiles()    project.logger.error("now proguard files is ${files}")}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

读取我们设置的mappingFile,设置

-applymapping applyMappingFile
  • 1
  • 1

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

PROGUARD_CONFIG_SETTINGS ="-keepattributes *Annotation* \n" +"-dontwarn com.tencent.tinker.anno.AnnotationProcessor \n" +"-keep @com.tencent.tinker.anno.DefaultLifeCycle public class *\n" +"-keep public class * extends android.app.Application {\n" +"    *;\n" +"}\n" +"\n" +"-keep public class com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +"    *;\n" +"}\n" +"-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +"    *;\n" +"}\n" +"\n" +"-keep public class com.tencent.tinker.loader.TinkerLoader {\n" +"    *;\n" +"}\n" +"-keep public class * extends com.tencent.tinker.loader.TinkerLoader {\n" +"    *;\n" +"}\n" +"-keep public class com.tencent.tinker.loader.TinkerTestDexLoad {\n" +"    *;\n" +"}\n" +"\n"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

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

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

七、TinkerMultidexConfigTask

对应文初:

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

如果multiDexEnabled开启。

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

"-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +    "    *;\n" +    "}\n" +    "\n" +    "-keep public class * extends com.tencent.tinker.loader.TinkerLoader {\n" +    "    *;\n" +    "}\n" +    "\n" +    "-keep public class * extends android.app.Application {\n" +    "    *;\n" +    "}\n"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
Iterable<String> loader = project.extensions.tinkerPatch.dex.loader    for (String pattern : loader) {        if (pattern.endsWith("*")) {            if (!pattern.endsWith("**")) {                pattern += "*"            }        }        lines.append("-keep class " + pattern + " {\n" +                "    *;\n" +                "}\n")                .append("\n")    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

八、TinkerPatchSchemaTask

主要执行Runner.tinkerPatch

protected void tinkerPatch() {    try {        //gen patch        ApkDecoder decoder = new ApkDecoder(config);        decoder.onAllPatchesStart();        decoder.patch(config.mOldApkFile, config.mNewApkFile);        decoder.onAllPatchesEnd();        //gen meta file and version file        PatchInfo info = new PatchInfo(config);        info.gen();        //build patch        PatchBuilder builder = new PatchBuilder(config);        builder.buildPatch();    } catch (Throwable e) {        e.printStackTrace();        goToError();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

主要分为以下环节:

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

(1)生成pacth

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

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

看下代码:

public boolean patch(File oldFile, File newFile) throws Exception {    //check manifest change first    manifestDecoder.patch(oldFile, newFile);    unzipApkFiles(oldFile, newFile);    Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(),            mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));    soPatchDecoder.onAllPatchesEnd();    dexPatchDecoder.onAllPatchesEnd();    manifestDecoder.onAllPatchesEnd();    resPatchDecoder.onAllPatchesEnd();    //clean resources    dexPatchDecoder.clean();    soPatchDecoder.clean();    resPatchDecoder.clean();    return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

代码内部包含四个Decoder:

  • manifestDecoder
  • dexPatchDecoder
  • soPatchDecoder
  • resPatchDecoder

刚才提到需要对dex、so、res文件做diff,但是为啥会有个manifestDecoder。目前tinker并不支持四大组件,也就是说manifest文件中是不允许出现新增组件的。

所以,manifestDecoder的作用实际上是用于检查的:

  1. minSdkVersion<14时仅允许dexMode使用jar模式(TODO:raw模式的区别是什么?)
  2. 会解析manifest文件,读取出组大组件进行对比,不允许出现新增的任何组件。

代码就不贴了非常好理解,关于manifest的解析是基于该库封装的:

https://github.com/clearthesky/apk-parser

然后就是解压两个apk文件了,old apk(我们设置的),old apk 生成的。

解压的目录为:

  • old apk: build/intermediates/outputs/old apk名称/
  • new apk: build/intermediates/outputs/app-debug/

解压完成后,就是单个文件对比了:

对比的思路是,以newApk解压目录下所有的文件为基准,去oldApk中找同名的文件,那么会有以下几个情况:

  1. 在oldApkDir中没有找到,那么说明该文件是新增的
  2. 在oldApkDir中找到了,那么比对md5,如果不同,则认为改变了(则需要根据情况做diff)

有了大致的了解后,可以看代码:

Files.walkFileTree(    mNewApkDir.toPath(),     new ApkFilesVisitor(        config,         mNewApkDir.toPath(),        mOldApkDir.toPath(),         dexPatchDecoder,         soPatchDecoder,         resPatchDecoder));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Files.walkFileTree会以mNewApkDir.toPath()为基准,遍历其内部所有的文件,ApkFilesVisitor中可以对每个遍历的文件进行操作。

重点看ApkFilesVisitor是如何操作每个文件的:

@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {    Path relativePath = newApkPath.relativize(file);    // 在oldApkDir中找到该文件    Path oldPath = oldApkPath.resolve(relativePath);    File oldFile = null;    //is a new file?!    if (oldPath.toFile().exists()) {        oldFile = oldPath.toFile();    }    String patternKey = relativePath.toString().replace("\\", "/");    if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {        dexDecoder.patch(oldFile, file.toFile());    }    if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {        soDecoder.patch(oldFile, file.toFile());    }    if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {         resDecoder.patch(oldFile, file.toFile());    }    return FileVisitResult.CONTINUE;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

首先去除newApkDir中的一个文件,在oldApkDir中寻找同名的apk;然后根据名称判断该文件属于:

  1. dexFile -> dexDecoder.patch 完成dex文件间的比对
  2. soFile -> soDecoder.patch 完成so文件的比对
  3. resFile -> resDecoder.patch 完成res文件的比对

各种文件的规则是可配置的。

(1)dexDecoder.patch

public boolean patch(final File oldFile, final File newFile)  {    final String dexName = getRelativeDexName(oldFile, newFile);    // 检查loader class,省略了抛异常的一些代码    excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);    File dexDiffOut = getOutputPath(newFile).toFile();    final String newMd5 = getRawOrWrappedDexMD5(newFile);    //new add file    if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {        hasDexChanged = true;        copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);        return true;    }    final String oldMd5 = getRawOrWrappedDexMD5(oldFile);    if ((oldMd5 != null && !oldMd5.equals(newMd5)) || (oldMd5 == null && newMd5 != null)) {        hasDexChanged = true;        if (oldMd5 != null) {            collectAddedOrDeletedClasses(oldFile, newFile);        }    }    RelatedInfo relatedInfo = new RelatedInfo();    relatedInfo.oldMd5 = oldMd5;    relatedInfo.newMd5 = newMd5;    // collect current old dex file and corresponding new dex file for further processing.    oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));    dexNameToRelatedInfoMap.put(dexName, relatedInfo);    return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

首先执行:

checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);

该方法主要用处是检查 tinker loader相关classes**必须存在primary dex中**,且不允许新增、修改和删除。

所有首先将两个dex读取到内存中,按照config.mDexLoaderPattern进行过滤,找出deletedClassInfosaddedClassInfoschangedClassInfosMap,必须保证deletedClassInfos.isEmpty() && addedClassInfos.isEmpty() && changedClassInfosMap.isEmpty()即不允许新增、删除、修改loader 相关类。

继续,拿到输出目录:

  • build/intermediates/outputs/tinker_result/

然后如果oldFile不存在,则newFile认为是新增文件,直接copy到输出目录,并记录log

copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
  • 1
  • 1

如果存在,则计算两个文件的md5,如果md5不同,则认为dexChanged(hasDexChanged = true),执行:

collectAddedOrDeletedClasses(oldFile, newFile);
  • 1
  • 1

该方法收集了addClasses和deleteClasses的相关信息,记录在:

  • addedClassDescToDexNameMap key为addClassDesc 和 该dex file的path
  • deletedClassDescToDexNameMap key为deletedClassDesc 和 该dex file的path

后续会使用这两个数据结构,mark一下。

继续往下走,初始化了一个relatedInfo记录了两个文件的md5,以及在oldAndNewDexFilePairList中记录了两个dex file,在dexNameToRelatedInfoMap中记录了dexName和relatedInfo的映射。

后续会使用该变量,mark一下。

到此,dexDecoder的patch方法就结束了,仅将新增的文件copy到了目标目录。

那么发生改变的文件,理论上应该要做md5看来在后面才会执行。

如果文件是so文件,则会走soDecoder.patch。

(2)soDecoder.patch

soDecoder实际上是BsDiffDecoder

@Overridepublic boolean patch(File oldFile, File newFile)  {    //new add file    String newMd5 = MD5.getMD5(newFile);    File bsDiffFile = getOutputPath(newFile).toFile();    if (oldFile == null || !oldFile.exists()) {        FileOperation.copyFileUsingStream(newFile, bsDiffFile);        writeLogFiles(newFile, null, null, newMd5);        return true;    }    //new add file    String oldMd5 = MD5.getMD5(oldFile);    if (oldMd5.equals(newMd5)) {        return false;    }    if (!bsDiffFile.getParentFile().exists()) {        bsDiffFile.getParentFile().mkdirs();    }    BSDiff.bsdiff(oldFile, newFile, bsDiffFile);    //超过80%,返回false    if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {        writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);    } else {        FileOperation.copyFileUsingStream(newFile, bsDiffFile);        writeLogFiles(newFile, null, null, newMd5);    }    return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

如果oldFile不存在,则认为newFile为新增文件,直接copy到目标文件(连着so相关目录)。

若oldFile存在,则比对二者md5,如果md5不一致,则直接进行bsdiff算法,直接在目标位置写入bsdiff产生的bsDiffFile。

本来到此应该已经结束了,但是接下来做了一件挺有意思的事:

继续判断了生成的patch文件是否已经超过newFile的80%,如果超过80%,则直接copy newFile到目标目录,直接覆盖了刚生成的patch文件。

那么soPatch整个过程:

  1. 如果是新增文件,直接copy至目标文件夹,记录log
  2. 如果是改变的文件,patch文件超过新文件的80%,则直接copy新文件至目标文件夹,记录log
  3. 如果是改变的文件,patch文件不超过新文件的80%,则copy patch文件至目标文件夹,记录log

如果newFile是res 资源,则会走resDecoder

(3)resDecoder.patch

@Overridepublic boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {    String name = getRelativePathStringToNewFile(newFile);    File outputFile = getOutputPath(newFile).toFile();    if (oldFile == null || !oldFile.exists()) {        FileOperation.copyFileUsingStream(newFile, outputFile);        addedSet.add(name);        writeResLog(newFile, oldFile, TypedValue.ADD);        return true;    }    //new add file    String newMd5 = MD5.getMD5(newFile);    String oldMd5 = MD5.getMD5(oldFile);    //oldFile or newFile may be 0b length    if (oldMd5 != null && oldMd5.equals(newMd5)) {        return false;    }    if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {        Logger.d("found modify resource: " + name + ", but it match ignore change pattern, just ignore!");        return false;    }    if (name.equals(TypedValue.RES_MANIFEST)) {        Logger.d("found modify resource: " + name + ", but it is AndroidManifest.xml, just ignore!");        return false;    }    if (name.equals(TypedValue.RES_ARSC)) {        if (AndroidParser.resourceTableLogicalChange(config)) {            Logger.d("found modify resource: " + name + ", but it is logically the same as original new resources.arsc, just ignore!");            return false;        }    }    dealWithModeFile(name, newMd5, oldFile, newFile, outputFile);    return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

如果oldFile不存在,则认为新增文件,直接copy且加入到addedSet集合,并记录log

如果存在,且md5不同调研dealWithModeFile(设置的sIgnoreChangePattern、MANIFEST和逻辑上相同的ARSC不做处理)。

private boolean dealWithModeFile(String name, String newMd5, File oldFile, File newFile, File outputFile) {    if (checkLargeModFile(newFile)) {        if (!outputFile.getParentFile().exists()) {            outputFile.getParentFile().mkdirs();        }        BSDiff.bsdiff(oldFile, newFile, outputFile);        //未超过80%返回true        if (Utils.checkBsDiffFileSize(outputFile, newFile)) {            LargeModeInfo largeModeInfo = new LargeModeInfo();            largeModeInfo.path = newFile;            largeModeInfo.crc = FileOperation.getFileCrc32(newFile);            largeModeInfo.md5 = newMd5;            largeModifiedSet.add(name);            largeModifiedMap.put(name, largeModeInfo);            writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);            return true;        }    }    modifiedSet.add(name);    FileOperation.copyFileUsingStream(newFile, outputFile);    writeResLog(newFile, oldFile, TypedValue.MOD);    return false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这里,首先check了largeFile,即改变的文件是否大于100K(该值可以配置)。

如果非大文件,则直接copy至目标文件,且记录到modifiedSet,并记录了log。

如果是大文件,则直接bsdiff,生成patch File;接下来也检查了一下patch file是否超过newFile的80%,如果超过,则直接copy newFile覆盖刚生成的patch File;

总体和so patch基本一致。

到这里,除了dex patch中对改变的dex文件没有做处理以外,so 和 res都做了。

接下来执行了:

public boolean patch(File oldFile, File newFile) throws Exception {    //...    soPatchDecoder.onAllPatchesEnd();    dexPatchDecoder.onAllPatchesEnd();    manifestDecoder.onAllPatchesEnd();    resPatchDecoder.onAllPatchesEnd();    //clean resources    dexPatchDecoder.clean();    soPatchDecoder.clean();    resPatchDecoder.clean();    return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

其中dexPatchDecoder和resPatchDecoder有后续实现。

(4) dexPatchDecoder.onAllPatchesEnd

# DexDiffDecoder@Overridepublic void onAllPatchesEnd() throws Exception {    if (!hasDexChanged) {        Logger.d("No dexes were changed, nothing needs to be done next.");        return;    }    generatePatchInfoFile();    addTestDex();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

如果dex文件没有改变,直接返回。

private void generatePatchInfoFile() throws IOException {    generatePatchedDexInfoFile();    logDexesToDexMeta();    checkCrossDexMovingClasses();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

主要看generatePatchedDexInfoFile

private void generatePatchedDexInfoFile() {    // Generate dex diff out and full patched dex if a pair of dex is different.    for (AbstractMap.SimpleEntry<File, File> oldAndNewDexFilePair : oldAndNewDexFilePairList) {        File oldFile = oldAndNewDexFilePair.getKey();        File newFile = oldAndNewDexFilePair.getValue();        final String dexName = getRelativeDexName(oldFile, newFile);        RelatedInfo relatedInfo = dexNameToRelatedInfoMap.get(dexName);        if (!relatedInfo.oldMd5.equals(relatedInfo.newMd5)) {            diffDexPairAndFillRelatedInfo(oldFile, newFile, relatedInfo);        } else {            // In this case newDexFile is the same as oldDexFile, but we still            // need to treat it as patched dex file so that the SmallPatchGenerator            // can analyze which class of this dex should be kept in small patch.            relatedInfo.newOrFullPatchedFile = newFile;            relatedInfo.newOrFullPatchedMd5 = relatedInfo.newMd5;        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

oldAndNewDexFilePairList中记录了两个dex文件,然后根据dex file获取到dexName,再由dexNameToRelatedInfoMap根据name获得到RelatedInfo。

RelatedInfo中包含了两个dex file的md5,如果不同,则执行diffDexPairAndFillRelatedInfo

private void diffDexPairAndFillRelatedInfo(File oldDexFile,                         File newDexFile, RelatedInfo relatedInfo) {    //outputs/tempPatchedDexes    File tempFullPatchDexPath = new File(config.mOutFolder                 + File.separator + TypedValue.DEX_TEMP_PATCH_DIR);    final String dexName = getRelativeDexName(oldDexFile, newDexFile);    File dexDiffOut = getOutputPath(newDexFile).toFile();    ensureDirectoryExist(dexDiffOut.getParentFile());    // dex diff , 去除loader classes    DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);    dexPatchGen.setAdditionalRemovingClassPatterns(config.mDexLoaderPattern);    dexPatchGen.executeAndSaveTo(dexDiffOut);    relatedInfo.dexDiffFile = dexDiffOut;    relatedInfo.dexDiffMd5 = MD5.getMD5(dexDiffOut);    File tempFullPatchedDexFile = new File(tempFullPatchDexPath, dexName);    try {        new DexPatchApplier(oldDexFile, dexDiffOut).executeAndSaveTo(tempFullPatchedDexFile);        Logger.d(                String.format("Verifying if patched new dex is logically the same as original new dex: %s ...", getRelativeStringBy(newDexFile, config.mTempUnzipNewDir))        );        Dex origNewDex = new Dex(newDexFile);        Dex patchedNewDex = new Dex(tempFullPatchedDexFile);        checkDexChange(origNewDex, patchedNewDex);        relatedInfo.newOrFullPatchedFile = tempFullPatchedDexFile;        relatedInfo.newOrFullPatchedMd5 = MD5.getMD5(tempFullPatchedDexFile);    } catch (Exception e) {        e.printStackTrace();        throw new TinkerPatchException(                "Failed to generate temporary patched dex, which makes MD5 generating procedure of new dex failed, either.", e        );    }    if (!tempFullPatchedDexFile.exists()) {        throw new TinkerPatchException("can not find the temporary full patched dex file:" + tempFullPatchedDexFile.getAbsolutePath());    }    Logger.d("\nGen %s for dalvik full dex file:%s, size:%d, md5:%s", dexName, tempFullPatchedDexFile.getAbsolutePath(), tempFullPatchedDexFile.length(), relatedInfo.newOrFullPatchedMd5);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

开始针对两个dex文件做dex diff,最终将生成的patch 文件放置在目标文件夹中。

接下来,生成一个临时文件夹,通过DexPatchApplier针对生成的patch文件和old dex file,直接做了合并操作,相当于在本地模拟执行了在客户端上的patch操作。

然后再对新合并生成的patchedNewDex与之前的origNewDex,进行了checkDexChange,即这两者类级别对比,应该所有的类都相同。

最后在dexDecoder的onAllPatchesEnd中还执行了一个addTestDex

private void addTestDex() throws IOException {    //write test dex    String dexMode = "jar";    if (config.mDexRaw) {        dexMode = "raw";    }    final InputStream is = DexDiffDecoder.class.getResourceAsStream("/" + TEST_DEX_NAME);    String md5 = MD5.getMD5(is, 1024);    is.close();    String meta = TEST_DEX_NAME + "," + "" + "," + md5 + "," + md5 + "," + 0 + "," + 0 + "," + dexMode;    File dest = new File(config.mTempResultDir + "/" + TEST_DEX_NAME);    FileOperation.copyResourceUsingStream(TEST_DEX_NAME, dest);    Logger.d("\nAdd test install result dex: %s, size:%d", dest.getAbsolutePath(), dest.length());    Logger.d("DexDecoder:write test dex meta file data: %s", meta);    metaWriter.writeLineToInfoFile(meta);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

完成了所有的diff工作后,后面就是生成patch文件了。

(2)打包所有生成的patch文件

//build patchPatchBuilder builder = new PatchBuilder(config);builder.buildPatch();
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

详细代码:

public PatchBuilder(Configuration config) {    this.config = config;    this.unSignedApk = new File(config.mOutFolder, PATCH_NAME + "_unsigned.apk");    this.signedApk = new File(config.mOutFolder, PATCH_NAME + "_signed.apk");    this.signedWith7ZipApk = new File(config.mOutFolder, PATCH_NAME + "_signed_7zip.apk");    this.sevenZipOutPutDir = new File(config.mOutFolder, TypedValue.OUT_7ZIP_FILE_PATH);}public void buildPatch() throws Exception {    final File resultDir = config.mTempResultDir;    //no file change    if (resultDir.listFiles().length == 0) {        return;    }generateUnsignedApk(unSignedApk);    signApk(unSignedApk, signedApk);    use7zApk(signedApk, signedWith7ZipApk, sevenZipOutPutDir);    if (!signedApk.exists()) {        Logger.e("Result: final unsigned patch result: %s, size=%d", unSignedApk.getAbsolutePath(), unSignedApk.length());    } else {        long length = signedApk.length();        Logger.e("Result: final signed patch result: %s, size=%d", signedApk.getAbsolutePath(), length);        if (signedWith7ZipApk.exists()) {            long length7zip = signedWith7ZipApk.length();            Logger.e("Result: final signed with 7zip patch result: %s, size=%d", signedWith7ZipApk.getAbsolutePath(), length7zip);            if (length7zip > length) {                Logger.e("Warning: %s is bigger than %s %d byte, you should choose %s at these time!",                    signedWith7ZipApk.getName(),                    signedApk.getName(),                    (length7zip - length),                    signedApk.getName());            }        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

主要会生成3个文件:unSignedApksignedApk以及signedWith7ZipApk

unSignedApk只要将tinker_result中的文件压缩到一个压缩包即可。 
signedApk将unSignedApk使用jarsigner进行签名。

signedWith7ZipApk主要是对signedApk进行解压再做sevenZip压缩。

好了,到此茫茫长的文章就结束啦~~~

受限于本人知识,文中难免出现错误,可以直接留言指出。

九、总结

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

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

可以阅读以下文章,继续了解tinker~~

  • Tinker:技术的初心与坚持
  • 微信Android热补丁实践演进之路
  • Android N混合编译与对热补丁影响解析
  • Dev Club 微信热补丁Tinker分享
  • 微信Tinker的一切都在这里,包括源码(一)
  • Tinker Dexdiff算法解析
  • ART下的方法内联策略及其对Android热修复方案的影响分析
  • Tinker MDCC会议 slide
  • DexDiff格式查看工具
阅读全文
0 0
原创粉丝点击