转载

Android Tinker集成采坑

Android Tinker集成采坑

官方文档 https://github.com/Tencent/tinker/wiki

官方demo怎么配置都可以从demo中找到 https://github.com/Tencent/tinker/tree/dev/tinker-sample-android

Tinker提供了两种接入方式,命令行接入和gradle接入。正常的项目中都基本都使用gradle,一次配置好以后就可以很方便的使用了,所以本次只使用gradle方式。

本文基于1.9.13版本,因为有好几个地方都需要用到版本信息,所以将它放在gradle.properties文件中方便版本的管理

TINKER_VERSION=1.9.13

在总工程的的build.gradle配置tinker的classpath,因为tinker定义了一些自己的gradle脚本,后面在配置参数的时候会用到。

classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") {
            changing = TINKER_VERSION?.endsWith("-SNAPSHOT")
            exclude group: 'com.android.tools.build', module: 'gradle'
        }

然后在app的gradle文件中配置核心库和谷歌的分包库,现在的应用功能都很多所以体积很大一般都会用到multidex

//核心sdk库
api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }

//注解编译器,生成application的时候用
annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }

implementation "com.android.support:multidex:1.0.3"

先配置app的gradle文件中android这个标签下的内容

 //配置签名,这里使用demo中的签名文件,真实项目中替换成自己的
  signingConfigs {
       release {
           try {
               storeFile file("./keystore/release.keystore")
               storePassword "testres"
               keyAlias "testres"
               keyPassword "testres"
           } catch (ex) {
               throw new InvalidUserDataException(ex.toString())
           }
       }

       debug {
           storeFile file("./keystore/debug.keystore")
       }
   }
// 支持大工程模式
   dexOptions {
       jumboMode = true
   }
//release包开始混淆
  buildTypes {
       release {
           minifyEnabled true
           signingConfig signingConfigs.release
           proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
       }
       debug {
           debuggable true
           minifyEnabled false
           signingConfig signingConfigs.debug
       }
   }

然后开始配置tinker的参数, 官方指南上gradle参数详解 官方指南上有参数的详解,建议都看一遍,更容易知道参数的作用和应该怎么配置。

def bakPath = file("${buildDir}/bakApk/")

ext {
    //是否启用tinker
    tinkerEnabled = true
//每次打包完都需要更改下面的三个路径,如果支持多渠道打包,下面第四个参数也需要修改
    //old apk 的路径
    tinkerOldApkPath = "${bakPath}/app-release-0508-10-52-50.apk"
    //old apk 混淆 mapping 文件的路径
    tinkerApplyMappingPath = "${bakPath}/app-release-0508-10-52-50-mapping.txt"
    //old apk R文件的路径
    tinkerApplyResourcePath = "${bakPath}/app-release-0508-10-52-50-R.txt"

    //多渠道打包的路径
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

static def gitSha() {
//      每次打包的时候版本要一致,官方demo的是git的版本,这里使用versionName
        String gitRev = "1.0"
        return gitRev
}

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") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}
//判断是否启用tinker
if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'

    tinkerPatch {
        /**
         * old apk 的路径
         */
        oldApk = getOldApkPath()
        /**
         * 在产生patch的时候是否忽略tinker的警告,最好不忽略
         * case 1: minSdkVersion小于14,但是dexMode的值为"raw"
         * case 2: 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
         * case 3: 定义在dex.loader用于加载补丁的类不在main dex中;
         * case 4: 定义在dex.loader用于加载补丁的类出现修改;
         * case 5:  resources.arsc改变,但没有使用applyResourceMapping编译
         */
        ignoreWarning = false

        /**
         * 是否启用签名,一般强制使用
         */
        useSign = true

        /**
         * 是否启用tinker
         */
        tinkerEnable = buildWithTinker()

        /**
         * Warning, applyMapping will affect the normal android build!
         */
        buildConfig {
            /**
             * 指定old apk 混淆时的打包文件
             */
            applyMapping = getApplyMappingPath()
            /**
             * 指定old apk 的资源文件
             */
            applyResourceMapping = getApplyResourceMappingPath()

            /**
             * 每个patch文件的唯一标识符
             */
            tinkerId = getTinkerIdValue()

            /**
             * 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
             */
            keepDexApply = false

            /**
             * 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
             */
            isProtectedApp = false

            /**
             * 是否支持新增非export的Activity
             */
            supportHotplugComponent = false
        }

        dex {
            /**
             * 只能是'raw'或者'jar'。
             * 对于'raw'模式,我们将会保持输入dex的格式。
             * 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,
             * 而且它更省存储空间,但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
             */
            dexMode = "jar"

            /**
             * 需要处理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。
             * 5. 使用1.7.6版本之后的gradle版本,参数1、2会自动填写。若使用newApk或者命令行版本编译,1、2依然需要手动填写
             */
            loader = [
                    //use sample, let BaseBuildInfo unchangeable with tinker
//                    "com.hsm.tinkertest.BuildInfo"
            ]
        }
        //lib相关的配置项
        lib {
            /**
             * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...
             */
            pattern = ["lib/*/*.so"]
        }
        //res相关的配置项
        res {
            /**
             * 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,
             * 例如assets/...,务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
             */
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**
             * 若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改
             */
            ignoreChange = ["assets/sample_meta.txt"]

            /**
             * 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,
             * 但是会增加合成时的复杂度。默认大小为100kb
             */
            largeModSize = 100
        }
        //用于生成补丁包中的'package_meta.txt'文件
        packageConfig {
            /**
             * configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,
             * 你可以定义其他的信息, 在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
             * 但是建议直接通过修改代码来实现,例如BuildConfig。
             */
            configField("patchMessage", "tinker is sample to use")
            configField("platform", "all")
            /**
             * patch version via packageConfig
             */
            configField("patchVersion", "1.0")
        }

        /**
         * 7zip路径配置项,执行前提是useSign为true
         */
        sevenZip {
            /**
             * 将自动根据机器属性获得对应的7za运行文件
             */
            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
    def date = new Date().format("MMdd-HH-mm-ss")

    /**
     * old apk复制到指定目录
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name

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

                }
            }
        }
    }
}

task sortPublicTxt() {
    doLast {
        File originalFile = project.file("public.txt")
        File sortedFile = project.file("public_sort.txt")
        List<String> sortedLines = new ArrayList<>()
        originalFile.eachLine {
            sortedLines.add(it)
        }
        Collections.sort(sortedLines)
        sortedFile.delete()
        sortedLines.each {
            sortedFile.append("${it}/n")
        }
    }
}

OK,参数配置完成,下面开始写代码。

先写一个TinkerManager类来管理Tinker的初始化

public class TinkerManager {

    private static final String TAG = "Tinker.TinkerManager";

    private static ApplicationLike                applicationLike;
    /**
     * 保证只初始化一次
     */
    private static boolean isInstalled = false;

    public static void setTinkerApplicationLike(ApplicationLike appLike) {
        applicationLike = appLike;
    }

    public static ApplicationLike getTinkerApplicationLike() {
        return applicationLike;
    }


    public static void setUpgradeRetryEnable(boolean enable) {
        UpgradePatchRetry.getInstance(applicationLike.getApplication()).setRetryEnable(enable);
    }

    public static void installTinker(ApplicationLike appLike) {
        if (isInstalled) {
            TinkerLog.w(TAG, "install tinker, but has installed, ignore");
            return;
        }
        //监听patch文件加载过程中的事件
        LoadReporter loadReporter = new DefaultLoadReporter(appLike.getApplication());
        //监听patch文件合成过程中的事件
        PatchReporter patchReporter = new DefaultPatchReporter(appLike.getApplication());
        //监听patch文件接收到之后可以做一些校验
        PatchListener patchListener = new CustomPatchListener(appLike.getApplication());
        //升级策略
        AbstractPatch upgradePatchProcessor = new UpgradePatch();

        TinkerInstaller.install(appLike,
                loadReporter, patchReporter, patchListener,
                CustomResultService.class, upgradePatchProcessor);

        isInstalled = true;
    }

}

这里面有几个类需要注意

  1. LoadReporter类:监听patch文件加载过程中的事件,这里使用DefaultLoadReporter,如果有需要可以继承DefaultLoadReporter写自己的业务逻辑
  2. PatchReporter :监听patch文件合成过程中的事件,这里使用DefaultPatchReporter,如果哟需要可以继承DefaultPatchReporter写自己的业务逻辑
  3. PatchListener :监听patch文件接收到之后可以做一些校验,这个一般用的比较多,为了保证我们下载的patch包的没有被篡改,可以重写PatchListener,写一些自己的校验
  4. AbstractPatch :升级策略,一般不用修改
  5. CustomResultService:继承自系统的DefaultTinkerResultService,决定在patch安装完以后的后续操作,因为tinker修复完之后需要重启才能生效,tinker默认是加载完patch包之后直接杀死进程。这样可能会不太友好,如果不想直接杀进程可以继承DefaultTinkerResultService类,写我们自己的逻辑。

CustomPatchListener和CustomResultService的样例:

public class CustomPatchListener extends DefaultPatchListener {

    private String currentMD5;

    public void setCurrentMD5(String md5Value) {
        this.currentMD5 = md5Value;
    }
    public CustomPatchListener(Context context) {
        super(context);
    }

    /**
     * 校验
     * @return
     */
    @Override
    public int patchCheck(String path, String patchMd5) {
        //做自己的校验
        
        return super.patchCheck(path, patchMd5);
    }
}
/**
 * 决定在patch安装完以后的后续操作,默认实现是杀进程
 */
public class CustomResultService extends DefaultTinkerResultService {
    private static final String TAG = "Tinker.CustomResultService";

    //返回patch文件的结果
    @Override
    public void onPatchResult(final PatchResult result) {
        if (result == null) {
            TinkerLog.e(TAG, "CustomResultService received null result!!!!");
            return;
        }
        TinkerLog.i(TAG, "CustomResultService receive result: %s", result.toString());

        //first, we want to kill the recover process
        TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (result.isSuccess) {
                    Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show();
                }
            }
        });
        // is success and newPatch, it is nice to delete the raw file, and restart at once
        // for old patch, you can't delete the patch file
        if (result.isSuccess) {
            deleteRawPatchFile(new File(result.rawPatchFilePath));

            //默认是直接重启体验可能不好,这里只是在后台重启
            if (checkIfNeedKill(result)) {
                if (Utils.isBackground()) {
                    TinkerLog.i(TAG, "it is in background, just restart process");
                    restartProcess();
                } else {
                    TinkerLog.i(TAG, "tinker wait screen to restart process");
                    new Utils.ScreenState(getApplicationContext(), new Utils.ScreenState.IOnScreenOff() {
                        @Override
                        public void onScreenOff() {
                            restartProcess();
                        }
                    });
                }
            } else {
                TinkerLog.i(TAG, "I have already install the newly patch version!");
            }
        }
    }

    /**
     * you can restart your process through service or broadcast
     */
    private void restartProcess() {
        TinkerLog.i(TAG, "app is background now, i can kill quietly");
        //you can send service or broadcast intent to restart your process
        android.os.Process.killProcess(android.os.Process.myPid());
    }

}

为了使真正的Application实现可以在补丁包中修改,tinker建议Appliction类的所有逻辑移动到ApplicationLike代理类中。

@DefaultLifeCycle(application = ".SampleTinkerApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class CustomTinkerLike extends DefaultApplicationLike {
    CustomTinkerLike mCustomTinkerLike;
    public CustomTinkerLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
                            long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        //必须使用multiDex
        MultiDex.install(base);

        mCustomTinkerLike = this;
        TinkerManager.setTinkerApplicationLike(this);

        //在 installed 之前设置
        TinkerManager.setUpgradeRetryEnable(true);

        //installTinker after load multiDex
        //or you can put com.tencent.tinker.** to main dex
        TinkerManager.installTinker(this);
        Tinker tinker = Tinker.with(getApplication());
    }
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }
}
  • 自定义一个CustomTinkerLike继承自DefaultApplicationLike,以前在我们自定义的Application中初始化的代码都移动到这里的onCreate()方法中。
  • 添加注解DefaultLifeCycle,第一个是application的名字,编译的时候会自动给我们生成一个application类,然后把这个生成的application注册到AndroidManifest.xml中。

最后Activity中定义一个按钮点击加载patch包

public void load(View view) {
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
    }

到这里配置和代码就都完成了,下面开始打包,先打基础包

线上的包基本都是release包,前面已经配置了签名,所以这就只打release包。

可以使用命令行输入命令 ./gradlew assemableRelease ,也可以使用studio的快捷操作,快捷操作图片如下

Android Tinker集成采坑

打完包之后,tinker会将outputs/release文件夹下的打包好的文件复制一份到bakApk文件夹中一份,并重命名,这个bakApk文件夹是前面在gradle中配置的。还有混淆的mapping文件和R文件也复制一份重命名放到bakApk文件夹下面。

Android Tinker集成采坑

把打包好的apk装到手机上,然后修改一些代码,开始打补丁包

如图修改gradle中的oldApk的信息。然后调用tinker的命令打包如下图

Android Tinker集成采坑

打包完成之后在outputs文件夹下会多出来一个tinkerPatch文件夹。patch_signed_7zip.apk就死我们需要的patch包了。直接放到前面加载sdk文件的路径,或者从网络下载到该路径,之后调用加载的方法就完成修复了。

Android Tinker集成采坑

原文  https://chsmy.github.io/2019/06/02/technology/Android-Tinker集成采坑/
正文到此结束
Loading...