Android 热修复 Tinker接入方法

来源:互联网 发布:众泰e200好还是知豆 编辑:程序博客网 时间:2024/06/08 02:22

什么是热修复

简单的说就是用户不用重新下载一个新的apk安装,而是直接下载一个补丁包,通过补丁来替换一些出现bug的类,当然下载补丁的过程用户一般是感觉不到的,表面上看是直接修复了bug。

热修复原理

简单的来说,就是把最后修改的类打包成dex,插入到ClassLoader的dex数组的最前面,当ClassLoader找类时,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找到则返回,如果找不到从下一个dex文件继续查找。因此我们最后修改的类会最先被找到,代替了旧的类,这样达到“修复”的目的,有点“换零件”的意思。

详细的热修复原理,请参考 安卓App热补丁动态修复技术介绍

为什么使用Tinker

官方是这么说的:

当前市面的热补丁方案有很多,其中比较出名的有阿里的AndFix、美团的Robust以及QZone的超级补丁方案。但它们都存在无法解决的问题,这也是正是我们推出Tinker的原因。

特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。

以上也是我使用Tinker热补丁方案的原因,不过还是有不足的。

Tinker的已知问题

由于原理与系统限制,Tinker有以下已知问题:

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;(所以,如果要新增功能的话,还是建议版本升级,毕竟这个只是热修复方案。)

  2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;(我倒觉得没关系)

  3. 在Android N上,补丁对应用启动时间有轻微的影响;(不知道程度有多大,目前只是做了Demo)

  4. 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”; (这个得做兼容)

  5. 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。 (希望以后能支持)

Tinker接入方法

接入Tinker目前给了两种方式,一种是基于命令行的方式,类似于AndFix的接入方式;一种就是gradle的方式。

gradle的方式

gradle接入的方式应该算是主流的方式,所以tinker也直接给出了例子,单独将该tinker-sample-android以project方式引入即可。

做法比较简单,具体方法请参考 Android 热修复 Tinker接入及源码浅析 的 “2gradle接入”一章。

由于这种方式要把整个“tinker-sample-android”引入到项目中,所以我不太喜欢,因此我决定用下面这种方式接入。

命令行的方式

估计用这种方式的不多,毕竟主流的gradle接入方式很方便,网上很多方法都说得不明不白,有的甚至步骤有问题,花了好几天,终于接入成功。在这里写下我踩的坑,希望能帮助有需要的人。

先给出Tinker的github地址:https://github.com/Tencent/tinker

(1)添加Tinker的依赖

// build.gradle...dependencies {    ...    //可选,用于生成application类    provided('com.tencent.tinker:tinker-android-anno:1.7.7')    //tinker的核心库    compile('com.tencent.tinker:tinker-android-lib:1.7.7')    ...}...

(2)配置签名信息(最好配一下)

// build.gradle...android{    ...    signingConfigs {        release {            try {                // keystore文件放在与该build.gradle的同一目录下                storeFile file("release.keystore")                storePassword "testres"                keyAlias "testres"                keyPassword "testres"            } catch (ex) {                throw new InvalidUserDataException(ex.toString())            }        }    }    buildTypes {        release {            minifyEnabled true            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }        debug {            debuggable true            minifyEnabled true            signingConfig signingConfigs.release            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }    ...}...

(3)编写ApplicationLike子类,并添加注解,生成Application

// 添加DefaultLifeCycle注解之后,编译后,会产生名为SimpleTinkerInApplication的Application子类// application = ".SimpleTinkerInApplication" 指定编译后生成Application子类的类名@DefaultLifeCycle(application = ".SimpleTinkerInApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)public class SimpleTinkerInApplicationLike extends ApplicationLike {    public SimpleTinkerInApplicationLike(Application application, int tinkerFlags,         boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,         long applicationStartMillisTime, Intent tinkerResultIntent) {        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);    }    @Override    public void onBaseContextAttached(Context base) {        super.onBaseContextAttached(base);    }    @Override    public void onCreate() {        super.onCreate();        TinkerInstaller.install(this);    }}

(4)在AndroidManifest文件设置application的名称

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.johan.demo">    <application        <!-- 因为SimpleTinkerInApplication是编译后才生成的,所以这里会报错,build一下工程就行了 -->        android:name=".SimpleTinkerInApplication"        ...        >    </application></manifest>

(5)在AndroidManifest文件指定TINKER_ID

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.johan.demo">    ...    <application        ...        <meta-data            android:name="TINKER_ID"            <!-- 随便指定一个id -->            android:value="tinker_id_56888" />    </application></manifest>

(6)在AndroidManifest文件声明权限,修复需要读取SD文件

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.johan.demo">    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />    ...</manifest>

(7)在Activity添加修复的按钮

import com.tencent.tinker.lib.tinker.TinkerInstaller;public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // Android6.0 之后需要申请权限         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);        }    }    /**     * 预留一个按钮     * 点击按钮,开始修复     **/    public void fixIt(View view) {        // 第二个参数指定更新包的路径,这里为SD卡根目录的patch_signed.apk文件        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");    }}

(8)添加Tinker的混淆

// proguard-rules.pro...-keepattributes *Annotation*-dontwarn com.tencent.tinker.anno.AnnotationProcessor-keep @com.tencent.tinker.anno.DefaultLifeCycle public class *-keep public class * extends android.app.Application {    *;}-keep public class com.tencent.tinker.loader.app.ApplicationLifeCycle {    *;}-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {    *;}-keep public class com.tencent.tinker.loader.TinkerLoader {    *;}-keep public class * extends com.tencent.tinker.loader.TinkerLoader {    *;}-keep public class com.tencent.tinker.loader.TinkerTestDexLoad {    *;}-keep public class com.tencent.tinker.loader.TinkerTestAndroidNClassLoader {    *;}#for command line version, we must keep all the loader class to avoid proguard mapping conflict#your dex.loader pattern here-keep public class com.tencent.tinker.loader.** {    *;}-keep class tinker.sample.android.app.SampleApplication {    *;}...

可以参考 tinker_proguard.pro

(9)这时生成一个APP,并签名。把签名后的APP(命名为old.apk,作为原始APP,假设有Bug)和mapping.txt(签名后,会在项目目录/app/build/outputs/mapping/release这个文件夹下)复制到另一个地方保存起来,接下来有用到

===================== 解决Bug的过程 =====================

(10)解决Bug之后,把之前保存的mapping.txt文件复制到与proguard-rules.pro同一文件夹下,并添加混淆

// proguard-rules.pro...-applymapping mapping.txt

(11)生成新的APP,并签名,这时签名APP(命名为new.apk)是已经修改好Bug的了

(12)制作补丁包

Tinker提供了补丁包生成的工具,源码见:tinker-patch-cli,打成一个jar就可以使用,并且提供了命令行相关的参数以及文件。

命令行如下:

java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output

需要注意的就是tinker_config.xml,里面包含tinker的配置,例如签名文件等。

这里我们直接使用Tinker提供的签名文件,所以不需要做修改(项目中一般会用自己的签名,所以一定要修改),不过里面有个Application的item修改为我们指定编译后指定生成的Application一致(包名+类名),见步骤(3),例子中改为:

// tinker_config.xml<loader value="com.johan.demo.SimpleTinkerInApplication"/>...// 如果签名改了,一定要记得修改(这个一直没注意)<!--sign, if you want to sign the apk, and if you want to use 7zip, you must fill in the following data--><issue id="sign">    <!--the signature file path, in window use \, in linux use /, and the default path is the running location-->    <path value="xxx.keystore"/>    <!--storepass-->    <storepass value="xxx"/>    <!--keypass-->    <keypass value="xxx"/>    <!--alias-->    <alias value="xxx"/></issue>

这里提供一个简单的工具包,地址:https://github.com/JohanMan/tinker-patch-tool

解压之后,把你的old.apk和new.apk覆盖工具包里的old.apk和new.apk,然后修改tinker_config.xml,然后使用命令

java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output

会在工具包的output文件夹生成patch_signed.apk文件:

这就是更新的包了。

(13)把制作好的补丁包复制指定的地方,见步骤(7),例子中指定的地方是SD卡根目录下

(14)启动原始APP(我们安装的old.apk,有Bug),点击修复按钮,程序就会加载我们的补丁包进行修复

以上是我以命令行方式接入Tinker踩过的坑,自己多尝试一下,应该能成功的!!!

补充

默认命令行式的接入,当热修复完毕之后,也就是执行

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");

参考 Tinker 自定义扩展 :

AbstractResultService类是:patch补丁合成进程将合成结果返回给主进程的类。我们为你提供了默认实现DefaultTinkerResultService.java。一般来说, 你可以继承DefaultTinkerResultService实现自己的回调,例如SampleResultService.java。当然,你也需要在AndroidManifest上添加你的Service。<service    android:name=".service.SampleResultService"    android:exported="false"/>默认我们在DefaultTinkerResultService会杀掉:patch进程,假设当前是补丁升级并且成功了,我们会杀掉当前进程,让补丁包更快的生效。若是修复类型的补丁包并且失败了,我们会卸载补丁包。

我们一般都不会想,修复补丁之后,立刻让app退出,应该等到合适的时机,才去重启app让补丁包生效。

我们先看一下Tinker为我们提供的DefaultTinkerResultService怎么处理合成结果:

@Overridepublic void onPatchResult(PatchResult result) {    if (result == null) {        TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");        return;    }    TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());    //first, we want to kill the recover process    TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());    // if success and newPatch, it is nice to delete the raw file, and restart at once    // only main process can load an upgrade patch!    if (result.isSuccess) {        deleteRawPatchFile(new File(result.rawPatchFilePath));        // 检测是否需要杀死进程才能让补丁包生效        if (checkIfNeedKill(result)) {            // 杀死app进程            android.os.Process.killProcess(android.os.Process.myPid());        } else {            TinkerLog.i(TAG, "I have already install the newly patch version!");        }    }}

根据以上代码,其实我们只要删除杀死app进程哪一行代码就行了,所以我们继承DefaultTinkerResultService,重写onPatchResult方法:

public class SimpleResultService extends DefaultTinkerResultService {    private static final String TAG = "SimpleResultService";    @Override    public void onPatchResult(PatchResult result) {        if (result == null) {            TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");            return;        }        TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());        //first, we want to kill the recover process        TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());        // if success and newPatch, it is nice to delete the raw file, and restart at once        // only main process can load an upgrade patch!        if (result.isSuccess) {            deleteRawPatchFile(new File(result.rawPatchFilePath));            if (checkIfNeedKill(result)) {                // 注释掉,不杀死app进程//              android.os.Process.killProcess(android.os.Process.myPid());                TinkerLog.i(TAG, "need kill and restart the app");            } else {                TinkerLog.i(TAG, "I have already install the newly patch version!");            }        }    }}

当然还要在AndroidManifest注册SimpleResultService:

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.johan.demo">    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />    <application        ...        <service android:name=".SimpleResultService" android:exported="false" />        ...    </application></manifest>

虽然我们定义了SimpleResultService,但是我们怎么让Tinker用这个自定义的合成结果处理类呢?答案在TinkerInstaller。

还记得 步骤(3)编写ApplicationLike子类(SimpleTinkerInApplicationLike)吗,我们在SimpleTinkerInApplicationLike的onCreate方法中调用了TinkerInstaller的install方法,其实install方法有2个,参数不一样

public static Tinker install(ApplicationLike applicationLike)public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter,             PatchReporter patchReporter, PatchListener listener,             Class<? extends AbstractResultService> resultServiceClass,             AbstractPatch upgradePatchProcessor)

我们可以使用第2个方法,让Tinker使用我们的SimpleResultService:

TinkerInstaller.install(this, new DefaultLoadReporter(getApplication()),                 new DefaultPatchReporter(getApplication()),                new DefaultPatchListener(getApplication()),                 SimpleResultService.class, new UpgradePatch());

更多关于自定义Tinker,请参考 Tinker 自定义扩展

参考资料

Tinker库地址
Tinker – 微信Android热补丁方案
Tinker 自定义扩展
安卓App热补丁动态修复技术介绍
Android 热修复 Tinker接入及源码浅析

原创粉丝点击