先到 Github 上下载,里面包含了
,使用 AndroidStudio 导入该例子工程即可。
- tinker-sample-android
导入工程后,运行程序 ,出现如下错误:
- 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:
- 热修复功能
- 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
如下图所示:
就相当于用户正在使用的 APK(old apk),后面用到的 patch apk 都是基于 old apk 和 new apk 的不同 (diff) 生成的。
- app-debug-0207-15-28-17.apk
R.txt 也是用于生成 patch apk 的。
- app-debug-0207-15-28-17-R.txt
介绍完了
下的两个文件,还需要配置
- 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
好了,最基本的配置就介绍到这里了,现在我们来修复一下上面
的 bug,很简单,直接让出一个不为零的除数就可,代码如下所示:
- 除数为0
- 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");
我们只要把
文件放到 SD 卡的根目录就可以了,然后点击按钮
- patch_signed_7zip.apk
,如果 不出意外的话,一会就会弹出 Toast(
- LOAD PATCH
)
- patch success,please restart process
然后杀死进程,重新进入 app,点击 Button(DIVISION),弹出 Toast:
至此,通过 Tinker 实现了热修复功能。
上面是我们直接运行 Tinker 官方的 sample 对 Tinker 的使用有了个了解,现在如何在一个全新的工程使用 Tinker。就以我以前做过的一个项目为例,该项目是我最近一个公司的产品,主要是做艺术品电商这块的(出现了一个小 bug),当我进入艺术品详情后界面出弹出
问的 Toast,如下图所示:
- 拒绝访
通过查看代码发现犯了一个低级错误,因为在界面里需要访问购物车的数量,然后显示在界面购车图标的右上角,忘记了判断用户是否登录,竟然犯了这种低级错误,真是老脸一红啊,但是毕竟是自己的错误,就要大胆的承认,知耻而后勇,哈哈。有 bug 的代码如下(再调用 API 之前应该判断用户是否登录):
- @Override
- public void onResume() {
- super.onResume();
- //if (UserManagerControl.isLogin()) {应该加上判断
- orderController.getShoppingCartCount();
- //}
- }
要想加入热修复功能,
第一步,加入 Tinker 的依赖包,我直接参考
app 下的 build.gralde 配置,把主要的拷贝到我们的 build.gradle. 这个非常简单,配置比较多,我就不再这里贴出来了。注意一定要记得修改 ext 下的
- tinker-sample-android
- 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
- }
其中
是在 gradle.properties 文件里配置的:
- TINKER_VERSION
- TINKER_VERSION=1.7.7
第三步,把一些需要的类拷贝到自己的工程, 我新建了一个 tinker package 专门放 tinker 相关的类,如下图所示:
第四步,通过上面的
的例子,我们知道我们需要在
- tinker-sample-android
清单文件中配置 Tinker 生成的 Application(SampleApplication),如下所示:
- AndroidManifest.xml
- ".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 的包名肯定要换的,我改成了
我的项目中本来就存在了自己定义的 Application(HoolayApplication)了,那怎么办呢?
- com.hoolay.app.SampleApplication
配置完了
里的 Application,记得配置 Tinker Service:
- AndroidManifest.xml
- <!--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),怎么生成上面已经介绍了。然后把生成的
放到 SD 卡根目录去。
- patch_signed_7zip.apk
进入艺术品详情提示
,然后点击底部的
- 拒绝访问
按钮,提示
- 购物车
,等待一段时间后,提示
- 正在合并热修复文件,请耐心等待
,流程如下图所示:
- patch success,please restart process
最后呢,我们重启 App 进程,进入
发现不会弹出
- 艺术品详情
提示,这说明热修复成功了。
- 拒绝访问
至此,我们就把一个没有 Tinker 热更新的项目,改造成支持热更新功能了。
需要注意的是,一般正式的项目不会这样去需要用户点击某个按钮来加载修复包 (patch apk),而是用户一开始进入我们 app 的时候,第一个界面就应该去服务器访问(一般一个 App 中都会有一个接口,用来获取服务器给客户端的全局配置)。
假设服务器返回有更新的包,应该就应该
去下载 patch 包,这个时候用户是什么都做不了的,最好加个进度条。当下载完成后,然后合并 patch 包,最后在跳到主界面。
- 弹框
最好使用 HTTPS 保证数据的安全,当我们访问服务器是否有修复包的时候,如果有,服务器应该返回修复包 patch 的下载地址和文件的 MD5 值,然后客户端下载完成后对文件进行 MD5,然后和服务器的返回的 MD5 值进行对比看是否一致,因为文件可能被篡改。
来源: