Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

热更新Tinker研究(六):TinkerPatchPlugin

$
0
0

热更新Tinker研究(一):运行tinker-sample-android
热更新Tinker研究(二):结合源码学习Dex格式
热更新Tinker研究(三):加载补丁
热更新Tinker研究(四):TinkerLoader
热更新Tinker研究(五):Application的隔离
热更新Tinker研究(六):TinkerPatchPlugin
热更新Tinker研究(七):Dex的patch文件生成
热更新Tinker研究(八):res和so的patch文件生成
热更新Tinker研究(九):Dex文件的patch
热更新Tinker研究(十):Res文件的patch
热更新Tinker研究(十一):so文件的patch

热更新Tinker研究(六):TinkerPatchPlugin

在我们运行tinkerPatchDebug或者tinkerPatchRelease任务的时候,会执行TinkerPatchPlugin的apply(),实际上编写一个gradle的task只需要继承Plugin即可。

整个插件做了以下几件事,

Created with Raphaël 2.1.0开始初始化构建TinkerPatchSchemaTask构建TinkerManifestTask构建TinkerResourceIdTask构建proguardConfigTask构建TinkerMultidexConfigTask结束

整个插件构造的task情况如下图:

enter description here

部分过程已经省略,左边蓝色的部分是原来就存在的task,右边绿色的task是插件构建的task。

一、初始化

1,创建一些扩展属性

project的扩展tinkerPatch

        project.extensions.create('tinkerPatch', TinkerPatchExtension)

TinkerPatchExtension的相关源码如下,它的字段就是支持我们去配置的。

public class TinkerPatchExtension {
    /**
     * Specifies the old apk path to diff with the new apk
     */
    String oldApk

    /**
     * If there is loader class changes,
     * or Activity, Service, Receiver, Provider change, it will terminal
     * if ignoreWarning is false
     * default: false
     */
    boolean ignoreWarning

    /**
     * If sign the patch file with the android signConfig
     * default: true
     */
    boolean useSign

    /**
     * whether use tinker
     * default: true
     */
    boolean tinkerEnable

    ......
}

创建tinkerPatch的扩展

tinkerPatch的扩展属性设置如下,

        project.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, project)

        project.tinkerPatch.extensions.create('dex', TinkerDexExtension, project)
        project.tinkerPatch.extensions.create('lib', TinkerLibExtension)
        project.tinkerPatch.extensions.create('res', TinkerResourceExtension)
        project.tinkerPatch.extensions.create('packageConfig', TinkerPackageConfigExtension, project)
        project.tinkerPatch.extensions.create('sevenZip', TinkerSevenZipExtension, project)

具体属性的每个设置类,这里不进行讨论。

2,检测和设置相关属性

检查是否包含com.android.application插件

        //判断需要android插件
        if (!project.plugins.hasPlugin('com.android.application')) {
            throw new GradleException('generateTinkerApk: Android Application plugin required')
        }

设置jumboMode和preDexLibaries属性

            //open jumboMode  增加字符串和方法的容量
            android.dexOptions.jumboMode = true

            //close preDexLibraries
            try {
                android.dexOptions.preDexLibraries = false
            } catch (Throwable e) {
                //no preDexLibraries field, just continue
            }

jumboMode设置为true,会允许更多的字符串个数。preDexLibraries会预编译一些libraries,为了更好的提升增量编译的速度,但是clean之后的编译就会变得比较慢。这里preDexLibraries设置为false。

检查是否开启了instantRun

由于插件不支持instantRun,所以这里会进行检测。

                def instantRunTask = getInstantRunTask(project, variantName)
                if (instantRunTask != null) {
                    throw new GradleException(
                            "Tinker does not support instant run mode, please trigger build"
                                    + " by assemble${variantName} or disable instant run"
                                    + " in 'File->Settings...'."
                    )
                }

二、TinkerPatchSchemaTask

TinkerPatchSchemaTask的作用主要是生成patch,由于依赖于assemble任务,会在assemble任务之后执行。这里 InputParam.Builder 会负责接收一系列参数,包含oldApkPath,newApkPath,loaderPattern等等。

        InputParam.Builder builder = new InputParam.Builder()
        if (configuration.useSign) {
            if (signConfig == null) {
                throw new GradleException("can't the get signConfig for this build")
            }
            builder.setSignFile(signConfig.storeFile)
                    .setKeypass(signConfig.keyPassword)
                    .setStorealias(signConfig.keyAlias)
                    .setStorepass(signConfig.storePassword)

        }

        builder.setOldApk(configuration.oldApk)
               .setNewApk(buildApkPath)
               .setOutBuilder(outputFolder)
               .setIgnoreWarning(configuration.ignoreWarning)
               .setDexFilePattern(new ArrayList(configuration.dex.pattern))
               .setDexLoaderPattern(new ArrayList(configuration.dex.loader))
               .setDexMode(configuration.dex.dexMode)
               .setSoFilePattern(new ArrayList(configuration.lib.pattern))
               .setResourceFilePattern(new ArrayList(configuration.res.pattern))
               .setResourceIgnoreChangePattern(new ArrayList(configuration.res.ignoreChange))
               .setResourceLargeModSize(configuration.res.largeModSize)
               .setUseApplyResource(configuration.buildConfig.usingResourceMapping)
               .setConfigFields(new HashMap(configuration.packageConfig.getFields()))
               .setSevenZipPath(configuration.sevenZip.path)
               .setUseSign(configuration.useSign)

然后由Runner负责具体的生成patch工作,与之相关的类有ApkDecoder、PatchInfo和PatchBuilder。

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

ApkDecoder主要负责对比oldApk和newApk,并生成对应的文件,这里是进行dex,so,res对比,生成差分包的关键步骤buzhou。PatchInfo主要负责由前面得到的信息,来生成package_meta.txt。PatchBuilder负责将patch.apk从tempPath拷贝到最终的路径,并且进行签名,以及利用7-zip去压缩文件。

三、TinkerManifestTask

TinkerManifestTask主要是负责处理manifest相关的工作,被processResources所依赖,所以在执行processResources之前会执行TinkerManifestTask。下面是TinkerManifestTask的taskAction的部分源码。

    @TaskAction
    def updateManifest() {
        // Parse the AndroidManifest.xml
        String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId
        if (tinkerValue == null || tinkerValue.isEmpty()) {
            throw new GradleException('tinkerId is not set!!!')
        }

        tinkerValue = TINKER_ID_PREFIX + tinkerValue

        project.logger.error("tinker add ${tinkerValue} to your AndroidManifest.xml ${manifestPath}")

        writeManifestMeta(manifestPath, TINKER_ID, tinkerValue)
        addApplicationToLoaderPattern()
        File manifestFile = new File(manifestPath)
        if (manifestFile.exists()) {
            FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))
            project.logger.error("tinker gen AndroidManifest.xml in ${MANIFEST_XML}")
        }

    }

这里主要也是做了两件事情,一个是将TINKER_ID写入到manifest,二是将当前的Application类,添加到LoaderPattern。LoaderPattern的作用主要是负责标记哪些类属于Loader主体类,不能用于热更新。

四、TinkerResourceIdTask

TinkerResourceIdTask也是在processResources之前执行,主要是负责处理资源文件的id,以保证和baseApk中资源id一致,这样的话在java层的代码dex文件中会做较小的改动。其中控制资源id的两个最重要的文件是ids.xml和public.xml。aapt会根据这两个文件去分配id。

  @TaskAction
    def applyResourceId() {
        String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping

        // Parse the public.xml and ids.xml
        if (!FileOperation.isLegalFile(resourceMappingFile)) {
            project.logger.error("apply resource mapping file ${resourceMappingFile} is illegal, just ignore")
            return
        }
        String idsXml = resDir + "/values/ids.xml";
        String publicXml = resDir + "/values/public.xml";
        FileOperation.deleteFile(idsXml);
        FileOperation.deleteFile(publicXml);
        List resourceDirectoryList = new ArrayList()
        resourceDirectoryList.add(resDir)

        project.logger.error("we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}")
        project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true
        Map> 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}")
        }
    }

首先是根据tinkerPatch中buildConfig中指定的applyResourceMapping文件,applyResourceMapping是baseApk的R.txt的记录。去解析这个文件,然后根据这个规则生成新的ids.xml和public.xml,从而保证资源文件的id变化不大。

五、proguardConfigTask

proguardConfigTask在transformClassesAndResourcesWithProguardFor之前执行,它的作用是对于一些特殊混淆规则做处理。
对loader的关键类不进行混淆,

    static final String 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"

也会对tinkerPatch中dex的loader属性中设置的类进行保留,具体代码如下:

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

六、TinkerMultidexConfigTask

TinkerMultidexConfigTask在multidexTask之前执行,换句话说,multidexTask是依赖TinkerMultidexConfigTask的。它的主要职责是保证和loader相关的类必须要在mainDex文件中。其原理就是去在maindexlist中添加一些规则,和混淆相关的内容类似,这里也有一个固定的需要keep的类的设置。

    static final String MULTIDEX_CONFIG_SETTINGS =
            "-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"

然后后面也会写入的配置的loader的相关类,把他们写入到maindexlist中去。

 @TaskAction
    def updateTinkerProguardConfig() {
        File file = project.file(MULTIDEX_CONFIG_PATH)
        project.logger.error("try update tinker multidex keep proguard file with ${file}")

        // Create the directory if it doesn't exist already
        file.getParentFile().mkdirs()

        StringBuffer lines = new StringBuffer()
        lines.append("\n")
             .append("#tinker multidex keep patterns:\n")
             .append(MULTIDEX_CONFIG_SETTINGS)
             .append("\n")
             .append("#your dex.loader patterns here\n")

        Iterable 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")
        }

        // Write our recommended proguard settings to this file
        FileWriter fr = new FileWriter(file.path)
        try {
            for (String line : lines) {
                fr.write(line)
            }
        } finally {
            fr.close()
        }

        File multiDexKeepProguard = null
        try {
            multiDexKeepProguard = applicationVariant.getVariantData().getScope().getManifestKeepListProguardFile()
        } catch (Throwable ignore) {
            try {
                multiDexKeepProguard = applicationVariant.getVariantData().getScope().getManifestKeepListFile()
            } catch (Throwable e) {
                project.logger.error("can't find getManifestKeepListFile method, exception:${e}")
            }
        }
        if (multiDexKeepProguard == null) {
            project.logger.error("auto add multidex keep pattern fail, you can only copy ${file} to your own multiDex keep proguard file yourself.")
            return
        }
        FileWriter manifestWriter = new FileWriter(multiDexKeepProguard, true)
        try {
            for (String line : lines) {
                manifestWriter.write(line)
            }
        } finally {
            manifestWriter.close()
        }
    }

七、给project注入一些任务

主要是给transform为DexTransform的TransformTask注入ImmutableDexTransform。ImmutableDexTransform是DexTransform的装饰类,相当于对DexTransform的功能进行补充。

      DexTransform dexTransform = task.transform
                            ImmutableDexTransform hookDexTransform = new ImmutableDexTransform(project,
                                    variant, dexTransform)
                            project.logger.info("variant name: " + variant.name)

                            Field field = TransformTask.class.getDeclaredField("transform")
                            field.setAccessible(true)
                            field.set(task, hookDexTransform)

这里面主要做的事,包含得到oldDexList,处理mainDexListFile,得到一些映射关系,生成jar,dex文件等等,这里不展开讨论。

作者:huweigoodboy 发表于2017/4/20 13:58:43 原文链接
阅读:44 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>