Android热修复技术Tinker在Android中的实践

欢迎转载,转载请标明出处:
http://blog.csdn.net/johnny901114/article/details/54934782
本文出自:【余志强的博客】

Tinker初体验

先到Github上下载Tinker源码,里面包含了tinker-sample-android,使用AndroidStudio导入该例子工程即可。

导入工程后,运行程序 ,出现如下错误:

Error:A problem occurred configuring project ':app'.
> Tinker does not support instant run mode, please trigger build by assembleDebug or disable instant run in 'File->Settings...'.

意思是说Tinker不支持 install run 模式,请手动 build assembleDebug 或者把 install run 模式禁用掉。

我把 install run 模式关闭了,然后运行成功了。如下图所示:

这里写图片描述

为了验证热修复功能,我在MainActivity的不布局里加一个Button:

<Button
    android:id="@+id/division"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true"
    android:layout_below="@+id/showInfo"
    android:text="division"/>

然后监听Button的点击事件,在onClick方法里做除法运算(1/0),当我们点击Button的时候肯定会闪退,因为除数不能为0

findViewById(R.id.division).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(v.getContext(), "" + (1 / 0), Toast.LENGTH_LONG).show();
    }
});

在MainActivity添加完代码后,重新运行,效果图如下:

这里写图片描述

点击我们新添加的Button(DIVISION),闪退报错:

java.lang.ArithmeticException: divide by zero
   at tinker.sample.android.app.MainActivity$6.onClick(MainActivity.java:114)
   at android.view.View.performClick(View.java:5207)
   at android.view.View$PerformClick.run(View.java:21177)
   at android.os.Handler.handleCallback(Handler.java:739)
   at android.os.Handler.dispatchMessage(Handler.java:95)
   at android.os.Looper.loop(Looper.java:148)
   at android.app.ActivityThread.main(ActivityThread.java:5438)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)

假设这个版本已经发布到了市场,用户安装使用了,我们如何不更新App的情况下,修复这个Bug呢?接下来Tinker就闪亮登场了。

在我们你运行该工程的时候,Tinker会在app/build/bakAPK目录下生成类似下面的文件:

  • app-debug-0207-15-28-17.apk
  • app-debug-0207-15-28-17-R.txt

如下图所示:

这里写图片描述

app-debug-0207-15-28-17.apk 就相当于用户正在使用的APK(old apk),后面用到的patch apk都是基于old apk和new apk的不同(diff)生成的。

app-debug-0207-15-28-17-R.txt R.txt 也是用于生成patch apk的。

介绍完了app/build/bakAPK下的两个文件,还需要配置app/gradle.build

ext {
    //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
    tinkerEnabled = true

    //for normal build
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-debug-0207-15-28-17.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-debug-0207-15-28-17-R.txt"

    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

如上面的配置所示,我们需要把tinkerOldApkPath设置为上面介绍的app-debug-0207-15-28-17.apk
tinkerApplyResourcePath设置为app-debug-0207-15-28-17-R.txt

好了,最基本的配置就介绍到这里了,现在我们来修复一下上面除数为0的bug,很简单,直接让出一个不为零的除数就可,代码如下所示:

findViewById(R.id.division).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(v.getContext(), "" + (1 / 1), Toast.LENGTH_LONG).show();
    }
});

如果热修复成功,不出意外的话,当我们点击按钮的的时候 ,应该会弹出Toast显示1。

要想热修复成功,首先要有patch apk,这个patch apk是根据old apk和new apk通过diff算法得出的,老版本(old apk)有bug,然后我们我们通过代码把这个bug修复了(new apk),通过Tinker diff算法就得出了patch apk,然后old apk(也就是用户正在使用的版本)从远程服务器下载这个patch apk然后通过Tinker的onReceiveUpgradePatch方法实现热修复,如下所示:

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

为了简单起见,这个patch apk我就不真的从服务器下载了,就直接放到本地了。

接下来点击Android Studio右侧的gradle菜单,找到tinkerPatchDebug双击它,如下图所示:

这里写图片描述

build完成之后会在app/outpus/tinkerPatch/debug/下生成patch_signed_7zip.apk文件,如下所示:

这里写图片描述

根据上面的代码

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

我们只要把patch_signed_7zip.apk文件放到SD卡的根目录就可以了,然后点击按钮LOAD PATCH ,如果 不出意外的话,一会就会弹出Toast(patch success,please restart process)

这里写图片描述

然后杀死进程,重新进入app,点击Button(DIVISION),弹出Toast:

这里写图片描述

至此,通过Tinker实现了热修复功能。

Tinker实战

上面是我们直接运行Tinker官方的sample对Tinker的使用有了个了解,现在如何在一个全新的工程使用Tinker。就以我以前做过的一个项目为例,该项目是我最近一个公司的产品,主要是做艺术品电商这块的(出现了一个小bug),当我进入艺术品详情后界面出弹出拒绝访问的Toast,如下图所示:

这里写图片描述

通过查看代码发现犯了一个低级错误,因为在界面里需要访问购物车的数量,然后显示在界面购车图标的右上角,忘记了判断用户是否登录,竟然犯了这种低级错误,真是老脸一红啊,但是毕竟是自己的错误,就要大胆的承认,知耻而后勇,哈哈。有bug的代码如下(再调用API之前应该判断用户是否登录):

@Override
public void onResume() {
    super.onResume();
    //if (UserManagerControl.isLogin()) {应该加上判断
        orderController.getShoppingCartCount();
   //}
}

要想加入热修复功能,

第一步,加入Tinker的依赖包,我直接参考tinker-sample-android app下的build.gralde配置,把主要的拷贝到我们的build.gradle. 这个非常简单,配置比较多,我就不再这里贴出来了。注意一定要记得修改ext下的 tinkerOldApkPath tinkerApplyResourcePath等属性。

第二步,把工程目录下的build.gradle加入Tinker plugin:

dependencies {
    classpath 'com.android.tools.build:gradle:2.2.3'
    classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}

其中TINKER_VERSION是在gradle.properties文件里配置的:

TINKER_VERSION=1.7.7

第三步,把一些需要的类拷贝到自己的工程,我新建了一个tinker package专门放tinker相关的类,如下图所示:

这里写图片描述

第四步,通过上面的tinker-sample-android的例子,我们知道我们需要在AndroidManifest.xml清单文件中配置Tinker生成的Application(SampleApplication),如下所示:

<application
    android:name=".app.SampleApplication"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme"/>

如果我们要修改这个生成的Application的名字或者报名,怎么修改呢?找到SampleApplicationLike把他的注解修改下即可:

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
                  flags = ShareConstants.TINKER_ENABLE_ALL,
                  loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike

现在是在自己的项目中使用,首先Application的包名肯定要换的,我改成了com.hoolay.app.SampleApplication
我的项目中本来就存在了自己定义的Application(HoolayApplication)了,那怎么办呢?
直接让HoolayApplication继承自Tinker生成的SampleApplication即可。可能找不到SampleApplication这个类,需要重新rebuild一下,这个类就自动生成了。

配置完了AndroidManifest.xml里的Application,记得配置Tinker Service:

<!--Tinker -->
<service
    android:name="com.hoolay.tinker.SampleResultService"
    android:exported="false"/>

到这里为止,我们的项目就已经把Tinker热修复的功能集成进来了。就下来就是测试了。

现在有bug的版本,已经上线了,用户正在使用,现在把这个bug修复一下(很简单加上判断即可):

if (UserManagerControl.isLogin()) {
    orderController.getShoppingCartCount();
}

为了不修改布局,我直接在点击购物车的时候加载patch了,便于测试用Toast提示:

String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk";
File patchFile = new File(patchPath);
if (patchFile.exists() && patchFile.length() > 0) {
    ToastUtils.showLongToast(getContext(),"正在合并热修复文件,请耐心等待");
    TinkerInstaller.onReceiveUpgradePatch(getContext().getApplicationContext(),
            patchPath);
}

要生成一个热修复的包(patch apk),怎么生成上面已经介绍了。然后把生成的patch_signed_7zip.apk放到SD 卡根目录去。

进入艺术品详情提示拒绝访问,然后点击底部的购物车按钮,提示正在合并热修复文件,请耐心等待,等待一段时间后,提示patch success,please restart process,流程如下图所示:

这里写图片描述

最后呢,我们重启App 进程,进入艺术品详情发现不会弹出拒绝访问提示,这说明热修复成功了。

至此,我们就把一个没有Tinker热更新的项目,改造成支持热更新功能了。

需要注意的是,一般正式的项目不会这样去需要用户点击某个按钮来加载修复包(patch apk),而是用户一开始进入我们app的时候,第一个界面就应该去服务器访问(一般一个App中都会有一个接口,用来获取服务器给客户端的全局配置)。

假设服务器返回有更新的包,应该就应该弹框去下载patch包,这个时候用户是什么都做不了的,最好加个进度条。当下载完成后,然后合并patch包,最后在跳到主界面。

最好使用HTTPS保证数据的安全,当我们访问服务器是否有修复包的时候,如果有,服务器应该返回修复包patch的下载地址和文件的MD5值,然后客户端下载完成后对文件进行MD5,然后和服务器的返回的MD5值进行对比看是否一致,因为文件可能被篡改。

未完待续

1、接下来测试热修改资源文件,如图片的热更新

2、测试SO文件、第三方库的热更新

3、混淆的情况下的热更新

4、一个Old apk下,做多次热更新

5、Tinker和不同厂商加固不兼容问题

6、如何兼容多渠道包?

7、开发中应该保留哪些文件,做好备份。

8、如何使生成的补丁包更小

猜你喜欢

转载自blog.csdn.net/johnny901114/article/details/54934782