您的位置:首页 > 移动开发 > 微信开发

Android热修复-微信Tinker

2017-02-13 13:52 435 查看


写在前面

正常情况下一旦线上版本出BUG时,这时候得改BUG,重新发布上线,用户重新下载安装,成本未免有点高;基于这种情况下很多热修复框架孕育而生,比较火的有:Andfix、HotFix等;本文旨在帮助没接触过Tinker的童鞋快速集成使用热修复;


本文环境

官方地址

SdkVersion 24

gradle:2.2.0

Tinker版本 1.7.5


集成Tinker

在项目根目录的build.gradle中添加,例:

1234
dependencies {      ···      classpath('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5')  }
切换到module的build.gradle中添加,例:

12345
dependencies {	···    provided('com.tencent.tinker:tinker-android-anno:1.7.5')    compile('com.tencent.tinker:tinker-android-lib:1.7.5')}
这里我把打包签名的jks文件配置到gradle中,例:

因为我这里测试用到的是项目的签名文件,所以不方便给出,需要的自行生成,毕竟这是很简单的事;

1234567891011121314151617181920212223242526
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最下面:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
/**-----------------------------------配置开始-----------------------------------*/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,例:

123456789101112131415161718
@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中添加:

123456789101112
<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,其它不管,例:

123456789
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,例:

1234567
Tinker tinker = Tinker.with(getApplicationContext());boolean isLoadSuccess = tinker.isTinkerLoaded();if(isLoadSuccess){	Log.i(TAG,"success");}else{	Log.i(TAG,"error");	}
OK,加载补丁前点击”SHOW”会抛出空指针异常,修复完后点击”SHOW”,BUG解决:



DEMO地址


总结

Tinker是腾讯出品,微信在用,我想兼容性应该不会差到哪去,相比其他热修复框架,这是个很大的优点,而且Tinker支持类替换、so库及资源的替换等,如果在相对稳定的情况下,使用Tinker用于线上产品的功能升级应该没什么问题,其它更高级的用法自行深入研究;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息