Tinker 接入

Tinker 热补丁接入过程中的坑!!!

===============

Tinker 介绍

官方接入说明

image

gradle 接入

gradle是推荐的接入方式,在gradle插件tinker-patch-gradle-plugin中我们帮你完成proguard、multiDex以及Manifest处理等工作。

添加gradle依赖

在项目的根目录build.gradle中,添加tinker-patch-gradle-plugin的依赖

这里写图片描述

引入tinker 核心库

然后在baseUI-lib文件的build.gradle,我们需要添加tinker的库依赖以及apply tinker的gradle插件.

在APP/build.gradle 下面添加tinker 的配置文件

keep_in_main_dex.txt 文件内容就是指定你要放置到主DEX 中的类

-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {
*;
}

-keep public class * extends com.tencent.tinker.loader.TinkerLoader {
*;
}

-keep public class * extends com.tencent.tinker.loader.app.TinkerApplication {
*
}

-keep class com.tencent.tinker.loader.** {
*;
}

-keep class com.anzogame.corelib.GameApplication {
*;
}

加入tinker EXT提供配置文件

tinkerEnabled: tinker 的开关
tinkerOldApkPath : 生补丁包的基础apk,线上包版本
tinkerApplyMappingPath : 线上包混淆文件生成mapping 文件
tinkerApplyResourcePath : 线上包的资源id 文件
以上版本是我们每一个正式版本需要保留的生成热补丁的基础信息
tinkerBuildFlavorDirectory : 多渠道用到,我们不用,下面说到我们怎么处理多渠道打包

加入 TinkerPatch 配置


if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        /**
         * necessary,default 'null'
         * the old apk path, use to diff with the new apk to build
         * add apk from the build/bakApk
         */
        oldApk = getOldApkPath()
        /**
         * optional,default 'false'
         * there are some cases we may get some warnings
         * if ignoreWarning is true, we would just assert the patch process
         * case 1: minSdkVersion is below 14, but you are using dexMode with raw.
         *         it must be crash when load.
         * case 2: newly added Android Component in AndroidManifest.xml,
         *         it must be crash when load.
         * case 3: loader classes in dex.loader{} are not keep in the main dex,
         *         it must be let tinker not work.
         * case 4: loader classes in dex.loader{} changes,
         *         loader classes is ues to load patch dex. it is useless to change them.
         *         it won't crash, but these changes can't effect. you may ignore it
         * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
         */
        ignoreWarning = false

        /**
         * optional,default 'true'
         * whether sign the patch file
         * if not, you must do yourself. otherwise it can't check success during the patch loading
         * we will use the sign config with your build type
         */
        useSign = true

        /**
         * Warning, applyMapping will affect the normal android build!
         */
        buildConfig {
            /**
             * optional,default 'null'
             * if we use tinkerPatch to build the patch apk, you'd better to apply the old
             * apk mapping file if minifyEnabled is enable!
             * Warning:
             * you must be careful that it will affect the normal assemble build!
             */
            applyMapping = getApplyMappingPath()
            /**
             * optional,default 'null'
             * It is nice to keep the resource id from R.txt file to reduce java changes
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * necessary,default 'null'
             * because we don't want to check the base apk with md5 in the runtime(it is slow)
             * tinkerId is use to identify the unique base apk when the patch is tried to apply.
             * we can use git rev, svn rev or simply versionCode.
             * we will gen the tinkerId in your manifest automatic
             */
            tinkerId = "1.2"
        }

        dex {
            /**
             * optional,default 'jar'
             * only can be 'raw' or 'jar'. for raw, we would keep its original format
             * for jar, we would repack dexes with zip format.
             * if you want to support below 14, you must use jar
             * or you want to save rom or check quicker, you can use raw mode also
             */
            dexMode = "jar"
            /**
             * optional,default 'false'
             * if usePreGeneratedPatchDex is true, tinker framework will generate auxiliary class
             * and insert auxiliary instruction when compiling base package using
             * assemble{Debug/Release} task to prevent class pre-verified issue in dvm.
             * Besides, a real dex file contains necessary class will be generated and packed into
             * patch package instead of any patch info files.
             *
             * Use this mode if you have to use any dex encryption solutions.
             *
             * Notice: If you change this value, please trigger clean task
             * and regenerate base package.
             */
            usePreGeneratedPatchDex = false
            /**
             * necessary,default '[]'
             * what dexes in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             */
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            /**
             * necessary,default '[]'
             * Warning, it is very very important, loader classes can't change with patch.
             * thus, they will be removed from patch dexes.
             * you must put the following class into main dex.
             * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
             * own tinkerLoader, and the classes you use in them
             *
             */
            loader = ["com.tencent.tinker.loader.*",
                      //warning, you must change it with your application
                      "com.anzogame.corelib.GameApplication",
                      "com.anzogame.corelib.BuildConfig.BaseBuildInfo"
            ]
        }

        lib {
            /**
             * optional,default '[]'
             * what library in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             * for library in assets, we would just recover them in the patch directory
             * you can get them in TinkerLoadResult with Tinker
             */
            pattern = ["lib/armeabi/*.so"]
        }

        res {


            /**
             * optional,default '[]'
             * what resource in apk are expected to deal with tinkerPatch
             * it support * or ? pattern.
             * you must include all your resources in apk here,
             * otherwise, they won't repack in the new apk resources.
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * optional,default '[]'
             * the resource file exclude patterns, ignore add, delete or modify resource change
             * it support * or ? pattern.
             * Warning, we can only use for files no relative with resources.arsc
             */
//            ignoreChange =  ["*.png"]
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * default 100kb
             * for modify resource, if it is larger than 'largeModSize'
             * we would like to use bsdiff algorithm to reduce patch file size
             */
            largeModSize = 100
        }

        packageConfig {
            /**
             * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
             * package meta file gen. path is assets/package_meta.txt in patch file
             * you can use securityCheck.getPackageProperties() in your ownPackageCheck method
             * or TinkerLoadResult.getPackageConfigByName
             * we will get the TINKER_ID from the old apk manifest for you automatic,
             * other config files (such as patchMessage below)is not necessary
             */
            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")

        /**
         * if you don't use zipArtifact or path, we just use 7za to try
         */
        sevenZip {
            /**
             * optional,default '7za'
             * the 7zip artifact path, it will use the right 7za with your platform
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
            /**
             * optional,default '7za'
             * you can specify the 7za path yourself, it will overwrite the zipArtifact value
             */
//        path = "/usr/local/bin/7za"
        }
    }

    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name
        def date = new Date().format("MMdd-HH-mm-ss")

        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.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 = getTinkerBuildFlavorDirectory()
                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 = getTinkerBuildFlavorDirectory()
                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"
                    }

                }
            }
        }
    }
  }

详细参数参看文档 传送门

配置就到这里,然后执行assembleDebug,安装apk,千万不要 RUN ,RUN方式生成是补丁打补丁的,编译不通过 彩蛋....(Too many classes in --main-dex-list, main dex capacity exceeded)

为什么会这样子呢 ? 我们已经采用了GOOGLE 的方案多DEX ,从报错上来看应该是主DEX 的类太多了,超过了限制,但是这个哪些类放到主DEX 不是我们决定的啊,很操蛋, 那么我们来看系统是如何分包的.

在项目中,可以直接运行 gradle 的 task 。

  • collect{flavor}{buildType}MultiDexComponents Task 。这个 task 是获取 AndroidManifest.xml 中 Application 、Activity 、Service 、 Receiver 、 Provider 等相关类,以及 Annotation ,之后将内容写到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 文件中去。

  • packageAll{flavor}DebugClassesForMultiDex Task 。该 task 是将所有类打包成 jar 文件存在 build/intermediates/multi-dex/{flavor}/debug/allclasses.jar 。 当 BuildType 为 Release 的时候,执行的是 proguard{flavor}Release Task,该 task 将 proguard 混淆后的类打包成 jar 文件存在 build/intermediates/classes-proguard/{flavor}/release/classes.jar

  • shrink{flavor}{buildType}MultiDexComponents Task 。该 task 会根据 maindexlist.txt 生成 componentClasses.jar ,该 jar 包里面就只有 maindexlist.txt 里面的类,该 jar 包的位置在 build/intermediates/multi-dex/{flavor}/{buildType}/componentClasses.jar

  • create{flavor}{buildType}MainDexClassList Task 。该 task 会根据生成的 componentClasses.jar 去找这里面的所有的 class 中直接依赖的 class ,然后将内容写到 build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 中。最终这个文件里面列出来的类都会被分配到第一个 dex 里面。

通过上面的流程我们可以得出 ,我们主DEX 中的类取决于build/intermediates/multi-dex/{flavor}/{buildType}/maindexlist.txt 中的内容 ,那么我们在执行MultiDexComponents task 时候做些拦截,把Activity 从主DEX中移除,这里面的移除不是全部移除,如果Activity中包含有子类,那么我们的移除是无效,还是会被放入到主DEX,另外,如果你 Application 、Service 、 Receiver 、 Provider 中的直接引用类还是会被放到第一个主DEX中。

当我们采用多DEX 的时候,应用启动的首先回加载主DEX ,其他的 dex 需要我们在应用启动后进行动态加载安装, 通过MultiDex.install(getApplication());加载其他DEX. 这样的方法虽然可行,我们把 很多 Activity 放到其他的DEX 中了 ,可是生成包发现DEX 还是有9.2M 这种方式如果随着代码增加还是有可能导致主DEX 超限的问题,这时候我们可以采用dexnkife 通过配置形式把Application 中相关的类和一些必要的首页类放置到主DEX 中,可以准确控制哪些放入到主DEX,这样也可以解决问题。

那么Google 官方Multidex是如何加载的呢?

Google 官方支持 Multidex 的 jar 包是 android-support-multidex.jar,该 jar 包从 build tools 21.1 开始支持。这个 jar 加载 apk 中的从 dex 流程如下:
[图片上传失败...(image-14a17f-1530665215929)]

此处主要的工作就是从 apk 中提取出所有的从 dex(classes2.dex,classes3.dex,…),然后通过反射依次安装加载从 dex 并合并 DexPathList 的 Element 数组。

为什么API 21 以上就没有主DEX 过大的问题呢?

  • 这是为了5.0以上系统在安装过程中的art阶段就将所有的classes(..N).dex合并到一个单独的oat文件(5.0以下只能苦逼的启动时加载 对于Art相关知识,可以参考老罗的系列文章 传送门

DEX类分包的规则

我们开启多DEX支持一般是指定了multiDexEnabled,系统其实它利用的是Android sdk build tool中的mainDexClasses脚本,这在版本21以上才会有。使用方法非常很简单:

mainDexClasses [--output <output file>] <application path>
该脚本要求输入一个文件组(包含编译后的目录或jar包),然后分析文件组中的类并写入到–output所指定的文件中。实现原理也不复杂,主要分为三步:
a. 环境检查,包括传入参数合法性检查,路径检查以及proguard环境检测等。
b. 使用mainDexClasses.rules规则,通过Proguard的shrink功能,裁剪无关类,生成一个tmp.jar包。
c. 通过生成的tmp jar包,调用MainDexListBuilder类生成主dex的文件列表。

这里只是简单的得到所有入口类(即rules中的Instrumentation、application、Activity、Annotation等等)的直接引入类。何为直接引用类?在init过程,会在校验阶段去resolve它各个方法、变量引用到的类,这些类统称为某个类的直接引用类。举个栗子:

public class MainActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
            DirectReferenceClass test = new DirectReferenceClass();
    }
}

public class DirectReferenceClass {
    public DirectReferenceClass() {
        InDirectReferenceClass test = new InDirectReferenceClass();
    }
}

public class InDirectReferenceClass {
    public InDirectReferenceClass() {
    }
}

上面有MainActivity、DirectReferenceClass、InDirectReferenceClass三个类,其中DirectReferenceClass是MainActivity的直接引用类,InDirectReferenceClass是DirectReferenceClass的直接引用类。而InDirectReferenceClass是MainActivity的间接引用类(即直接引用类的所有直接引用类)。

对于5.0以下的系统,我们需要在启动时手动加载其他的dex。而我们并没有要求得到所有的间接引用类,这是因为我们在attachBaseContext的时候,已将其他dex加载。

事实上,若我们在attachBaseContext中调用Multidex.install,我们只需引入Application的直接引用类即可,mainDexClasses将Activity、ContentProvider、Service等的直接引用类也引入,主要是满足需要在非attachBaseContent加载多dex的需求。另一方面,若存在以下代码,将出现NoClassDefFoundError错误。

public class HelloMultiDexApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
         DirectReferenceClass test = new DirectReferenceClass();
        MultiDex.install(this);
    }
}

这是因为在实际运行过程中,DirectReferenceClass需要的InDirectReferenceClass并不一定在主dex。解决方法是手动将该类放于dx的-main-dex-list参数中:

afterEvaluate {
    tasks.matching {
        it.name.startsWith('dex')
    }.each { dx ->
        if (dx.additionalParameters == null) {
            dx.additionalParameters = []
        }
        dx.additionalParameters += '--multi-dex'
        dx.additionalParameters += "--main-dex-list=$projectDir/<filename>".toString()
    }
}

LinearAlloc 是什么

  • LinearAlloc 主要用来管理 Dalvik 中 class 加载时的内存,就是让 App 在执行时减少系统内存的占用。在 App 的安装过程中,系统会运行一个名为 dexopt 的程序为该应用在当前机型中运行做准备。dexopt 使用 LinearAlloc 来存储应用的方法信息。App 在执行前会将 class 读进 LinearAlloc 这个 buffer 中,这个 LinearAlloc 在 Android 2.3 之前是 4M 或 5M ,到 4.0 之后变为 8M 或 16M。因为 5M 实在是太小了,可能还没有 65536 就已经超过 5M 了,什么意思呢,就是只有一个包的情况下也有可能出现 INSTALL_FAILED_DEXOPT ,原因就在于 LinearAlloc。

解决 LinearAlloc

DEXOPT && DEX2OAT 是什么?

image

dexopt
当 Android 系统安装一个应用的时候,有一步是对 Dex 进行优化,这个过程有一个专门的工具来处理,叫 DexOpt。DexOpt 是在第一次加载 Dex 文件的时候执行的,将 dex 的依赖库文件和一些辅助数据打包成 odex 文件,即 Optimised Dex,存放在 cache/dalvik_cache 目录下。保存格式为 apk路径 @ apk名 @ classes.dex 。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多。

更多可查看 Dalvik Optimization and Verification With dexopt

dex2oat

Android Runtime 的 dex2oat 是将 dex 文件编译成 oat 文件。而 oat 文件是 elf 文件,是可以在本地执行的文件,而 Android Runtime 替换掉了虚拟机读取的字节码转而用本地可执行代码,这就被叫做 AOT(ahead-of-time)。dex2oat 对所有 apk 进行编译并保存在 dalvik-cache 目录里。PackageManagerService 会持续扫描安装目录,如果有新的 App 安装则马上调用 dex2oat 进行编译。

更多可查看 Android运行时ART简要介绍和学习计划

Application Not Responding

因为第一次运行(包括清除数据之后)的时候需要 dexopt ,然而 dexopt 是一个比较耗时的操作,同时 MultiDex.install() 操作是在 Application.attachBaseContext() 中进行的,占用的是UI线程。那么问题来了,当我的第二个包、第三个包很大的时候,程序就阻塞在 MultiDex.install() 这个地方了,一旦超过规定时间,那就 ANR 了。那怎么办?放子线程?如果 Application 有一些初始化操作,到初始化操作的地方的时候都还没有完成 install + dexopt 的话,那又会 NoClassDefFoundError 了吗?同时 ClassLoader 放在哪个线程都让主线程挂起。

微信/手Q加载方案

对于微信来说,我们一共有111052个方法。以线性内存3355444(限制5m,给系统预留部分)、方法数64K为限制,即当满足任意一个条件时,将拆分dex。由此微信将得到一个主dex,两个子dex,若微信采用Android方案,在首次启动时将长期无响应(没有出现黑屏时因为默认皮肤的原因),这对处女座的我来说是无法接受的。应该如何去做?微信与手Q的方案是类似的,将首次加载放于地球中,并用线程去加载(但是5.0之前加载dex时还是会挂起主线程)。

Dex形式
暂时我们还是放于assets下,以assets/secondary-program-dex-jars/secondary-N.dex.jar命名。为什么不以classes(..N).dex?这是因为一来觉得以Android的推广速度,5.0用户增长应该是遥遥无期的,二来加载Dex的代码,传进去的是zip,在加载前我们需要验证MD5,确保所加载的Dex没有被篡改(Android官方没有验证,主要是只有root才能更改吧)。

/**

  • Makes an array of dex/resource path elements, one per element of
  • the given array.
    */
    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
    ArrayList<IOException> suppressedExceptions) {
    事实上,应该传进去的是dex也是应该可以的,这块在下一个版本将采用classes(..N).dex。但是如果我们使用了线程加载,并且弹出提示界面,对用户来说并不是无法接受。

Dex类分包的规则
分包规则即将所有Application、ContentProvider以及所有export的Activity、Service、Receiver的间接依赖集都必须放在主dex。对于微信现在来说,这部分大约有41306个方法,每次通过扫描AndroidMifest计算耗时大约为20s不到。怎么计算?可以参考buck或者mainDexClasses的做法。

public MainDexListBuilder(String rootJar, String pathString) throws IOException {
path = new Path(pathString);
ClassReferenceListBuilder mainListBuilder=new ClassReferenceListBuilder(path);
加载Dex的方式
加载逻辑这边主要判断是否已经dexopt,若已经dexopt,即放在attachBaseContext加载,反之放于地球中用线程加载。怎么判断?其实很低级,因为在微信中,若判断revision改变,即将dex以及dexopt目录清空。只需简单判断两个目录dex名称、数量是否与配置文件的一致。

(name md5 校验是否加载成功类)
secondary-1.dex.jar 63e5240eac9bdb5101fc35bd40a98679 secondary.dex01.Canary
secondary-2.dex.jar e7d2a4a181f579784a4286193feaf457 secondary.dex02.Canary
总的来说,这种方案用户体验较好,缺点在于太过复杂,每次都需重新扫描依赖集,而且使用的是比较大的间接依赖集(要真正运行到,直接依赖集是不行的)。当前微信必要的依赖集已经41306个方法,说不定哪一天就爆了。

FaceBook加载方案

那是否存在一种加载方式它的依赖集很小,但却不会像官方方案一样造成明显的卡顿?逆过不少app,发现facebook的思路还是挺不错的,下面作一个简单的说明:

Dex形式
微信与facebook的dex形式是完全一致的,这是因为我们也是使用facebook开源工具buck编译的。但是我们做了一个自动生成buck脚本的工作,即开发人员无须关心buck脚本如何编写。

Dex类分包的规则
facebook将加载Dex的逻辑放于单独的nodex进程,这是一个非常简单、轻量级的进程。它没有任何的ContentProvider,只有有限的几个Activity、Service。

<activity android:exported="false" android:process=":nodex"
android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
所以依赖集为Application、NodexSplashActivity的间接依赖集即可,而且这部分逻辑应该相对稳定,我们无须做动态扫描。这就实现了一个非常轻量级的依赖集方案。

加载Dex的方式
加载dex逻辑也非常简单,由于NodexSplashActivity的intent-filter指定为Main与LAUNCHER。首先拉起nodex进程,然后初始化NodexSplashActivityActivity,若此时Dex已经初始化过,即直接跳转到主页面。

image

这种方式好处在于依赖集非常简单,同时首次加载Dex时也不会卡死。但是它的缺点也很明显,即每次启动主进程时,都需先启动nodex进程。尽管nodex进程逻辑非常简单,这也需100ms以上。若微信对启动时间非常敏感,很难会去采用这个方案。

美团加载方案

传送门

在 gradle 生成 dex 文件的这步中,自定义一个 task 来干预 dex 的生产过程,从而产生多个 dex 。

tasks.whenTaskAdded { task ->
    if (task.name.startsWith('proguard') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
        task.doLast {
            makeDexFileAfterProguardJar();
        }
        task.doFirst {
            delete "${project.buildDir}/intermediates/classes-proguard";

            String flavor = task.name.substring('proguard'.length(), task.name.lastIndexOf(task.name.endsWith('Debug') ? "Debug" : "Release"));
            generateMainIndexKeepList(flavor.toLowerCase());
        }
    } else if (task.name.startsWith('zipalign') && (task.name.endsWith('Debug') || task.name.endsWith('Release'))) {
        task.doFirst {
            ensureMultiDexInApk();
        }
    }
}

把 Service、Receiver、Provider 涉及到的代码都放到主 dex 中,而把 Activity 涉及到的代码进行了一定的拆分,把首页 Activity、Laucher Activity 、欢迎页的 Activity 、城市列表页 Activity 等所依赖的 class 放到了主 dex 中,把二级、三级页面的 Activity 以及业务频道的代码放到了第二个 dex 中,为了减少人工分析 class 的依赖所带了的不可维护性和高风险性,美团编写了一个能够自动分析 class 依赖的脚本, 从而能够保证�主 dex 包含 class 以及他们所依赖的所有 class 都在其内,这样这个脚本就会在打包之前自动分析出启动到主 dex 所涉及的所有代码,保证主 dex 运行正常。

加载 dex 的方式

通过分析 Activity 的启动过程,发现 Activity 是由 ActivityThread 通过 Instrumentation 来启动的,那么是否可以在 Instrumentation 中做一定的手脚呢?通过分析代码 ActivityThread 和 Instrumentation 发现,Instrumentation 有关 Activity 启动相关的方法大概有:execStartActivity、 newActivity 等等,这样就可以在这些方法中添加代码逻辑进行判断这个 class 是否加载了,如果加载则直接启动这个 Activity,如果没有加载完成则启动一个等待的 Activity 显示给用户,然后在这个 Activity 中等待后台第二个 dex 加载完成,完成后自动跳转到用户实际要跳转的 Activity;这样在代码充分解耦合,以及每个业务代码能够做到颗粒化的前提下,就做到第二个 dex 的按需加载了。

美团的这种方式对主 dex 的要求非常高,因为第二个 dex 是等到需要的时候再去加载。重写Instrumentation 的 execStartActivity 方法,hook 跳转 Activity 的总入口做判断,如果当前第二个 dex 还没有加载完成,就弹一个 loading Activity等待加载完成。

引入dexnkife 核心库

dexnkife 项目地址: DexKnifePlugin.

dexnkife 帮助我们划分类到主DEX ,使DEX 划分通过配置形式来完成

使用 # 进行注释, 当行起始加上 #, 这行配置被禁用.

# 全局过滤, 如果没设置 -filter-suggest 并不会应用到 建议的maindexlist.
# 如果你想要某个包路径在maindex中,则使用 -keep 选项,即使他已经在分包的路径中.
-keep android.support.v4.view.**

# 这条配置可以指定这个包下类在第二dex中.
android.support.v?.**

# 使用.class后缀,代表单个类.
-keep android.support.v7.app.AppCompatDialogFragment.class

# 不包含Android gradle 插件自动生成的miandex列表.
-donot-use-suggest

# 将 全局过滤配置应用到 建议的maindexlist中, 但 -donot-use-suggest 要关闭.
-filter-suggest

# 不进行dex分包, 直到 dex 的id数量超过 65536.
-auto-maindex

# dex 扩展参数, 例如 --set-max-idx-number=50000
# 如果出现 DexException: Too many classes in --main-dex-list, main dex capacity exceeded,则需要调大数值
-dex-param --set-max-idx-number=50000

# 显示miandex的日志.
-log-mainlist

# 如果你只想过滤 建议的maindexlist, 使用 -suggest-split 和 -suggest-keep.
# 如果同时启用 -filter-suggest, 全局过滤会合并到它们中.
-suggest-split **.MainActivity2.class
-suggest-keep android.support.multidex.**

搞定分DEX ,继续集成Tinker

生成基础版本

首先执行gradle 任务中的assableRelease,release 环境下打包会输出到/home/anzogame/release/包名/版本/release/,同时会在release同级目录下生成bakApk/app-1209-15-34-25/_test/ 生成apk ,mapping ,R.txt 文件,该文件就是基础版本

生成补丁包

在build.grale 中将tinkerOldApkPath,tinkerApplyMappingPath,tinkerApplyResourcePath 替换生新生成apk,maping和R.txt 文件,
然后修改自己代码,注意要修改一行CoreApplicationLike 中的代码 ,可以加一个日志,因为1.7.5的tinker 有一个bug ,最好修改一下,然后执行
gradle 任务中的,tinkerPatchRelease 生成补丁文件,最后生成的补丁文件

输出文件详解

然后将生成补丁patch_signed_7zip.apk 重名 patch_signed_7zip.so 通过后台CMS 传到SERVER ,客户端安装基础版本,启动查看补丁是否安装成功。

/Tinker.DexDiffPatchInternal: success recover dex file: /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes.dex.jar, use time: 4118
12-09 16:21:34.900 12198-12228/? I/Tinker.DexDiffPatchInternal: try Extracting /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes2.dex.jar
12-09 16:21:36.416 12198-12228/? I/Tinker.DexDiffPatchInternal: isExtractionSuccessful: true
12-09 16:21:36.526 12198-12228/? I/Tinker.DexDiffPatchInternal: try Extracting /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes3.dex.jar
12-09 16:21:36.902 12198-12228/? I/Tinker.DexDiffPatchInternal: isExtractionSuccessful: true
12-09 16:21:36.932 12198-12228/? I/Tinker.DexDiffPatchInternal: try Extracting /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes4.dex.jar
12-09 16:21:36.976 12198-12228/? I/Tinker.DexDiffPatchInternal: isExtractionSuccessful: true
12-09 16:21:36.982 12198-12228/? I/Tinker.DexDiffPatchInternal: try Extracting /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/test.dex.jar
12-09 16:21:36.985 12198-12228/? I/Tinker.DexDiffPatchInternal: isExtractionSuccessful: true
12-09 16:21:49.737 12198-12228/? I/Tinker.DexDiffPatchInternal: recover dex result:true, cost:19018, isUpgradePatch:true
12-09 16:21:49.739 12198-12228/? W/Tinker.BsDiffPatchInternal: patch recover, library is not contained
12-09 16:21:49.746 12198-12228/? I/Tinker.ResDiffPatchInternal: res dir: /data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/res/, meta: resArscMd5:91dfea65855052958dc4cd3abd408550
                                                                arscBaseCrc:2718315645
                                                                pattern:res/.*
                                                                pattern:resources\.arsc
                                                                pattern:assets/.*
                                                                addedSet:assets/only_use_to_test_tinker_resource.txt
                                                                modifiedSet:res/layout/activity_patch.xml
12-09 16:21:49.797 12198-12228/? I/Tinker.ResDiffPatchInternal: no large modify resources, just return
12-09 16:21:53.353 12198-12228/? I/Tinker.ResDiffPatchInternal: final new resource file:/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/res/resources.apk, entry count:7596, size:39259384
12-09 16:21:53.353 12198-12228/? I/Tinker.ResDiffPatchInternal: recover resource result:true, cost:3613, isNewPatch:true
12-09 16:21:53.366 12198-12228/? W/Tinker.UpgradePatch: UpgradePatch tryPatch: done, it is ok
12-09 16:21:53.366 12198-12228/? I/Tinker.DefaultPatchReporter: patchReporter: patch all result path:/storage/emulated/0/AnZoLOL/patch/patch_signed_7zip.so, success:true, cost:22752, isUpgrade:true
12-09 16:21:53.367 12198-12228/? I/Tinker.PatchFileUtil: safeDeleteFile, try to delete path: /data/user/0/com.anzogame.lol/tinker/patch.retry
12-09 16:21:53.368 12198-12228/? I/Tinker.PatchFileUtil: safeDeleteFile, try to delete path: /data/user/0/com.anzogame.lol/tinker/temp.apk

看到如下日志,表示补丁合成成功,杀进程,重启应用,查看日志

TinkerLoader: tryLoadPatchFiles:isEnabledForResource:true
12-09 16:24:00.771 12420-12420/? D/Tinker.TinkerInternals: same fingerprint:Xiaomi/gemini/gemini:6.0.1/MXB48T/V8.1.1.0.MAACNDI:user/release-keys
12-09 16:24:00.775 12420-12420/? W/Tinker.TinkerLoader: tinker safe mode preferName:tinker_own_config_com.anzogame.lol count:0
12-09 16:24:00.780 12420-12420/? W/Tinker.TinkerLoader: after tinker safe mode count:1
12-09 16:24:00.780 12420-12420/? I/Tinker.TinkerDexLoader: classloader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.anzogame.lol-1/base.apk"],nativeLibraryDirectories=[/data/app/com.anzogame.lol-1/lib/arm, /data/app/com.anzogame.lol-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]]
12-09 16:24:00.806 12420-12420/? W/Tinker.ClassLoaderAdder: checkDexInstall result:true
12-09 16:24:00.807 12420-12420/? I/Tinker.TinkerDexLoader: after loaded classloader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes.dex.jar", zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes2.dex.jar", zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes3.dex.jar", zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/classes4.dex.jar", zip file "/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/dex/test.dex.jar", zip file "/data/app/com.anzogame.lol-1/base.apk"],nativeLibraryDirectories=[/data/app/com.anzogame.lol-1/lib/arm, /data/app/com.anzogame.lol-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]]
12-09 16:24:00.812 12420-12420/? E/Tinker.ResourcePatcher: checkResUpdate success, found test resource assets file only_use_to_test_tinker_resource.txt
12-09 16:24:00.813 12420-12420/? I/Tinker.ResourceLoader: monkeyPatchExistingResources resource file:/data/user/0/com.anzogame.lol/tinker/patch-3ce34da5/res/resources.apk, use time: 6
12-09 16:24:00.813 12420-12420/? I/Tinker.TinkerLoader: tryLoadPatchFiles: load end, ok!
12-09 16:24:00.845 12420-12420/? D/Tinker.DefaultAppLike: onBaseContextAttached:
12-09 16:24:00.853 12420-12420/? I/Tinker.SamplePatchListener: application maxMemory:256
12-09 16:24:00.858 12420-12420/? W/Tinker.Tinker: tinker patch directory: /data/user/0/com.anzogame.lol/tinker
12-09 16:24:00.863 12420-12420/? I/Tinker.TinkerLoadResult: parseTinkerResult loadCode:0
12-09 16:24:00.863 12420-12420/? I/Tinker.TinkerLoadResult: parseTinkerResult oldVersion:, newVersion:3ce34da54e4e3ec20c9f8868a8970db0, current:3ce34da54e4e3ec20c9f8868a8970db0
12-09 16:24:00.863 12420-12420/? I/Tinker.TinkerLoadResult: oh yeah, tinker load all success
12-09 16:24:00.863 12420-12420/? I/Tinker.DefaultLoadReporter: patch version change from  to 3ce34da54e4e3ec20c9f8868a8970db0
12-09 16:24:00.863 12420-12420/? I/Tinker.DefaultLoadReporter: try kill all other process
12-09 16:24:00.865 12420-12420/? I/Tinker.DefaultLoadReporter: patch load result, path:/data/user/0/com.anzogame.lol/tinker, code:0, cost:81

oh yeah, tinker load all success

补丁合成加载成功。

tinker 多渠道打包怎么处理?

tinker 本身是支持flavor 打包的:


加入上面的配置,执行assembleRelease task, 会在app/build/bakApk/目录下面生成所有flavor 中的渠道包.

接着修改代码和资源文件,执行tinkerPatchAllFlavorRelease 生成所有渠道的补丁包


然在后在手机上执行通渠道的补丁升级,可以正常升级,如果你用tencent渠道的包升级test 渠道的补丁包,就会失败,什么原因呢?查看tinker文档


额。。。。 实际使用中不可能对不同渠道进行补丁包的管理,多个渠道需要使用一个补丁包,那么我们就需要对我们现在有的打包方式进行修改,tinker 的建议方式原理和美团的快速打包方案类似,那么我们来看下美团的打包方案。

美团打包方案

传送门1
传送门2

第一步:

修改我们的多渠道flavors 打包方式去掉所有渠道,只剩下一个test渠道做为基础渠道,然后在启动APP的时候动态设置渠道值,例如可以用友盟提供的方式AnalyticsConfig.setChannel(ChannelUtil.getChannel(getCurrentActivity()));动态设置渠道值
获取渠道代码


第二步:

替换我们之前的获取渠道名称代码 AndroidApiUtils.getUmengChannel(Context context) , 改为
友盟提供的方式AnalyticsConfig.getChannel(getApplicationContext()) ,因为我们以前的代码是直接读取的
meta 中的与友盟渠道号


,查看友盟的代码,AnalyticsConfig.getChannel 是在渠道号不为空的情况下才会去读取meta 中的,我们打包方式是不会对AndroidManifest.xml 中的渠道号做替换的,只是内存中的channel 替换。

第三步:

打包生成apk ,在把生成APK 放到脚本同级的目录下面,进入目录执行python MultiChannelBuildTool.py
生成apk, 不到一分钟,所有渠道的APK 已经生成好了,channel.txt 是所有渠道的渠道列表。

下面是打包脚本:


按照美团的打包方式生成基础APK 多渠道APK 补丁包,经验证补丁可以正常运行。

以后发版本打包的流程, 执行gradle clean assembleRelease -PDEV_PACKET=false 任务,release 目录下面生成基础版本的APK ,然后在当前目录下面实行python MultiChannelBuildTool.py ,同时在release 同级目录下面会生成 bakApk/.../ 备份的apk ,mapping.txt, R.txt 文件。

Tinker 常用API

传送门

Tinker 自定义扩展

[传送门](file:///Users/zhulizhi/Downloads/Tinker-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%89%A9%E5%B1%95.html)

Tinker 常见问题

传送门

热补丁流程错误码定义:

1,下载失败上报
2,接口请求错误上报
3,MD5文件校验失败错误上报
5,文件格式校验错误上报
6,补丁合成失败上报
7,补丁合成成功上报

热补丁相关疑问?

tinker 热补丁和DroidPlug插件有什么区别?

tinker 是热更新工具 目前补丁不支持新增四大组件

DroidPlug 核心思想是hook 系统流程,占坑实现插件。

tinker 的资源是怎么修复的?

tinker 的DEX是怎么修复的?

tinker 的so是怎么修复的?

Tinker 相关文章

微信Tinker的一切都在这里,包括源码(一)

Android_N混合编译与对热补丁影响解析

微信Android热补丁实践演进之路

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容