写在前面
正常情况下一旦线上版本出BUG时,这时候得改BUG,重新发布上线,用户重新下载安装,成本未免有点高;基于这种情况下很多热修复框架孕育而生,比较火的有:Andfix、HotFix等;本文旨在帮助没接触过Tinker的童鞋快速集成使用热修复;
本文环境
官方地址
SdkVersion 24
gradle:2.2.0
Tinker版本 1.7.5
集成Tinker
在项目根目录的build.gradle中添加,例:
1 2 3 4 | dependencies { ··· classpath('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5') } |
切换到module的build.gradle中添加,例:
1 2 3 4 5 | dependencies { ··· provided('com.tencent.tinker:tinker-android-anno:1.7.5') compile('com.tencent.tinker:tinker-android-lib:1.7.5') } |
这里我把打包签名的jks文件配置到gradle中,例:
因为我这里测试用到的是项目的签名文件,所以不方便给出,需要的自行生成,毕竟这是很简单的事;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | android { ··· signingConfigs { release { try { storeFile file("./keystore/app.jks") storePassword "password" keyAlias "alias" keyPassword "password" } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } debug { try { storeFile file("./keystore/app.jks") storePassword "password" keyAlias "alias" keyPassword "password" } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } } } |
OK,到这里配置还是相对简单的,下面配置Tinker的gradle插件,官方demo配置还是比较麻烦的,看着就头晕,建议可以把先前的内容先复制出来,在拷贝官方demo的build.gradle进去一点点修改;
如果还是懒得改,直接把下面内容复制到build.gradle最下面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | /**-----------------------------------配置开始-----------------------------------*/ def bakPath = file("${buildDir}/bakApk/") def gitSha() { try { String gitRev = '11.2.3.5' 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 { tinkerEnabled = true //旧apk tinkerOldApkPath = "${bakPath}/app-debug-0118-15-13-26.apk" //旧包混淆文件 tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt" //旧apk R文件 tinkerApplyResourcePath = "${bakPath}/app-debug-0118-15-13-26-R.txt" //多渠道 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.tinker.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" } } } } } } } /**-----------------------------------配置结束-----------------------------------*/ |
注意tinkerId目前暂时是写死的;
新建SampleApplicarion文件继承DefaultApplicationLike,例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @DefaultLifeCycle(application = "com.tinker.MyApplication",flags = ShareConstants.TINKER_ENABLE_ALL) 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); } @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); TinkerInstaller.install(this); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); } } |
上面的application=”你的包名.MyApplication”并在上一步将dex中的loader修改为:
1 | loader = ["com.tencent.tinker.loader.*","你的包名.MyApplication",] |
记得在AndroidManifest.xml中添加:
1 2 3 4 5 6 7 8 9 10 11 12 | <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <application android:name=".MyApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> ··· ··· </application> |
OK,到这里就算配置完成了,配置完了当然要测试了;
测试修复BUG
在MainActivity中添加完初始代码后点击Build->Build APK生成APK;
将outputs里面的apk拷贝出去安装在手机上,然后打开的build.radle修改ext,其它不管,例:
1 2 3 4 5 6 7 8 9 | ext { tinkerEnabled = true //旧apk tinkerOldApkPath = "${bakPath}/app-debug-0118-16-38-14.apk" ··· //旧apk R文件 tinkerApplyResourcePath = "${bakPath}/app-debug-0118-16-38-14-R.txt" ··· } |
然后切换到MainActivity修复你的bug,这里我添加了一张资源图片(刚好同事在玩支付宝集福,就P了张有9张”敬业福”的图片骗他们,哈哈),修复好代码后点击右侧Gradle找到:app->Tasks->tinker,然后双击tinkerPatchDebug后会在outputs下面生成tinkerPatch文件夹,其中的patch_signed_7zip.apk就是你需要的补丁:
将patch_signed_7zip.apk文件放到你设置的SD卡目录,调用Tinker加载补丁,例:
1 | TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patchPath); |
如果加载成功下面的tinker.isTinkerLoaded()返回的是true,例:
1 2 3 4 5 6 7 | Tinker tinker = Tinker.with(getApplicationContext()); boolean isLoadSuccess = tinker.isTinkerLoaded(); if(isLoadSuccess){ Log.i(TAG,"success"); }else{ Log.i(TAG,"error"); } |
OK,加载补丁前点击”SHOW”会抛出空指针异常,修复完后点击”SHOW”,BUG解决:
总结
Tinker是腾讯出品,微信在用,我想兼容性应该不会差到哪去,相比其他热修复框架,这是个很大的优点,而且Tinker支持类替换、so库及资源的替换等,如果在相对稳定的情况下,使用Tinker用于线上产品的功能升级应该没什么问题,其它更高级的用法自行深入研究;