微信Tinker热修复接入及操作流程

微信Tinker热修复接入及操作流程

阿里百川HotFix,RocooFix,及Tinker,多方对比,选择Tinker

接入流程

  1. 先配置远程依赖,(目前最新的版本是1.7.5)
    Project的Gradle中添加:
        dependencies {
            classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5')
        }

Module的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')
    }

接下来还需要在其中添加一些Tinke插件:


    /**Tinker 相关配置----------------开始-----------------------------------*/
        def bakPath = file("${buildDir}/bakApk/")
        def gitSha() {
        try {
        String gitRev = '11.2.3.4'
        if (gitRev == null) {
            throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
        }
        return gitRev
        } catch (Exception e) {
            throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
            }
        }
        ext {
            //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
            //Tinker是否可用
        tinkerEnabled = false
        //for normal build
        //old apk file to build patch apk 旧包名
        tinkerOldApkPath = "${bakPath}/tinker_demo-debug-1121-17-43-24.apk"
        //proguard mapping file to build patch apk  旧包混淆文件目录
        tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
        //resource R.txt to build patch apk, must input if there is resource changed 旧包R文件
        tinkerApplyResourcePath = "${bakPath}/tinker_demo-debug-1121-17-43-24-R.txt"
        //only use for build all flavor, if not, just ignore this field  多渠道
        tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
        }
        def getOldApkPath() {
            return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
        }
        def getApplyMappingPath() {
            return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
        }
        def getApplyResourceMappingPath() {
            return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
        }
        def getTinkerIdValue() {
            return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
        }
        def buildWithTinker() {
            return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
        }
        def getTinkerBuildFlavorDirectory() {
            return ext.tinkerBuildFlavorDirectory
        }
        if (buildWithTinker()) {
            apply plugin: 'com.tencent.tinker.patch'
            tinkerPatch {
                /**
                 * 基准apk包路径,也就是旧包路径
                 * */
                oldApk = getOldApkPath()
                /**
                 * 如果出现以下的情况,并且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 = getApplyMappingPath()
            /**
             * 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的
             * 大小,同时也避免由于ResId改变导致remote view异常。
             * */
            applyResourceMapping = getApplyResourceMappingPath()
            /**
             * 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些
             * 基准包上面,一般来说我们可以使用git版本号、versionName等等。
             * */
            tinkerId = getTinkerIdValue()
        }
        dex {
            /**
             * 只能是'raw'或者'jar'。
             * 对于'raw'模式,我们将会保持输入dex的格式。
             * 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式
             * ,而且它更省存储空间,但是验证md5时比'raw'模式耗时()
             * */
            dexMode = "jar"
            /**
             * 是否提前生成dex,而非合成的方式。这套方案即回退成Qzone的方案,对于需要使用加固或者多flavor打包(建
             * 议使用其他方式生成渠道包)的用户可使用。但是这套方案需要插桩,会造成Dalvik下性能损耗以及Art补丁包可
             * 能过大的问题,务必谨慎使用。另外一方面,这种方案在Android N之后可能会产生问题,建议过滤N之后的用户。
             */
            usePreGeneratedPatchDex = false
            /**
             * 需要处理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。
             */
            loader = ["com.tencent.tinker.loader.*",
                      //warning, you must change it with your application
                    //TODO 换成自己的Application
                      "com.material.hbl.tinker_demo.MyApplication",
            ]
        }
        lib {
            /**
             * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/...
             */
            pattern = ["lib/armeabi/*.so"]
        }
        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
        }
        packageConfig {
            /**
             * configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动
             * 写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。
             */
            configField("patchMessage", "tinker is sample to use")
        }
        sevenZip {
            /**
             * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用
             */
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
        /**
         *  文件名                              描述
         *  patch_unsigned.apk                  没有签名的补丁包
         *  patch_signed.apk                  签名后的补丁包
         *  patch_signed_7zip.apk              签名后并使用7zip压缩的补丁包,也是我们通常使用的补丁包。但正式发布的时候,最好不要以.apk结尾,防止被运营商挟持。
         *  log.txt                              在编译补丁包过程的控制台日志
         *  dex_log.txt                          在编译补丁包过程关于dex的日志
         *  so_log.txt                          在编译补丁包过程关于lib的日志
         *  tinker_result                      最终在补丁包的内容,包括diff的dex、lib以及assets下面的meta文件
         *  resources_out.zip                  最终在手机上合成的全量资源apk,你可以在这里查看是否有文件遗漏
         *  resources_out_7z.zip              根据7zip最终在手机上合成的全量资源apk
         *  tempPatchedDexes                  在Dalvik与Art平台,最终在手机上合成的完整Dex,我们可以在这里查看dex合成的产物。
         *
         *
         * */

        /**
         * 获得所有渠道集合,并判断数量
         */
        List<String> flavors = new ArrayList<>();
        project.android.productFlavors.each {flavor ->
            flavors.add(flavor.name)
        }
        boolean hasFlavors = flavors.size() > 0
        /**
         * bak apk and mapping
         *  创建Task并执行文件操作
         */
        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"
                        }

                    }
                }
            }
        }
        }
        }
        /**Tinker 相关配置----------------结束-----------------------------------*/

代码很多,在每一个的配置上面都有相应的注释,比较关注的事ext中的配置,和Loader中的配置。

  1. 创建一个SampleApplication类,继承DefaultApplicationLike:
    @DefaultLifeCycle(application = "com.material.hbl.tinkerdemo.MyApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
        public class SampleApplicarion extends DefaultApplicationLike{
            public SampleApplicarion(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);
            }
            @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
            @Override
            public void onBaseContextAttached(Context base) {
                super.onBaseContextAttached(base);
        /************************Action*************************/
                TinkerInstaller.install(this);
        /************************Action*************************/
            }
            @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
            public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
                getApplication().registerActivityLifecycleCallbacks(callback);
            }
            }

其中DefaultLifeCycle中的Application为我们真正的Application,flags表示需要支持的修复的范围,然后将AndroidManifest.xml文件中的Application的name改为我们的Application,添加读写磁盘的权限。重点只有标示的那一句,初始化Tinker。

  1. 将上面的Gradle配置中的loader中的Application改为之前我们的Application。至此接入工作基本配置完成。

操作流程

  1. 在将Tinker成功接入以后,可以写一个简单的例子来检测一下是否可以使用。在MainActivity中创建四个按钮,分别对应Load,Show,Bug,Fix
    ,其中load对应加载补丁,show会显示一些加载的信息,Bug为简单创建一个bug,Fix为修复此bug.布局就不贴了,直接看代码:
        public class MainActivity extends AppCompatActivity {
        String TAG="MainActivity";
        TextView tv;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Log.e(TAG, "i am on onCreate string:" );
        //        Log.e(TAG, "i am on patch onCreate");
                tv= (TextView) findViewById(R.id.title);
                findViewById(R.id.btn_load).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        //加载补丁的方法
                        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
                        Log.d(TAG, "onClick: load");
                    }
                });
                findViewById(R.id.btn_show).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        showInfo(MainActivity.this);
                    }
                });
                findViewById(R.id.btn_bug).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Log.d(TAG, "onClick: btn_bug");
                        tv.setText("出现BUG");
                    }
                });
        //        findViewById(R.id.btn_fix).setOnClickListener(new View.OnClickListener() {
        //            @Override
        //            public void onClick(View v) {
        //                Log.d(TAG, "onClick: btn_fix");
        //                tv.setText("修复BUG");
        //            }
        //        });
        }

showInfo使用的是官方的Demo中的方法,不过多介绍了,也可以简单的使用:

         Tinker tinker = Tinker.with(getApplicationContext());
         boolean isLoadSuccess = tinker.isTinkerLoaded();

方法来判断是否补丁加载成功。

  1. 将Gradle中ext中的tinkerEnabled置为true,然后点击studio顶部的Build中Build APk,打一个DeBug包,成功之后会在Module的build目录下生成一个文件夹,如图:

因为是Debug包所以只会有两个文件,一个是APK,一个是对应的R文件。如果有使用混淆的话,还会有一个mapping的混淆映射文件。
我们把outputs/apk/中的apk文件拷出去,并安装到自己的手机上,运行。

  1. 将Gradle中ext中的tinkerOldApkPath后的路径设置为上一步中bakApk中的apk名称,同理把tinkerApplyResourcePath设置为R文件的名称。其他的不用管。
  2. 修改MainActivity中代码,将注释解开,并将 Log.e(TAG, "i am on onCreate string:" );注释掉。
  3. 点击Studio右侧的Gradle工具栏,展开之后找到自己的Module找到tinker,双击其中的tinkerPathDeBug,运行之后会在


outputs下多出一个目录,都是一些编译打差异包过程的log和相关生成的apk,选择其中的patch_signed_7zip.apk,拷出来,放在手机根目录下。

  1. 重新启动app,点击load,之后点击show,会提示patch load success,重启apk,点击fix就会发现,bug已经被修复了。

遇到的问题

  1. 在执行tinkerPatchDebug的时候失败,提示其他Module中的ext中文件不存在,将其他module中的tinkerenable关闭。

待优化

  1. tinkerId 目前是写死的
  2. Application 其他初始化的代码。

文章持续更新,不断会更新一些在实际使用中遇到的问题和解决办法。

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

推荐阅读更多精彩内容