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 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已经成功集成到我们的项目中了。

你可能感兴趣的:(Tinker学习之旅(一)--- Demo接入Tinker)