Tinker学习之旅(一)--- Demo接入Tinker

前言:

当下市面上比较流行的热修复技术有很多,其中比较出名的有阿里的AndFix、美团的Robust以及腾讯的Tinker。在这里我选取Tinker作为学习对象,除了它最为强大的功能外,尤其喜欢Tinker官方文档中的一句话“Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?”。哈哈下面就开始我们的Tinker学习之旅。

Tinker是什么?

Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。

为什么使用Tinker?

当前市面的热补丁方案有很多,其中比较出名的有阿里的AndFix、美团的Robust以及QZone的超级补丁方案。但它们都存在无法解决的问题,这也是正是我们推出Tinker的原因。下面我们来看一张图,就可以知道Tinker的强大之处。
几种流行的热修复技术对比图

Tinker的已知问题:

由于原理与系统限制,Tinker有以下已知问题:

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件

  2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;

  3. 在Android N上,补丁对应用启动时间有轻微的影响;

  4. 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";

  5. 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

上面的介绍摘选自Tinker官方文档,如需要更加详细文档,访问:Tinker官方地址。在对Tinker有个简单了解后,下面我们就开始在项目中一步步集成Tinker了。

  1. Gradle文件配置

(1)在项目根目录下的gradle.properties文件中添加Tinker版本属性(对tinker的版本信息统一管理,方便后续版本升级维护)如下所示:

TINKER_VERSION = 1.9.1

(2)在项目根目录下的build.gradle文件中添加“tinker-gradle-plugin ”。

  dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        classpath ("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")

    }

(3)在app目录下的build.gradle文件中添加依赖:

 //tinker自定义注解库,生成application时使用   只参与编译,不参与打包
    provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    //tinker核心sdk库     参与编译与打包
    compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    //支持分包操作
    compile 'com.android.support:multidex:1.0.1'

(4)根据Tinker官方Demo在app目录下build.gradle文件中配置信息:

def bakPath = file("${buildDir}/bakApk/") //指定基准文件存放位置
ext {
    tinkerEnable = true
    tinkerOldApkPath = "${bakPath}/"
    tinkerID = "1.0"
    tinkerApplyMappingPath = "${bakPath}/"
    tinkerApplyResourcePath = "${bakPath}/"
}

def buildWithTinker() {
    return ext.tinkerEnable
}

def getOldApkPath() {
    return ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return ext.tinkerID
}

def getTinkerBuildFlavorDirectory(){
    return ext.tinkerBuildFlavorDirectory
}

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

    //所有tinker相关的参数配置
    tinkerPatch {

        oldApk = getOldApkPath() //指定old apk文件路径

        ignoreWarning = false   //不忽略tinker的警告,有警告则中止patch文件的生成

        useSign = true  //强制patch文件也使用签名

        tinkerEnable = buildWithTinker(); //指定是否启用tinker

        buildConfig {

            applyMapping = getApplyMappingPath()  //指定old apk打包时所使用的混淆文件

            applyResourceMapping = getApplyResourceMappingPath()  //指定old apk的资源文件

            tinkerId = getTinkerIdValue() //指定TinkerID

            keepDexApply = false
        }

        dex {

            dexMode = "jar" //jar、raw
            pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目录
            loader = ["androidjian.tinker.MyTinkerApplication"] //指定加载patch文件时用到的类
        }

        lib {

            pattern = ["libs/*/*.so"]
        }

        res {

            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            //指定tinker可以修改的所有资源路径

            ignoreChange = ["assets/sample_meta.txt"] //指定不受影响的资源路径

            largeModSize = 100 //资源修改大小默认值
        }

        packageConfig {

            configField("patchMessage", "fix the 1.0 version's bugs")

            configField("patchVersion", "1.0")
        }
    }

    //判断当前是否配置多渠道
    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0

    /**
     * 复制基准包和其它必须文件到指定目录
     */
    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")
                        }
                    }
                }
            }
        }
    }
}
  1. 创建TinkerManager类,对所有Tinker相关的API进行封装,(减少对项目的侵入,方便后续维护)。
public class TinkerManager {

    //标记是否安装过Tinker
    private static boolean isInstalled =  false;

    private static ApplicationLike mAppLike;

    /**
     * 完成tinker的初始化
     * @param applicationLike
     */
    public static void installTinker(ApplicationLike applicationLike) {
        mAppLike = applicationLike;
        if (isInstalled){
            return;
        }

        //完成Tinker的初始化
        TinkerInstaller.install(mAppLike);
        isInstalled = true;
    }

    /**
     * 完成patch文件的加载
     * @param path
     */
    public static void loadPatch(String path){
        if (Tinker.isTinkerInstalled()){
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
        }
    }

    private static Context getApplicationContext(){
        if (mAppLike != null){
            return mAppLike.getApplication().getApplicationContext();
        }
        return null;
    }
}
  1. 新建TinkerApplicationLike 类,继承自DefaultApplicationLike:
@DefaultLifeCycle(application = ".MyTinkerApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class TinkerApplicationLike extends DefaultApplicationLike{
    public TinkerApplicationLike(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);
        MultiDex.install(base);
        TinkerManager.installTinker(this);
    }
}

注意这里我们自定义的application类的名称为MyTinkerApplication,记得在manifest清单文件中的application节点下添加name属性为MyTinkerApplication。然后我们还需要重写onBaseContextAttached()方法,在该方法中完成Tinker的初始化操作。

到此为止,tinker的集成操作已经完成了。接下来我们来验证一下。

1.首先我们要生成基准APK:

在命令行工具中输入gradlew assemblerelease命令,运行完毕后将old APK push到我们的手机上。打开app目录下的build文件夹,如下图所示:
app目录下的build文件夹
结合之前build.gradle文件配置操作,我们看到tinker为我们生成了bakApk文件夹,在该文件夹下存放了基准文件相关信息。
  1. 配置基准文件信息

在1中我们看到tinker为我们生成了基准文件信息,接下来我们需要在build.gradle文件中配置基准文件信息,为后续生成patch文件做准备。操作如下:

ext {
    tinkerEnable = true
    tinkerOldApkPath = "${bakPath}/app-release-0722-20-04-03.apk"
    tinkerID = "1.0"
    tinkerApplyMappingPath = "${bakPath}/app-release-0722-20-04-03-mapping.txt"
    tinkerApplyResourcePath = "${bakPath}/app-release-0722-20-04-03-R.txt"
}
  1. 改动代码,模拟项目更新或者bug修复
    由tinker的官方文档可知,tinker支持动态下发代码、So库以及资源,它的功能不仅仅限于修改bug操作。old APK中只放置了一个按钮,负责加载指定目录下的patch文件。这里我在布局文件中新增了一个textView,text为“hello tinker”。

  2. 生成patch补丁文件

    我们打开Android Studio右侧面板的Gradle,可以看到如下:
    Android Studio右侧面板的Gradle
    这个时候,我们双击tinkerPatchRelease,如果不出意外的话,补丁文件就生成完毕了。结果如下:
    patch_signed.apk

    patch_signed.apk就是我们最终要用到的补丁文件。

  3. 将patch补丁文件push到手机指定目录,进行验证。

    在patch补丁文件加载之前,界面中只有一个按钮,如下图:
    old APK界面
    我们点击按钮,tinker会完成指定目录下补丁文件的加载,这个时候在按钮的下方会出现我们新增的textview,我们来看下结果:
    加载patch文件后

    可以看到tinker已经成功集成到我们的项目中了。

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

推荐阅读更多精彩内容