Android 热修复 Tinker接入方法

什么是热修复

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

热修复原理

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

Android 热修复 Tinker接入方法_第1张图片

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

为什么使用Tinker

官方是这么说的:

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

Android 热修复 Tinker接入方法_第2张图片

特别是在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的名称


<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

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    package="com.johan.demo">
    ...
    ...
        "TINKER_ID"
            
            android:value="tinker_id_56888" />
    

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


<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"/>
...
// 如果签名改了,一定要记得修改(这个一直没注意)

<issue id="sign">
    
    <path value="xxx.keystore"/>
    
    <storepass value="xxx"/>
    
    <keypass value="xxx"/>
    
    <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文件:

Android 热修复 Tinker接入方法_第3张图片

这就是更新的包了。

(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上添加你的Serviceandroid:name=".service.SampleResultService"
    android:exported="false"
/>

默认我们在DefaultTinkerResultService会杀掉:patch进程,假设当前是补丁升级并且成功了,我们会杀掉当前进程,让补丁包更快的生效。若是修复类型的补丁包并且失败了,我们会卸载补丁包。

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

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

@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());
        } 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:

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    package="com.johan.demo">
    "android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
        ".SimpleResultService" android:exported="false" />
        ...
    

虽然我们定义了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, 
            Classextends 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接入及源码浅析

你可能感兴趣的:(Android)