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; } }
这里面有几个类需要注意
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); } }
最后Activity中定义一个按钮点击加载patch包
public void load(View view) { TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk"); }
到这里配置和代码就都完成了,下面开始打包,先打基础包
线上的包基本都是release包,前面已经配置了签名,所以这就只打release包。
可以使用命令行输入命令 ./gradlew assemableRelease
,也可以使用studio的快捷操作,快捷操作图片如下
打完包之后,tinker会将outputs/release文件夹下的打包好的文件复制一份到bakApk文件夹中一份,并重命名,这个bakApk文件夹是前面在gradle中配置的。还有混淆的mapping文件和R文件也复制一份重命名放到bakApk文件夹下面。
把打包好的apk装到手机上,然后修改一些代码,开始打补丁包
如图修改gradle中的oldApk的信息。然后调用tinker的命令打包如下图
打包完成之后在outputs文件夹下会多出来一个tinkerPatch文件夹。patch_signed_7zip.apk就死我们需要的patch包了。直接放到前面加载sdk文件的路径,或者从网络下载到该路径,之后调用加载的方法就完成修复了。