tinker_tinker热更新 - CSDN
精华内容
参与话题
  • Android 热修复 Tinker Gradle Plugin解析

    万次阅读 热门讨论 2017-05-23 23:13:43
    前面写了两篇分析了tinker的loader部分源码以及dex diff/patch算法相关解析,那么为了保证完整性,最后一篇主要写tinker-patch-gradle-plugin相关了。 (距离看的时候已经快两个月了,再不写就忘了,赶紧记录下来...

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

    一、概述

    前面写了两篇分析了tinker的loader部分源码以及dex diff/patch算法相关解析,那么为了保证完整性,最后一篇主要写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的学习资料~

    二、寻找查看代码入口

    下载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

    SouthEast

    名称实际上就是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.assemble
    
    TinkerManifestTask manifestTask 
            = project.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask)
    manifestTask.mustRunAfter variantOutput.processManifest
    variantOutput.processResources.dependsOn manifestTask
    
    TinkerResourceIdTask applyResourceTask 
            = project.tasks.create("tinkerProcess${variantName}ResourceId", TinkerResourceIdTask)
    applyResourceTask.mustRunAfter manifestTask
    variantOutput.processResources.dependsOn applyResourceTask
    
    if (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
        }
    }

    可以看到它通过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编写以及依赖方式的设置。

    三、每个Task的具体行为

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

    TinkerManifestTask
    TinkerResourceIdTask
    TinkerProguardConfigTask
    TinkerMultidexConfigTask
    TinkerPatchSchemaTask

    丢个图,对应下:

    SouthEast

    四、TinkerManifestTask

    #TinkerManifestTask
    @TaskAction
    def 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))
        }
    }

    这里主要做了两件事:

    • 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.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" />

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

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

    开始源码分析

    @TaskAction
    def 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}");
        }
    }

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

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

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

    int anim abc_slide_in_bottom 0x7f050006
    int id useLogo 0x7f0b0012
    int[] styleable AppCompatImageView { 0x01010119, 0x7f010027 }
    int styleable AppCompatImageView_android_src 0
    int styleable AppCompatImageView_srcCompat 1

    大概有两类,一类是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}");
        }
    

    那么到了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;
    }

    首先初始化了一个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);
                        }
                    }
                }
            }
        }
    }

    对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);
                    }
                }
            }
        }
    }

    遍历我们的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);
            }
        }
    }

    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);

    注:除此以外,这里在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);
        }
    }

    如果不是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);
        }
    }

    首先构建一个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);

    查看代码:

    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);
        }
    
    }

    大体意思就是如果该资源不存在就添加到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);
            }
        }
    }

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

    resourceCollector
    .addIntResourceIfNotPresent(rType, resourceName);

    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);
            }
        }
    }

    遍历除了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()));
        }
    
        // 省略了无关代码
    }
    

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

    resourceCollector
        .addIntResourceIfNotPresent(RType.ID, 
            resourceName.substring(ID_DEFINITION_PREFIX.length()));
    

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

    
    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();
            }
        }
    }

    主要就是遍历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一些必要的类了。

    @TaskAction
    def 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}")
    }

    读取我们设置的mappingFile,设置

    -applymapping applyMappingFile

    然后设置一些默认需要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"

    最后是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"
    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")
        }

    相关类都在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();
        }
    }
    

    主要分为以下环节:

    • 生成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;
    }

    代码内部包含四个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));

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

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

    @Override
    public 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;
    }

    首先去除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;
    }

    首先执行:

    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);

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

    collectAddedOrDeletedClasses(oldFile, newFile);

    该方法收集了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

    @Override
    public 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;
    }

    如果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

    @Override
    public 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;
    }
    

    如果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;
    }

    这里,首先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;
    }

    其中dexPatchDecoder和resPatchDecoder有后续实现。

    (4) dexPatchDecoder.onAllPatchesEnd

    # DexDiffDecoder
    @Override
    public void onAllPatchesEnd() throws Exception {
        if (!hasDexChanged) {
            Logger.d("No dexes were changed, nothing needs to be done next.");
            return;
        }
    
        generatePatchInfoFile();
    
        addTestDex();
    }

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

    private void generatePatchInfoFile() throws IOException {
        generatePatchedDexInfoFile();
    
        logDexesToDexMeta();
    
        checkCrossDexMovingClasses();
    }

    主要看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;
            }
        }
    }

    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);
    }

    开始针对两个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);
    }

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

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

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

    //build patch
    PatchBuilder builder = new PatchBuilder(config);
    builder.buildPatch();

    详细代码:

    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());
                }
            }
        }
    
    }
    

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

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

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

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

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

    九、总结

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

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

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


    支持我的话可以关注下我的公众号,每天都会推送新知识~

    欢迎关注我的微信公众号:hongyangAndroid
    (可以给我留言你想学习的文章,支持投稿)
    1468942304_6791.jpg

    展开全文
  • Android 热修复 Tinker接入及源码浅析

    万次阅读 多人点赞 2017-02-06 10:03:22
    一、概述 放了一个大长假,happy,先祝大家2017年笑口常开。 假期中一行代码没写,但是想着马上要上班了,赶紧写篇...现在热修复的技术基本上有阿里的AndFix、QZone的方案、美团提出的思想方案以及腾讯的Tinker等。

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

    一、概述

    放了一个大长假,happy,先祝大家2017年笑口常开。

    假期中一行代码没写,但是想着马上要上班了,赶紧写篇博客回顾下技能,于是便有了本文。

    热修复这项技术,基本上已经成为项目比较重要的模块了。主要因为项目在上线之后,都难免会有各种问题,而依靠发版去修复问题,成本太高了。

    现在热修复的技术基本上有阿里的AndFix、QZone的方案、美团提出的思想方案以及腾讯的Tinker等。

    其中AndFix可能接入是最简单的一个(和Tinker命令行接入方式差不多),不过兼容性还是是有一定的问题的;QZone方案对性能会有一定的影响,且在Art模式下出现内存错乱的问题(其实这个问题我之前并不清楚,主要是tinker在MDCC上指出的);美团提出的思想方案主要是基于Instant Run的原理,目前尚未开源,不过这个方案我还是蛮喜欢的,主要是兼容性好。

    这么看来,如果选择开源方案,tinker目前是最佳的选择,tinker的介绍有这么一句:

    Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?

    好了,说了这么多,下面来看看tinker如何接入,以及tinker的大致的原理分析。希望通过本文可以实现帮助大家更好的接入tinker,以及去了解tinker的一个大致的原理。

    二、接入Tinker

    接入tinker目前给了两种方式,一种是基于命令行的方式,类似于AndFix的接入方式;一种就是gradle的方式。

    考虑早期使用Andfix的app应该挺多的,以及很多人对gradle的相关配置还是觉得比较繁琐的,下面对两种方式都介绍下。

    (1)命令行接入

    接入之前我们先考虑下,接入的话,正常需要的前提(开启混淆的状态)。

    • 对于API

      一般来说,我们接入热修库,会在Application#onCreate中进行一下初始化操作。然后在某个地方去调用类似loadPatch这样的API去加载patch文件。

    • 对于patch的生成

      简单的方式就是通过两个apk做对比然后生成;需要注意的是:两个apk做对比,需要的前提条件,第二次打包混淆所使用的mapping文件应该和线上apk是一致的。

    最后就是看看这个项目有没有需要配置混淆;

    有了大致的概念,我们就基本了解命令行接入tinker,大致需要哪些步骤了。

    依赖引入

    dependencies {
        // ...
        //可选,用于生成application类
        provided('com.tencent.tinker:tinker-android-anno:1.7.7')
        //tinker的核心库
        compile('com.tencent.tinker:tinker-android-lib:1.7.7')
    }

    顺便加一下签名的配置:

    android{
      //...
        signingConfigs {
            release {
                try {
                    storeFile file("release.keystore")
                    storePassword "testres"
                    keyAlias "testres"
                    keyPassword "testres"
                } catch (ex) {
                    throw new InvalidUserDataException(ex.toString())
                }
            }
        }
    
        buildTypes {
            release {
                minifyEnabled true
                signingConfig signingConfigs.release
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
            debug {
                debuggable true
                minifyEnabled true
                signingConfig signingConfigs.release
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
    }

    文末会有demo的下载地址,可以直接参考build.gradle文件,不用担心这些签名文件去哪找。

    API引入

    API主要就是初始化和loadPacth。

    正常情况下,我们会考虑在Application的onCreate中去初始化,不过tinker推荐下面的写法:

    @DefaultLifeCycle(application = ".SimpleTinkerInApplication",
            flags = ShareConstants.TINKER_ENABLE_ALL,
            loadVerifyFlag = false)
    public class SimpleTinkerInApplicationLike extends ApplicationLike {
        public SimpleTinkerInApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
            super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
        }
    
        @Override
        public void onBaseContextAttached(Context base) {
            super.onBaseContextAttached(base);
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            TinkerInstaller.install(this);
        }
    }

    ApplicationLike通过名字你可能会猜,并非是Application的子类,而是一个类似Application的类。

    tinker建议编写一个ApplicationLike的子类,你可以当成Application去使用,注意顶部的注解:@DefaultLifeCycle,其application属性,会在编译期生成一个SimpleTinkerInApplication类。

    所以,虽然我们这么写了,但是实际上Application会在编译期生成,所以AndroidManifest.xml中是这样的:

     <application
            android:name=".SimpleTinkerInApplication"
            .../>

    编写如果报红,可以build下。

    这样其实也能猜出来,这个注解背后有个Annotation Processor在做处理,如果你没了解过,可以看下:

    通过该文会对一个编译时注解的运行流程和基本API有一定的掌握,文中也会对tinker该部分的源码做解析。

    上述,就完成了tinker的初始化,那么调用loadPatch的时机,我们直接在Activity中添加一个Button设置:

    
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
    
        public void loadPatch(View view) {
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                    Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");
        }
    }
    

    我们会将patch文件直接push到sdcard根目录;

    所以一定要注意:添加SDCard权限,如果你是6.x以上的系统,自己添加上授权代码,或者手动在设置页面打开SDCard读写权限。

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    除以以外,有个特殊的地方就是tinker需要在AndroidManifest.xml中指定TINKER_ID。

    <application>
      <meta-data
                android:name="TINKER_ID"
                android:value="tinker_id_6235657" />
        //...
    </application>

    到此API相关的就结束了,剩下的就是考虑patch如何生成。

    patch生成

    tinker提供了patch生成的工具,源码见:tinker-patch-cli,打成一个jar就可以使用,并且提供了命令行相关的参数以及文件。

    命令行如下:

    java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output

    需要注意的就是tinker_config.xml,里面包含tinker的配置,例如签名文件等。

    这里我们直接使用tinker提供的签名文件,所以不需要做修改,不过里面有个Application的item修改为与本例一致:

    <loader value="com.zhy.tinkersimplein.SimpleTinkerInApplication"/>

    大致的文件结构如下:

    SouthEast

    可以在tinker-patch-cli中提取,或者直接下载文末的例子。

    上述介绍了patch生成的命令,最后需要注意的就是,在第一次打出apk的时候,保留下生成的mapping文件,在/build/outputs/mapping/release/mapping.txt

    可以copy到与proguard-rules.pro同目录,同时在第二次打修复包的时候,在proguard-rules.pro中添加上:

    -applymapping mapping.txt

    保证后续的打包与线上包使用的是同一个mapping文件。

    tinker本身的混淆相关配置,可以参考:

    如果,你对该部分描述不了解,可以直接查看源码即可。

    测试

    首先随便生成一个apk(API、混淆相关已经按照上述引入),安装到手机或者模拟器上。

    然后,copy出mapping.txt文件,设置applymapping,修改代码,再次打包,生成new.apk。

    两次的apk,可以通过命令行指令去生成patch文件。

    如果你下载本例,命令需要在[该目录]下执行。

    最终会在output文件夹中生成产物:

    SouthEast

    我们直接将patch_signed.apk push到sdcard,点击loadpatch,一定要观察命令行是否成功。

    SouthEast

    本例修改了title。

    点击loadPatch,观察log,如果成功,应用默认为重启,然后再次启动即可达到修复效果。

    到这里命令行的方式就介绍完了,和Andfix的接入的方式基本上是一样的。

    值得注意的是:该例仅展示了基本的接入,对于tinker的各种配置信息,还是需要去读tinker的文档(如果你确定要使用)tinker-wiki

    (2)gradle接入

    gradle接入的方式应该算是主流的方式,所以tinker也直接给出了例子,单独将该tinker-sample-android以project方式引入即可。

    引入之后,可以查看其接入API的方式,以及相关配置。

    在你每次build时,会在build/bakApk下生成本地打包的apk,R文件,以及mapping文件。

    如果你需要生成patch文件,可以通过:

    ./gradlew tinkerPatchRelease  // 或者 ./gradlew tinkerPatchDebug

    生成。

    生成目录为:build/outputs/tinkerPatch

    SouthEast

    需要注意的是,需要在app/build.gradle中设置相比较的apk(即old.apk,本次为new.apk),

    ext {
        tinkerEnabled = true
        //old apk file to build patch apk
        tinkerOldApkPath = "${bakPath}/old.apk"
        //proguard mapping file to build patch apk
        tinkerApplyMappingPath = "${bakPath}/old-mapping.txt"
    }

    提供的例子,基本上展示了tinker的自定义扩展的方式,具体还可以参考:

    所以,如果你使用命令行方式接入,也不要忘了学习下其支持哪些扩展。

    三、Application是如何编译时生成的

    从注释和命名上看:

    //可选,用于生成application类
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')

    明显是该库,其结构如下:

    SouthEast

    典型的编译时注解的项目,源码见tinker-android-anno

    入口为com.tencent.tinker.anno.AnnotationProcessor,可以在该services/javax.annotation.processing.Processor文件中找到处理类全路径。

    再次建议,如果你不了解,简单阅读下Android 如何编写基于编译时注解的项目该文。

    直接看AnnotationProcessor的process方法:

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        processDefaultLifeCycle(roundEnv.getElementsAnnotatedWith(DefaultLifeCycle.class));
        return true;
    }

    直接调用了processDefaultLifeCycle:

    private void processDefaultLifeCycle(Set<? extends Element> elements) {
            // 被注解DefaultLifeCycle标识的对象
            for (Element e : elements) {
              // 拿到DefaultLifeCycle注解对象
                DefaultLifeCycle ca = e.getAnnotation(DefaultLifeCycle.class);
    
                String lifeCycleClassName = ((TypeElement) e).getQualifiedName().toString();
                String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf('.'));
                lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf('.') + 1);
    
                String applicationClassName = ca.application();
                if (applicationClassName.startsWith(".")) {
                    applicationClassName = lifeCyclePackageName + applicationClassName;
                }
                String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf('.'));
                applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf('.') + 1);
    
                String loaderClassName = ca.loaderClass();
                if (loaderClassName.startsWith(".")) {
                    loaderClassName = lifeCyclePackageName + loaderClassName;
                }
    
                 // /TinkerAnnoApplication.tmpl
                final InputStream is = AnnotationProcessor.class.getResourceAsStream(APPLICATION_TEMPLATE_PATH);
                final Scanner scanner = new Scanner(is);
                final String template = scanner.useDelimiter("\\A").next();
                final String fileContent = template
                    .replaceAll("%PACKAGE%", applicationPackageName)
                    .replaceAll("%APPLICATION%", applicationClassName)
                    .replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName)
                    .replaceAll("%TINKER_FLAGS%", "" + ca.flags())
                    .replaceAll("%TINKER_LOADER_CLASS%", "" + loaderClassName)
                    .replaceAll("%TINKER_LOAD_VERIFY_FLAG%", "" + ca.loadVerifyFlag());
                    JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName);
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Creating " + fileObject.toUri());
              Writer writer = fileObject.openWriter();
                PrintWriter pw = new PrintWriter(writer);
                pw.print(fileContent);
                pw.flush();
                writer.close();
    
            }
        }

    代码比较简单,可以分三部分理解:

    • 步骤1:首先找到被DefaultLifeCycle标识的Element(为类对象TypeElement),得到该对象的包名,类名等信息,然后通过该对象,拿到@DefaultLifeCycle对象,获取该注解中声明属性的值。
    • 步骤2:读取一个模板文件,读取为字符串,将各个占位符通过步骤1中的值替代。
    • 步骤3:通过JavaFileObject将替换完成的字符串写文件,其实就是本例中的Application对象。

    我们看一眼模板文件:

    package %PACKAGE%;
    
    import com.tencent.tinker.loader.app.TinkerApplication;
    
    /**
     *
     * Generated application for tinker life cycle
     *
     */
    public class %APPLICATION% extends TinkerApplication {
    
        public %APPLICATION%() {
            super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);
        }
    
    }

    对应我们的SimpleTinkerInApplicationLike

    @DefaultLifeCycle(application = ".SimpleTinkerInApplication",
            flags = ShareConstants.TINKER_ENABLE_ALL,
            loadVerifyFlag = false)
    public class SimpleTinkerInApplicationLike extends ApplicationLike {}

    主要就几个占位符:

    • 包名,如果application属性值以点开始,则同包;否则则截取
    • 类名,application属性值中的类名
    • %TINKER_FLAGS%对应flags
    • %APPLICATION_LIFE_CYCLE%,编写的ApplicationLike的全路径
    • “%TINKER_LOADER_CLASS%”,这个值我们没有设置,实际上对应@DefaultLifeCycle的loaderClass属性,默认值为com.tencent.tinker.loader.TinkerLoader
    • %TINKER_LOAD_VERIFY_FLAG%对应loadVerifyFlag

    于是最终生成的代码为:

    /**
     *
     * Generated application for tinker life cycle
     *
     */
    public class SimpleTinkerInApplication extends TinkerApplication {
    
        public SimpleTinkerInApplication() {
            super(7, "com.zhy.tinkersimplein.SimpleTinkerInApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
        }
    
    }

    tinker这么做的目的,文档上是这么说的:

    为了减少错误的出现,推荐使用Annotation生成Application类。

    这样大致了解了Application是如何生成的。

    接下来我们大致看一下tinker的原理。

    四、原理

    SouthEast

    来源于:https://github.com/Tencent/tinker

    tinker贴了一张大致的原理图。

    可以看出:

    tinker将old.apk和new.apk做了diff,拿到patch.dex,然后将patch.dex与本机中apk的classes.dex做了合并,生成新的classes.dex,运行时通过反射将合并后的dex文件放置在加载的dexElements数组的前面。

    运行时替代的原理,其实和Qzone的方案差不多,都是去反射修改dexElements。

    两者的差异是:Qzone是直接将patch.dex插到数组的前面;而tinker是将patch.dex与app中的classes.dex合并后的全量dex插在数组的前面。

    tinker这么做的目的还是因为Qzone方案中提到的CLASS_ISPREVERIFIED的解决方案存在问题;而tinker相当于换个思路解决了该问题。

    接下来我们就从代码中去验证该原理。

    本片文章源码分析的两条线:

    • 应用启动时,从默认目录加载合并后的classes.dex
    • patch下发后,合成classes.dex至目标目录

    五、源码分析

    (1)加载patch

    加载的代码实际上在生成的Application中调用的,其父类为TinkerApplication,在其attachBaseContext中辗转会调用到loadTinker()方法,在该方法内部,反射调用了TinkerLoader的tryLoad方法。

    @Override
    public 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中会调用到loadTinkerJars方法:

    private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {
        // 省略大量安全性校验代码
    
        if (isEnabledForDex) {
            //tinker/patch.info/patch-641e634c/dex
            boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
            if (!dexCheck) {
                //file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:dex check fail");
                return;
            }
        }
    
        //now we can load patch jar
        if (isEnabledForDex) {
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);
            if (!loadTinkerJars) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
                return;
            }
        }
    }

    TinkerDexLoader.checkComplete主要是用于检查下发的meta文件中记录的dex信息(meta文件,可以查看生成patch的产物,在assets/dex-meta.txt),检查meta文件中记录的dex文件信息对应的dex文件是否存在,并把值存在TinkerDexLoader的静态变量dexList中。

    TinkerDexLoader.loadTinkerJars传入四个参数,分别为application,tinkerLoadVerifyFlag(注解上声明的值,传入为false),patchVersionDirectory当前version的patch文件夹,intent,当前patch是否仅适用于art。

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, 
        String directory, Intent intentResult, boolean isSystemOTA) {
            PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
    
            String dexPath = directory + "/" + DEX_PATH + "/";
            File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH);
    
            ArrayList<File> legalFiles = new ArrayList<>();
    
            final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();
            for (ShareDexDiffPatchInfo info : dexList) {
                //for dalvik, ignore art support dex
                if (isJustArtSupportDex(info)) {
                    continue;
                }
                String path = dexPath + info.realName;
                File file = new File(path);
    
                legalFiles.add(file);
            }
            // just for art
            if (isSystemOTA) {
                parallelOTAResult = true;
                parallelOTAThrowable = null;
                Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");
    
                TinkerParallelDexOptimizer.optimizeAll(
                    legalFiles, optimizeDir,
                    new TinkerParallelDexOptimizer.ResultCallback() {
                    }
                );
    
            SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
            return true;
        }
    

    找出仅支持art的dex,且当前patch是否仅适用于art时,并行去loadDex。

    关键是最后的installDexes:

    @SuppressLint("NewApi")
    public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
        throws Throwable {
    
        if (!files.isEmpty()) {
            ClassLoader classLoader = loader;
            if (Build.VERSION.SDK_INT >= 24) {
                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 dex2opt
            if (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 done
            sPatchDexCount = files.size();
            Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
    
            if (!checkDexInstall(classLoader)) {
                //reset patch dex
                SystemClassLoaderAdder.uninstallPatchDex(classLoader);
                throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
            }
        }
    }

    这里实际上就是根据不同的系统版本,去反射处理dexElements。

    我们看一下V19的实现(主要我看了下本机只有个22的源码~):

    private static final class V19 {
    
        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
    
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makeDexElement", e);
                    throw e;
                }
            }
        }
    }        
    1. 找到PathClassLoader(BaseDexClassLoader)对象中的pathList对象
    2. 根据pathList对象找到其中的makeDexElements方法,传入patch相关的对应的实参,返回Element[]对象
    3. 拿到pathList对象中原本的dexElements方法
    4. 步骤2与步骤3中的Element[]数组进行合并,将patch相关的dex放在数组的前面
    5. 最后将合并后的数组,设置给pathList

    这里其实和Qzone的提出的方案基本是一致的。如果你以前未了解过Qzone的方案,可以参考此文:

    (2)合成patch

    这里的入口为:

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

    上述代码会调用DefaultPatchListener中的onPatchReceived方法:

    # DefaultPatchListener
    @Override
    public int onPatchReceived(String path) {
    
        int returnCode = patchCheck(path);
    
        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
        }
        return returnCode;
    
    }

    首先对tinker的相关配置(isEnable)以及patch的合法性进行检测,如果合法,则调用TinkerPatchService.runPatchService(context, path);

    public static void runPatchService(Context context, String path) {
        try {
            Intent intent = new Intent(context, TinkerPatchService.class);
            intent.putExtra(PATCH_PATH_EXTRA, path);
            intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
            context.startService(intent);
        } catch (Throwable throwable) {
            TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);
        }
    }

    TinkerPatchService是IntentService的子类,这里通过intent设置了两个参数,一个是patch的路径,一个是resultServiceClass,该值是调用Tinker.install的时候设置的,默认为DefaultTinkerResultService.class。由于是IntentService,直接看onHandleIntent即可,如果你对IntentService陌生,可以查看此文:Android IntentService完全解析 当Service遇到Handler

    @Override
    protected void onHandleIntent(Intent intent) {
        final Context context = getApplicationContext();
        Tinker tinker = Tinker.with(context);
    
    
        String path = getPatchPathExtra(intent);
    
        File patchFile = new File(path);
    
        boolean result;
    
        increasingPriority();
        PatchResult patchResult = new PatchResult();
    
        result = upgradePatchProcessor.tryPatch(context, path, patchResult);
    
        patchResult.isSuccess = result;
        patchResult.rawPatchFilePath = path;
        patchResult.costTime = cost;
        patchResult.e = e;
    
        AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
    
    }

    比较清晰,主要关注upgradePatchProcessor.tryPatch方法,调用的是UpgradePatch.tryPatch。ps:这里有个有意思的地方increasingPriority(),其内部实现为:

    private void increasingPriority() {
        TinkerLog.i(TAG, "try to increase patch process priority");
        try {
            Notification notification = new Notification();
            if (Build.VERSION.SDK_INT < 18) {
                startForeground(notificationId, notification);
            } else {
                startForeground(notificationId, notification);
                // start InnerService
                startService(new Intent(this, InnerService.class));
            }
        } catch (Throwable e) {
            TinkerLog.i(TAG, "try to increase patch process priority error:" + e);
        }
    }

    如果你对“保活”这个话题比较关注,那么对这段代码一定不陌生,主要是利用系统的一个漏洞来启动一个前台Service。如果有兴趣,可以参考此文:关于 Android 进程保活,你所需要知道的一切

    下面继续回到tryPatch方法:

    # UpgradePatch
    @Override
    public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
        Tinker manager = Tinker.with(context);
    
        final File patchFile = new File(tempPatchPath);
    
        //it is a new patch, so we should not find a exist
        SharePatchInfo oldInfo = manager.getTinkerLoadResultIfPresent().patchInfo;
        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
    
        //use md5 as version
        patchResult.patchVersion = patchMd5;
        SharePatchInfo newInfo;
    
        //already have patch
        if (oldInfo != null) {
            newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, Build.FINGERPRINT);
        } else {
            newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT);
        }
    
        //check ok, we can real recover a new patch
        final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
        final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);
        final String patchVersionDirectory = patchDirectory + "/" + patchName;
    
        //copy file
        File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));
        // check md5 first
        if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {
            SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
        }
    
        //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
        if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, 
                    destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
            return false;
        }
    
        return true;
    }

    拷贝patch文件拷贝至私有目录,然后调用DexDiffPatchInternal.tryRecoverDexFiles

    protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
                                                    String patchVersionDirectory, File patchFile) {
        String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);
        boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
        return result;
    }

    直接看patchDexExtractViaDexDiff

    private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
        String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
    
        if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
            TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
            return false;
        }
    
        final Tinker manager = Tinker.with(context);
    
        File dexFiles = new File(dir);
        File[] files = dexFiles.listFiles();
    
        ...files遍历执行:DexFile.loadDex
         return true;
    }

    核心代码主要在extractDexDiffInternals中:

    private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
        //parse meta
        ArrayList<ShareDexDiffPatchInfo> patchList = new ArrayList<>();
        ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
    
        File directory = new File(dir);
        //I think it is better to extract the raw files from apk
        Tinker manager = Tinker.with(context);
        ZipFile apk = null;
        ZipFile patch = null;
    
        ApplicationInfo applicationInfo = context.getApplicationInfo();
    
        String apkPath = applicationInfo.sourceDir; //base.apk
        apk = new ZipFile(apkPath);
        patch = new ZipFile(patchFile);
    
        for (ShareDexDiffPatchInfo info : patchList) {
    
            final String infoPath = info.path;
            String patchRealPath;
            if (infoPath.equals("")) {
                patchRealPath = info.rawName;
            } else {
                patchRealPath = info.path + "/" + info.rawName;
            }
    
            File extractedFile = new File(dir + info.realName);
    
            ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
            ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
    
            patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
        }
    
        return true;
    }

    这里的代码比较关键了,可以看出首先解析了meta里面的信息,meta中包含了patch中每个dex的相关数据。然后通过Application拿到sourceDir,其实就是本机apk的路径以及patch文件;根据mate中的信息开始遍历,其实就是取出对应的dex文件,最后通过patchDexFile对两个dex文件做合并。

    private static void patchDexFile(
                ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
                ShareDexDiffPatchInfo patchInfo,  File patchedDexFile) throws IOException {
        InputStream oldDexStream = null;
        InputStream patchFileStream = null;
    
        oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));
        patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);
    
        new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);
    
    }

    通过ZipFile拿到其内部文件的InputStream,其实就是读取本地apk对应的dex文件,以及patch中对应dex文件,对二者的通过executeAndSaveTo方法进行合并至patchedDexFile,即patch的目标私有目录。

    至于合并算法,这里其实才是tinker比较核心的地方,这个算法跟dex文件格式紧密关联,如果有机会,然后我又能看懂的话,后面会单独写篇博客介绍。此外dodola已经有篇博客进行了介绍:

    感兴趣的可以阅读下。

    好了,到此我们就大致了解了tinker热修复的原理~~

    测试demo地址:

    当然这里只分析了代码了热修复,后续考虑分析资源以及So的热修、核心的diff算法、以及gradle插件等相关知识~


    最后欢迎关注我的公众号~

    我的微信公众号:hongyangAndroid
    (可以给我留言你想学习的文章,支持投稿)
    2b15652affddc0cceb24.jpg

    展开全文
  • Tinker使用指南

    千次阅读 2018-07-09 16:07:59
    随着技术的发展,我们来到了热修复时代,网上一大堆关于热修复的文章和框架,...然后我们再众多热修复框架中,选择Tinker作为我们实现热修复的手段。那就来吧! 前言 其实怎么接入tinker,官方已经给出了方法...

    随着技术的发展,我们来到了热修复时代,网上一大堆关于热修复的文章和框架,相信能来到这里,你也知道什么叫热修复吧,让我用通俗的语言给大家定义一个概念:

    热修复就是在无需重新安装应用的情况下,修改目前存在的bug,或者新增功能

    这样讲是不是蛮好理解的。

    然后我们再众多热修复框架中,选择Tinker作为我们实现热修复的手段。那就来吧!


    前言

    其实怎么接入tinker,官方已经给出了方法,但是我们在接入的时候,难免会遇到不少问题(官方文档不是非常非常详细背锅)。其实官方文档写的还是蛮详细的,只是我们在照着文档操作的时候,还是有那么一点不顺畅,中间遇到一些小问题,所以我希望大家在观看我这篇文章后,能够丝滑接入Tinker,并得到成就感。那,我们就开始吧!

    依赖接入

    假设你已经创建好一个新项目了(我用的Android studio 3.1.2),首先我们先给这个应用加上依赖吧,需要加依赖的地方有2个,我们一个一个来:

    • 在项目根build.gradle添加以下依赖:
    classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')
    • 在应用目录添加以下依赖
    // 可选,用于生成application类 
    compileOnly('com.tencent.tinker:tinker-android-anno:1.9.1')
    // tinker的核心库
    implementation('com.tencent.tinker:tinker-android-lib:1.9.1') 
    // Google提供的分包小助手
    implementation "com.android.support:multidex:1.0.3"

    Gradle配置

    注:以下配置,均在app的gradle文件中做配置

    在项目的build.gradle中配置,在做配置前,我们先创建两个签名文件,一个作为正式版的签名,另一个作为debug版的签名吧,然后放到项目中,这是为了使以后直接就生成签名后的apk文件,免得生成apk以后还要自己去签一次名。
    这里我偷个懒,直接用tinker-sample-android中的签名吧,签名放这里。
    这里写图片描述

    我们先来配置下签名,在android{}中做以下配置:

    android{
        ...
        // 这里做签名的配置
        signingConfigs {
            release {
                try {
                    storeFile file("./keystore/release.keystore")
                    storePassword "testres"
                    keyAlias "testres"
                    keyPassword "testres"
                } catch (ex) {
                    throw new InvalidUserDataException(ex.toString())
                }
            }
    
            debug {
                storeFile file("./keystore/debug.keystore")
            }
        }
        ...
    }

    然后我们再指定下构建的种类:

    android {
        ...
        buildTypes {
            release {
                minifyEnabled true
                signingConfig signingConfigs.release
                proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
            }
            debug {
                debuggable true
                minifyEnabled false
                signingConfig signingConfigs.debug
            }
        }
        ...
    }

    还记得我们添加了Google提供的分包小助手吗,我们需要开启这个分包的功能:

    android {
        ...
        defaultConfig {
            ...
            multiDexEnabled true // 开启dex分包
            ...
        }
    }

    好的,以上就把android{}中的东西配置的差不多了,接下来我们需要配置点其他东西,具体配置可以参照官方的build.gradle,不过官方配置的东西太多了,但我没打算配置这么多东西,所以来看看我们的版本(其实也挺多),直接上码?

    def bakPath = file("${buildDir}/bakApk/")
    
    /**
     * you can use assembleRelease to build you base apk
     * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
     * add apk from the build/bakApk
     */
    ext {
        // 是否使用Tinker
        tinkerEnabled = true
        // 原先编译好的APK包
        tinkerOldApkPath = "${bakPath}/app-debug-1018-17-32-47.apk"
        // 使用原APK的混淆方式
        tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
        // R文件,跟原APK的R做对比,以便知道哪些资源文件做了变化
        tinkerApplyResourcePath = "${bakPath}/app-debug-1018-17-32-47-R.txt"
        // 这里是APK风味构建啥的(是否为支付应用等等),如果APK不涉及这些东西,可以忽略
        tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
    }
    
    def getTinkerEnabled(){
        ext.tinkerEnabled
    }
    def getTinkerApplyMappingPath() {
        ext.tinkerOldApkPath
    }
    def getTinkerApplyResourcePath(){
        ext.tinkerApplyResourcePath
    }
    
    if (ext.tinkerEnabled) {
        apply plugin: 'com.tencent.tinker.patch'
    
        // 全局信息相关的配置项
        tinkerPatch {
    
            // 是否打开tinker的功能。
            tinkerEnable = getTinkerEnabled()
    
            // 基准apk包的路径,必须输入,否则会报错。
            oldApk = "${bakPath}/app-debug-0704-16-30-41.apk"
    
            // 选填,用于编译补丁apk路径。如果路径合法,即不再编译新的安装包,使用oldApk与newApk直接编译。
    //        newApk =
    
            // 选填,设置编译输出路径。默认在build/outputs/tinkerPatch中
    //        outputFolder = null
    
            // 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
            // 1. minSdkVersion小于14,但是dexMode的值为"raw";
            // 2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
            // 3. 定义在dex.loader用于加载补丁的类不在main dex中;
            // 4. 定义在dex.loader用于加载补丁的类出现修改;
            // 5. resources.arsc改变,但没有使用applyResourceMapping编译。
    //        ignoreWarning = false
    
            // 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。
            useSign = true
    
            // 编译相关的配置项
            buildConfig {
    
                // 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。
                applyMapping = getTinkerApplyMappingPath()
    
                // 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
                applyResourceMapping = getTinkerApplyResourcePath()
    
                // 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
                tinkerId = "tinkerdemo"
    
                // 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
                keepDexApply = false
    
                // 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
                isProtectedApp = true
    
                // 是否支持新增非export的Activity(1.9.0新加的功能)
                supportHotplugComponent = false
    
            }
    
            // dex相关的配置项
            dex {
                // 只能是'raw'或者'jar'。
                // 对于'raw'模式,我们将会保持输入dex的格式。
                // 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
                dexMode = "jar"
    
                // 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
                pattern = ["classes*.dex",
                           "assets/secondary-dex-?.jar"]
    
                // 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
                // 这里需要定义的类有:
                // 1. 你自己定义的Application类;
                // 2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
                // 3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
                // 4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中。或者你需要将这个类变成非preverify。
                // 5. 使用1.7.6版本之后的gradle版本,参数1、2会自动填写。若使用newApk或者命令行版本编译,1、2依然需要手动填写
                loader = [
                        // 不会修改的类放在这里,格式如下
                        // "com.qxf.tinkerdemo.BuildConfig"
                ]
            }
    
            // lib相关的配置项
            lib {
                // 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...
                pattern = ["lib/*/*.so"]
            }
    
            // res相关的配置项
            res {
                // 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...,务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
                pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
    
                // 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
                ignoreChange = ["assets/sample_meta.txt"]
    
                // 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
                largeModSize = 100
            }
    
            // 用于生成补丁包中的'package_meta.txt'文件
            packageConfig {
                // configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。
                configField("patchMessage", "tinker is sample to use")
                /**
                 * just a sample case, you can use such as sdkVersion, brand, channel...
                 * you can parse it in the SamplePatchListener.
                 * Then you can use patch conditional!
                 */
                configField("platform", "all")
                /**
                 * patch version via packageConfig
                 */
                configField("patchVersion", "1.0")
            }
            //or you can add config filed outside, or get meta value from old apk
            //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
            //project.tinkerPatch.packageConfig.configField("test2", "sample")
    
            // 7zip路径配置项,执行前提是useSign为true
            sevenZip {
                // 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用。
                zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
                // 系统中的7za路径,例如"/usr/local/bin/7za"。path设置会覆盖zipArtifact,若都不设置,将直接使用7za去尝试。
                // path = "/usr/local/bin/7za"
            }
        }
    
        List<String> flavors = new ArrayList<>();
        project.android.productFlavors.each { flavor ->
            flavors.add(flavor.name)
        }
        boolean hasFlavors = flavors.size() > 0
        def date = new Date().format("MMdd-HH-mm-ss")
    
        /**
         * bak apk and mapping
         */
        android.applicationVariants.all { variant ->
            /**
             * task type, you want to bak
             */
            def taskName = variant.name
    
            tasks.all {
                if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
    
                    it.doLast {
                        copy {
                            def fileNamePrefix = "${project.name}-${variant.baseName}"
                            def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
    
                            def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                            from variant.outputs.first().outputFile
                            into destPath
                            rename { String fileName ->
                                fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                            }
    
                            from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                            into destPath
                            rename { String fileName ->
                                fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                            }
    
                            from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                            into destPath
                            rename { String fileName ->
                                fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                            }
                        }
                    }
                }
            }
        }
        project.afterEvaluate {
            //sample use for build all flavor for one time
            if (hasFlavors) {
                task(tinkerPatchAllFlavorRelease) {
                    group = 'tinker'
                    def originOldPath = ext.tinkerBuildFlavorDirectory
                    for (String flavor : flavors) {
                        def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                        dependsOn tinkerTask
                        def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                        preAssembleTask.doFirst {
                            String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                            project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                            project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                            project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
    
                        }
    
                    }
                }
    
                task(tinkerPatchAllFlavorDebug) {
                    group = 'tinker'
                    def originOldPath = ext.tinkerBuildFlavorDirectory
                    for (String flavor : flavors) {
                        def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                        dependsOn tinkerTask
                        def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                        preAssembleTask.doFirst {
                            String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                            project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                            project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                            project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                        }
    
                    }
                }
            }
        }
    }

    还是挺多的对不?我知道你要CV大法了。

    简单配置

    按照以上步骤,我相信你应该已经配置完毕了,接下来我们就要开始使用了,如何在代码中使用这些东西呢。

    在Tinker里面,有一个叫着ApplicatioLike的东西,这个东西,看着名字挺像Application的让人怀疑它是Application的子类,其实不是的,不过这个类你完全可以把它当作一个Application来使用,好吧,我们来使用一下吧。
    首先我们创建一个类MyApplicationLike来继承Application,OK,里面要怎么写呢?

    @DefaultLifeCycle(
            // 将 要自动生成的application的类,指定生成位置和类名
            application = "com.qxf.tinkerdemo.MyApplication",
            flags = ShareConstants.TINKER_ENABLE_ALL,
            //loaderClassName, 我们这里使用默认即可!
            loaderClass = "com.tencent.tinker.loader.TinkerLoader",
            loadVerifyFlag = false)
    public class MyApplicationLike extends ApplicationLike {
    
        public MyApplicationLike(
                Application application,
                int tinkerFlags,
                boolean tinkerLoadVerifyFlag,
                long applicationStartElapsedTime,
                long applicationStartMillisTime,
                Intent tinkerResultIntent) {
    
            super(
                    application,
                    tinkerFlags,
                    tinkerLoadVerifyFlag,
                    applicationStartElapsedTime,
                    applicationStartMillisTime,
                    tinkerResultIntent);
        }
    
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
        public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
            getApplication().registerActivityLifecycleCallbacks(callback);
        }
    
    }

    (上面的代码,别光顾着复制到自己的项目中就完了,记得看一下,有注释的= =)

    然后我们build以下项目,我们会发现项目中的这里,自动生成了一个类:
    这里写图片描述
    既然现在有application了,那我们就在AndroidManifest.xml中做一些配置吧。

    首先加上两个权限,内存读写权限,至于Android版本大于6.0的手机,记得要做一下动态授权,以免到时候找不到补丁包:

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    然后加上自动生成的application:

    <application
            android:name=".MyApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <!--顺便加上这个服务,tinker自带的-->
            <service android:name="com.tencent.tinker.lib.service.DefaultTinkerResultService" />
        </application>

    既然application已经配置完毕了,我们再回头来看看MyApplicationLike这个类,这个类里面要写一些东西,来表示我们不止会配置,还会用。
    我们要重写onBaseContextAttached方法,那就来吧:

        @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
        @Override
        public void onBaseContextAttached(Context base) {
            super.onBaseContextAttached(base);
    
            // 分包需要的东西
            MultiDex.install(base);
    
            // LoadReporter类定义了Tinker在加载补丁时的一些回调
            LoadReporter loadReporter = new DefaultLoadReporter(getApplication());
            // PatchReporter类定义了Tinker在修复或者升级补丁时的一些回调
            PatchReporter patchReporter = new DefaultPatchReporter(getApplication());
            // PatchListener类是用来过滤Tinker收到的补丁包的修复、升级请求,也就是决定我们是不是真的要唤起:patch进程去尝试补丁合成。
            PatchListener patchListener = new DefaultPatchListener(getApplication());
            // UpgradePatch类是用来升级当前补丁包的处理类,一般来说你也不需要复写它。
            AbstractPatch upgradePatchProcessor = new UpgradePatch();
    
            TinkerInstaller.install(this,
                    loadReporter, patchReporter, patchListener,
                    DefaultTinkerResultService.class, upgradePatchProcessor);
    
        }

    老实说,其实这里的也算配置= =

    不过到这里到时候,我们的Tinker,简单的配置,基本就都完成了。

    实践

    现在我们要开始正式使用tinker了。
    tinker的使用过程(概念):

     1. 运行含有bug的app
     2. 加载补丁
     3. 重启应用
     4. bug消失
    

    我们无法判断应用是否有bug,因为上线的时候肯定是感觉各个地方都没有问题才上线的吧(汗!)

    我们这样来测试吧,MainActivity做两个按钮,一个加载补丁的按钮,一个测试按钮。嗯,说干就干!

    界面搭建:
    这里写图片描述
    界面就这么简单了
    点击事件的逻辑如下:

        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.load:
                    // 加载补丁
                    TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                            Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
                    break;
                case R.id.test:
                    show.setText("测试结果:"+"bug");
                    break;
            }
        }

    接下来我们将这个打包成APK,在这里打包
    这里写图片描述
    打包完毕后,在这里会生成一个apk
    这里写图片描述
    将这个apk安装到手机上,然后点击一下测试,好吧,你们已经猜到答案了:
    这里写图片描述

    然后我们来修复以下这个伪bug,就这样修复吧

        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.load:
                    // 加载补丁
                    TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                            Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
                    break;
                case R.id.test:
                    show.setText("测试结果:"+"bug已被修复");
                    break;
            }
        }

    我们把生成的刚刚生成的APK的名字,复制到app下的build.gradle中oldApk这里,像这样
    这里写图片描述
    然后我们利用tinker的打包工具tinkerPatchDebug生成补丁
    这里写图片描述
    生成的补丁将会来到这里
    这里写图片描述

    接下来重点来了,我们要将这个文件放到手机的指定位置/storage/emulated/0
    (毕竟是个demo,就不把补丁放到服务器上,然后从服务器下载到手机上再加载补丁了),所以我就直接通过adb命令直接把文件push到手机上了。

    push命令这里大概说一下:

    adb push 电脑上的文件的路径 手机路径

    这里写图片描述
    这里顺便说一个你们可能已经知道了的快捷键,选中studio中的文件,crtl+shift+c 可以直接复制文件的绝对路径

    好了,现在我们重新打开应用看看怎么样了,重新打开应用,点击加载补丁,加载完补丁后,点击测试,还是有bug,不过重启应用后,再次点击,我们将会看到,bug已经被修复了
    这里写图片描述

    tinker的简单使用差不多就是这样,各种配置完毕后,得到补丁,加载补丁,重启应用,bug消失。

    常见问题

    在使用tinker的时候,可能会遇到一些小问题,我这里大概说几个:

    1. 找不到补丁文件
      多半都是你没有加读写权限,可能你加了,但是由于你手机是Android6.0以上,所以需要动态获取权限

    2. tinkerId is not set!!!
      如果按照我的方式来,应该不会出现这些问题,出现这个问题的主要原因多半都是按照官方说明接入的,由于官方获取tinkerId是通过得到git提交的提交码得到的,所以只需要把代码通过git提交一次就可以得到tinkerId了,不过我不喜欢这样做,因为像我这种写一行代码都可能会提交的人来说,tinkerId会一直变动。

    3. Annotation processors must be explicitly declared now.
      有可能在使用Tinker的时候,会报这个错,解决方案也很简单:

    javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }

    将这句代码,加入android{}下的defaultConfig{}里面即可

    加固
    这里多提一句,isProtectedApp,关于这个属性,如果要使用加固的功能,需要把这个值设置为true,搬一下官方的话就是:
    tinker 1.7.8 可以通过 isProtectedApp 开启加固支持,这种模式仅仅可以使用在加固应用中。
    支持加固的厂商有:

    腾讯云·乐固
    爱加密
    梆梆加固
    360加固(需要5月8号后的加固版本)

    其他 请自行测试,只要满足下面规则的都可以支持
    这里是否支持加固,需要加固厂商明确以下两点:
    1.不能提前导入类;
    2.在art平台若要编译oat文件,需要将内联取消。

    其他常见问题或许你可以在官方常见问题这里找到答案。

    最后

    Tinker提供的东西不止我这里写的这么简单,大家可以参考Tinker自定义扩展,其实Tinker还是很灵活的,推荐大概看下自定义扩展里面的内容,然后在把官方的例子tinker-sample-android下载下来好好看看官方是这么实现的,相信你能够了解到更多的东西。

    展开全文
  • Tinker的简单使用(超级详细)

    千次阅读 2016-11-21 23:21:24
    第一步:下载tinker的demo(可以自己先跑通) 出现问题可以去官网wike去看 下面说接入tinker,跑一个简单的demo 1、在项目目录的build.gradle文件里面引入 buildscript { dependencies { classpath ('...

    第一步:下载tinker的demo(可以自己先跑通) 出现问题可以去官网wike去看

    下面说接入tinker,跑一个简单的demo

    1、在项目目录的build.gradle文件里面引入

    buildscript {
        dependencies {
            classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5')
        }
    }
    2、在你的app的build.gradle文件中加入

    dependencies {
        //optional, help to generate the final application 
        provided('com.tencent.tinker:tinker-android-anno:1.7.5')
        //tinker's main Android lib
        compile('com.tencent.tinker:tinker-android-lib:1.7.5') 
    }
    3、简单、粗暴的做法可以略过2,直接用app的build.gradle,编译会缺少keystore文件夹,和keep_in_main_dex.txt文件

    4、写你的application类,并完成tinker的初始化(注意在mainfest.xml文件中注册你的applicatin),以下是官方推荐

    @DefaultLifeCycle(
            application = "tinker.sample.android.app.SampleApplication",             //application name to generate
            flags = ShareConstants.TINKER_ENABLE_ALL)                                //tinkerFlags above
    public class MyApp extends DefaultApplicationLike {
        public MyApp(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent, Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {
            super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent, resources, classLoader, assetManager);
        }
    
        @Override
        public void onBaseContextAttached(Context base) {
            super.onBaseContextAttached(base);
            TinkerInstaller.install(this);
        }
    }
    
    5、一定不要忘记在mainfest.xml文件中定义sd卡的读写权限;

    6、剩下的就是你自己定义不同的代码玩tinker了,可以参考https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97;

    以上内容仅仅是tinker的一个超级简单demo

    祝大家身体健康


    展开全文
  • Tinker是腾讯出的一款热修复框架,关于热修复框架Tinker介绍本文就不描述太多了,网络已经很多了 本文主要介绍主要基础的集成使用,本文主要使用Gradle方式集成 Tinker的github地址,详细介绍可以看wiki ...
  • Tinker 使用

    2019-03-18 10:45:36
    Tinker时微信推出的热修复框架,优点就是很稳定,可以gradle打包,缺点是这个修复不是实时的,需要重启。这是由于其实现的原理决定的,简单的说他是通过把生成的不定apk,加载进来,通过与基线apk的整合生成新的dex...
  • 1、配置tinker-id,保证唯一性 2、执行assembleRelease 3、查看生成的基准包信息 二、补丁包生成 1、修改baseApkDir,必须和基准包的路径保持一致,重要的事情说三遍:和基准包的路径保持一致,和基准包的路径保持...
  • Tinker 接入

    2018-07-24 15:11:38
    一.原理 二.使用步骤 1.工程根目录的build.gradle中添加...在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖: buildscript { repositories { mavenLocal() google() jcenter() } depend...
  • Tinker热修复

    千次阅读 2019-12-17 17:18:27
    Sophix,Tinker简介: https://www.cnblogs.com/ldq2016/p/10449433.html Tinker: 安装过程https://blog.csdn.net/qq_22393017/article/details/82110343 Tinker和QQ空间方案最大的不同是,Tinker 下发新旧DEX的...
  • 项目中我们总会遇到这样的问题刚刚发布版本就发现了一个严重错误,对用户的使用体验非常的差,所以需要立马更新....Tinker就是为了解决这种问题而生的, 修改少量的代码,生成差分包,然后用户下载非常小...
  • Tinker 是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。选择使用Tinker的原因大概就是因为这句话吧:Tinker已运行在微信的数亿Android设备上...
  • 因为app 需要集成热修复框架,对比几个成熟的解决方案后,最终选用微信的解决方案 Tinker ,具体文档请看 https://github.com/Tencent/tinker/wiki 最终为了方便,使用了腾讯的bugly, 具体集成方式请看 ...
  • Tinker集成和使用(一)Tinker简介和运行Tinker示例出现的问题Tinker出来有一段时间了,我也是第一时间开始使用这个框架,感觉真的很好用,而且支持动态根性的类型也很多,当然还有它的多平台的兼容性,毕竟是在微信...
  • 热更新Tinker研究(三):加载补丁

    千次阅读 2017-04-20 19:35:51
    热更新Tinker研究-加载补丁 本文主要讲解Tinker加载patch.apk的过程,主要是研究当把patch_signed_7zip.apk推送到sdcard之后,点击LOAD PATCH按钮之后的流程分析。
  • Tinker board开发板是由国际知名IT厂商华硕ASUS出品的精品开源硬件项目,兼容了树莓派的生太链,可以运行树莓派的系统,同时,华硕还专门支持了很多第三方的知名系统,同时华硕基于debian高度定制了一款tinker OS,专...
  • 解决Tinker Exception:createDelegate failed

    千次阅读 2018-12-13 14:38:26
    项目之前是用的是继承application,之前用tinker也没什么问题,最近,升级tinker到1.9.9, 加了 provided('com.tencent.tinker:tinker-android-anno:1.9.9') compile('...
  • com.tinker.app:patch E/Tinker.ParallelDex: Failed to optimize dex: /data/user/0/com.tinker.app/tinker/patch-985c8c6e/dex/oat java.io.IOException: No original dex files found for dex location /data/...
  • Bugly补丁下发后出现以上异常, 原因 升级sdk和tinker-support插件不对应。https://bugly.qq.com/docs/release-notes/release-android-beta/?v=20171212190105 看对应关系。
  • Tinkerboard(tinkerboardS)有3组串口 (点击图片可以放大)UART1:PIN8/PIN10UART3 : PIN36/PIN37UART4 : PIN13/PIN15驱动已经都写好了,只需要调用相应节点,我们附件放出一个安卓串口工具,可以直接在安桌下调用...
  • Android - Tinker编译patch时候报错

    千次阅读 2018-12-10 14:50:05
    转载请声明,本文来自: 错误: log太长,放在文末, 分析: 解决方案: Check for loader classes in dex: classes.dex Check for loader classes in dex: classes2.dex Warning:ignoreWarning is false, but we ...
1 2 3 4 5 ... 20
收藏数 5,977
精华内容 2,390
关键字:

tinker