Tinker是微信团队开发的一款热补丁框架,由于微信的用户体系庞大,因此对于微信开发的Tinker的稳定性是毋庸置疑的。下面先来说一下我问什么会选择使用Tinker:
1.稳定性与兼容性:因为只有稳定的热修复框架,才能稳定的去实现我们bug的解决。
2.性能以及易用性:也研究过其他的热修复框架,如阿里的AndFix,大众点评的nuwa,美团的robust,在使用复杂度上个人感觉还是Tinker更加优秀,并且Tinker的补丁包非常的小,更容易进行下载修复
综合上边的优点我选择在我的项目中使用Tinker,来实现热补丁功能,下面就通过一个例子来看看如何使用Tinker吧。
1.首先在项目的build.gradle文件的dependencies中添加:
classpath 'com.tencent.tinker:tinker-patch-gradle-plugin:1.6.2'
2.使用Tinker首先要导包,在app模块的build.gradle文件的dependencies下面添加:
第一条是可选的,用于生成application类compile 'com.tencent.tinker:tinker-android-anno:1.6.2' compile 'com.tencent.tinker:tinker-android-lib:1.6.2' compile 'com.android.support:multidex:1.0.1'
后面两条是Tinker的核心库 3.在清单配置文件中打开sd权限,在这里为了方便测试时直接将补丁文件放到sd卡上
4.除了在步骤2中的配置还需要在app模块的build文件中添加:(在该文件的所有代码的外部添加)不要怕,上边的内容直接放到你app的build文件的最外层即可def gitSha() { try { String gitRev = 'git rev-parse --short HEAD'.execute().text.trim() 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'") } } def bakPath = file("${buildDir}/bakApk/") /** * you can use assembleRelease to build you base apk * use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch * add apk from the build/bakApk */ ext { //for some reason, you may want to ignore tinkerBuild, such as instant run debug build? tinkerEnabled = true //you should bak the following files //old apk file to build patch apk tinkerOldApkPath = "${bakPath}/app-debug-1230-14-05-12.apk" //修改成你第一次运行的apk名称 //proguard mapping file to build patch apk tinkerApplyMappingPath = "${bakPath}/" //resource R.txt to build patch apk, must input if there is resource changed tinkerApplyResourcePath = "${bakPath}/app-debug-1230-14-05-12-R.txt" //修改成你第一次运行生成的R.txt } 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 } if (buildWithTinker()) { apply plugin: 'com.tencent.tinker.patch' tinkerPatch { /** * necessary,default 'null' * the old apk path, use to diff with the new apk to build * add apk from the build/bakApk */ oldApk = getOldApkPath() /** * optional,default 'false' * there are some cases we may get some warnings * if ignoreWarning is true, we would just assert the patch process * case 1: minSdkVersion is below 14, but you are using dexMode with raw. * it must be crash when load. * case 2: newly added Android Component in AndroidManifest.xml, * it must be crash when load. * case 3: loader classes in dex.loader{} are not keep in the main dex, * it must be let tinker not work. * case 4: loader classes in dex.loader{} changes, * loader classes is ues to load patch dex. it is useless to change them. * it won't crash, but these changes can't effect. you may ignore it * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build */ ignoreWarning = true /** * optional,default 'true' * whether sign the patch file * if not, you must do yourself. otherwise it can't check success during the patch loading * we will use the sign config with your build type */ useSign = true /** * Warning, applyMapping will affect the normal android build! */ buildConfig { /** * optional,default 'null' * if we use tinkerPatch to build the patch apk, you'd better to apply the old * apk mapping file if minifyEnabled is enable! * Warning: * you must be careful that it will affect the normal assemble build! */ applyMapping = getApplyMappingPath() /** * optional,default 'null' * It is nice to keep the resource id from R.txt file to reduce java changes */ applyResourceMapping = getApplyResourceMappingPath() /** * necessary,default 'null' * because we don't want to check the base apk with md5 in the runtime(it is slow) * tinkerId is use to identify the unique base apk when the patch is tried to apply. * we can use git rev, svn rev or simply versionCode. * we will gen the tinkerId in your manifest automatic */ // tinkerId = getTinkerIdValue() tinkerId = "TinkerSimple" //或者其他你想要的id,此处可以随便修改 } dex { /** * optional,default 'jar' * only can be 'raw' or 'jar'. for raw, we would keep its original format * for jar, we would repack dexes with zip format. * if you want to support below 14, you must use jar * or you want to save rom or check quicker, you can use raw mode also */ dexMode = "jar" /** * necessary,default '[]' * what dexes in apk are expected to deal with tinkerPatch * it support * or ? pattern. */ pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] /** * necessary,default '[]' * Warning, it is very very important, loader classes can't change with patch. * thus, they will be removed from patch dexes. * you must put the following class into main dex. * Simply, you should add your own application {@code tinker.sample.android.SampleApplication} * own tinkerLoader, and the classes you use in them * */ loader = ["com.tencent.tinker.loader.*", "tinker.sample.android.SampleApplication", //use sample, let BaseBuildInfo unchangeable with tinker "tinker.sample.android.app.BaseBuildInfo" ] } lib { /** * optional,default '[]' * what library in apk are expected to deal with tinkerPatch * it support * or ? pattern. * for library in assets, we would just recover them in the patch directory * you can get them in TinkerLoadResult with Tinker */ pattern = ["lib/armeabi/*.so"] } res { /** * optional,default '[]' * what resource in apk are expected to deal with tinkerPatch * it support * or ? pattern. * you must include all your resources in apk here, * otherwise, they won't repack in the new apk resources. */ pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] /** * optional,default '[]' * the resource file exclude patterns, ignore add, delete or modify resource change * it support * or ? pattern. * Warning, we can only use for files no relative with resources.arsc */ ignoreChange = ["assets/sample_meta.txt"] /** * default 100kb * for modify resource, if it is larger than 'largeModSize' * we would like to use bsdiff algorithm to reduce patch file size */ largeModSize = 100 } packageConfig { /** * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE' * package meta file gen. path is assets/package_meta.txt in patch file * you can use securityCheck.getPackageProperties() in your ownPackageCheck method * or TinkerLoadResult.getPackageConfigByName * we will get the TINKER_ID from the old apk manifest for you automatic, * other config files (such as patchMessage below)is not necessary */ configField("patchMessage", "tinker is sample to use") /** * just a sample case, you can use such as sdkVersion, brand, channel... * you can parse it in the SamplePatchListener. * Then you can use patch conditional! */ configField("platform", "all") } //or you can add config filed outside, or get meta value from old apk //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test")) //project.tinkerPatch.packageConfig.configField("test2", "sample") /** * if you don't use zipArtifact or path, we just use 7za to try */ sevenZip { /** * optional,default '7za' * the 7zip artifact path, it will use the right 7za with your platform */ 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" } } /** * bak apk and mapping */ 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 date = new Date().format("MMdd-HH-mm-ss") from "${buildDir}/outputs/apk/${project.getName()}-${taskName}.apk" into bakPath rename { String fileName -> fileName.replace("${project.getName()}-${taskName}.apk", "${project.getName()}-${taskName}-${date}.apk") } from "${buildDir}/outputs/mapping/${taskName}/mapping.txt" into bakPath rename { String fileName -> fileName.replace("mapping.txt", "${project.getName()}-${taskName}-${date}-mapping.txt") } from "${buildDir}/intermediates/symbols/${taskName}/R.txt" into bakPath rename { String fileName -> fileName.replace("R.txt", "${project.getName()}-${taskName}-${date}-R.txt") } } } } } } }
5.下面就是最重要的一点了,创建一个类去继承DefaultApplicationLike,并且在该类上加上注解如下图所示,
注解后边替换成你的application的包名路径,记住不需要你创建出这个appliction,只需要这样写,如果你又继续创建了,会编译冲突,
同时添上三个方法,onBaseContextAttached方法中完成初始化Tinker (注意:这三个方法直接在文件中创建即可,不需要修改)@DefaultLifeCycle( application = "com.hdwx.tencenttinkertest.SimpleApplication", flags = ShareConstants.TINKER_ENABLE_ALL) public class SimpleApplicationLike extends DefaultApplicationLike{ public SimpleApplicationLike(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); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback){ getApplication().registerActivityLifecycleCallbacks(callback); } @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); //初始化Tinker MultiDex.install(base); TinkerInstaller.install(this); } }
注意:将上面代码中加红色背景的内容,作为你清单配置文件中要注册的Application,清单配置文件中的代码如下所示:
SimpleApplication报红并不影响程序运行。6.在你的界面上设置点击按钮,点击按钮的监听事件中写入如下代码:<application android:name=".SimpleApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity">
7.测试public void btnLoadPatch(View view) { TinkerInstaller.onReceiveUpgradePatch( getApplicationContext(), Environment.getExternalStorageDirectory() .getAbsolutePath()+"/patch_signed_7zip.apk");//红色的是你生成的补丁文件 这整个路径就是你补丁文件的路径 }
在你项目的运行界面创建一个Toast,点击弹出“bug”。然后运行到手机上(注意:只需要运行这一次),这时候你就会在你项目的Project视图下,
app-->build-->bakApk中看到生成的apk和R.txt文件,将这两个文件的名称添加到步骤4中的你app的build.gradle文件中对应的位置
最后修改Toast中的代码,将内容改为“bug被完美解决啦!”,然后点击下图中的tinkerPatchDebug按钮:
最后就会在控制台打印出我们修改内容后生成的补丁包在文件夹中的路径,然后在生成的debug目录中,我们就能拿到名成为的补丁包,然后将这个补丁包放到我们手机的SD卡上,执行btnLoadPatch点击事件,这时程序就会修复补丁,然后程序会闪退(这是框架内设定好的,有兴趣的可以查看源码)patch_signed_7zip.apk
然后重新启动应用,你就会发现Toast弹出的内容就改变成“bug被完美解决啦!”