8,创建补丁文件
创建补丁文件的时候需要线上的 apk。所以在这里 我们将刚才打包的 apk 名字复制下来,然后放在build.gradle 中的 ext 中,如下所示:
这里的路径就是 build 中 bakApk 中的文件,第一个是 apk,第二个是混淆路径,因为我没有使用混淆,所以在上面打包后就没有混淆文件。第三个对应资源文件。注意这里一定要填正确,否则会导致不能生成补丁文件。弄完之后同步一下。
接着就可以修复bug了。这里我们进行模拟一下。
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <androidx.appcompat.widget.AppCompatButton android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="50dp" android:layout_marginTop="20dp" android:text="加载 PATCH 包" android:textSize="20sp" tools:ignore="HardcodedText" /> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:text="我是被修复的bug" android:textSize="25sp" /> </androidx.appcompat.widget.LinearLayoutCompat>
修改了一下布局,添加了一个 TextView。
接着就需要使用 tinker 插件生成补丁了。如下
点击 tinkerPatchRelease 即可生成 补丁包,如果出现 IO 异常,可以删除 build 文件夹中除过 bakApk 文件以外的所有文件,可能是有冲突。
或者是 出现 config 的错误:则在 gradle 的 android 包下添加如下:
signingConfigs { config { storeFile file('C:\\Users\\Administrator\\Desktop\\345\\Project\\Tinker\\keystore.jks') storePassword '123456789' keyAlias = 'key0' keyPassword '123456789' } }
因为使用 tinker 插件打包的时候也需要 key 和 密码等。
最后生成的结果如下
9,加载补丁文件,修复bug
其中 patch_signed.apk 就是我们需要的补丁包。我们需要将补丁包复制到的我们程序中定义的路径中:
这里我用的电脑直接复制到手机中了
然后点击加载 加载 PATCH 包 接着程序会直接退出。当你再次打开的时候就会发现你添加的 TextView 已经显示出来了,最终的结果如下:
经过上面几步,我们就已经完成了一个从本地加载的热修复
在项目中使用 Tinker
当然了,我们不可能一直从本地加载补丁文件。所以我们需要对加载 补丁文件进行修改一下。
新建一个 服务。在服务中我们会进行请求服务器是否有新的补丁,如果有补丁就下载到指定的目录中,然后进行加载补丁文件。
public class TinkerService extends Service { /** * 文件后缀名 */ private static final String FILE_END = ".apk"; /** * 下载 patch 文件信息 */ private static final int DOWNLOAD_PATCH = 0x01; /** * 检查是否有 patch 更新 */ private static final int UPDATE_PATCH = 0x02; /** * patch 要保存的文件夹 */ private String mPatchFileDir; /** * patch 文件保存路径 */ private String mFilePath; /** * 服务器 patch 的信息 */ private BasePatch mBasePatchInfo; @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case UPDATE_PATCH: checkPatchInfo(); break; case DOWNLOAD_PATCH: downloadPatch(); break; } } }; private void downloadPatch() { //下载补丁文件 mFilePath = mPatchFileDir.concat("tinker") .concat(FILE_END); //.....下载完成,进行加载 TinkerManager.loadPatch(mFilePath); //加载完成后终止服务 stopSelf(); } @Override public void onCreate() { super.onCreate(); init(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { //检查是否有 patch 更新 mHandler.sendEmptyMessage(UPDATE_PATCH); return START_NOT_STICKY; } private void init() { mPatchFileDir = getExternalCacheDir().getAbsolutePath() + "/tPatch/"; File patchFileDir = new File(mPatchFileDir); try { if (!patchFileDir.exists()) { //文件夹不存在则创建 patchFileDir.mkdir(); } } catch (Exception e) { e.printStackTrace(); //无法创建文件,终止服务 stopSelf(); } } @Override public IBinder onBind(Intent intent) { return null; } /** * 检查是否有更新 */ private void checkPatchInfo() { //.....网络请求获取是否更新补丁文件,不更新就终止服务 //下载补丁文件 mHandler.sendEmptyMessage(DOWNLOAD_PATCH); } }
Tinker 高级用法
Tinker 如何支持多渠道打包
首先在项目中集成多渠道,我使用的是 walle,集成的方式非常简单:
添加插件
classpath 'com.meituan.android.walle:plugin:1.1.6'
在 app 的 build.gradle 中引入插件,设置依赖,进行配置
apply plugin: 'walle' android{ ...... walle { // 指定渠道包的输出路径 apkOutputFolder = new File("${project.buildDir}/outputs/channels"); // 定制渠道包的APK的文件名称 apkFileNameFormat = '${appName}-${channel}-${buildType}-v${versionName}-${versionCode}.apk'; // 渠道配置文件 channelFile = new File("${project.getProjectDir()}/channel") } } dependencies { implementation 'com.meituan.android.walle:library:1.1.6' }
然后在 app 中新建一个channel 文件,不要指定任何后缀,里面写入渠道名称即可
这样多渠道就配置完了。
使用多渠道后打包需要用命令行或者插件来执行,这里使用插件的方式,如下:
一种是 debug,一种是 release。点击 replace就会生成相应的渠道文件,如下:
可以看到渠道已经生成了。在bakApk 中也生成了 基准包,这个 apk 不是用来发布的,而是用来生成补丁用的。生成补丁的方式就和上面讲的一样了。
将渠道文件中的 apk 发布后,出现 bug 。使用基准包生成 补丁文件,然后放在服务器中,进行下发即可。
如果使用 android 自带的渠道,需要每个渠道apk 都要生成补丁文件,而 使用 walle 只需要一个补丁即可,而且 使用 walle 生成渠道apk 的速度非常快。
我们可以app中获取渠道的信息,并且传给 友盟统计,这样非常方便,如下:
//当前渠道 String channel = WalleChannelReader.getChannel(getApplication()); UMConfigure.init(getApplication(), "*********", channel, UMConfigure.DEVICE_TYPE_PHONE, "");
如何自定义 Tinker 行为
1,自定义TinkerResultService 改变 patch 安装成功后行为
一般情况下 patch 安装后 tinker 会杀掉进程,所以我们才会看到 patch 加载完成后程序闪退的问题,我们可以通过 重写 DefaultTinkerResultService 来自定义这个行为,如下:
在 DefaultTinkerResultService 的 onPatchResult 方法中 判断了 patch 安装成功后的行为。如下:
@Override public void onPatchResult(PatchResult result) { ...... // only main process can load an upgrade patch! if (result.isSuccess) { //加载完成后删除 patch 文件 deleteRawPatchFile(new File(result.rawPatchFilePath)); //如果成功,则杀掉进程 if (checkIfNeedKill(result)) { android.os.Process.killProcess(android.os.Process.myPid()); } else { TinkerLog.i(TAG, "I have already install the newly patch version!"); } } }
从源码可以很清楚地看到杀掉进程。我们可以通过继承的方式来解决如下:
public class CustomResultService extends DefaultTinkerResultService { private static final String TAG = "CustomResultService"; //返回 patch 文件的安装结果 @Override public void onPatchResult(PatchResult result) { ...... // 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)) { TinkerLog.e(TAG, "patch加载成功,重启生效"); } else { TinkerLog.i(TAG, "I have already install the newly patch version!"); } } } }
其实和源码中差不多,只是改了一句而已。加载成功后打印 log 即可。
弄完以后,我们需要修改一下初始化方法。我们是在 TinkerMananger 的 installTinker 方法中初始化的,下面我们进行修改,如下:
LoadReporter loadReporter = new DefaultLoadReporter(applicationLike.getApplication()); PatchReporter patchReporter = new DefaultPatchReporter(applicationLike.getApplication()); PatchListener patchListener = new DefaultPatchListener(applicationLike.getApplication()); AbstractPatch upgradePatchProcessor = new UpgradePatch(); //完成 tinker 初始化 TinkerInstaller.install(applicationLike, loadReporter, patchReporter, patchListener, CustomResultService.class, upgradePatchProcessor);
CustomResultService 就是我们自定义的。将他传入即可,注意这是一个服务,必须在 AndroidManifest.xml 中注册
结果如下:
2019-11-21 16:59:42.335 6337-6404/? E/CustomResultService: patch加载成功,重启生效
2,自定义 PatchListener 监听 patch receiver 事件
也就是上面代码中的 DefaultPatchListener ,继承他即可,使用它可以完成 patch 的校验
例如,从服务器拉取补丁时服务器返回一个 md5, 我们可以在这个里面进行判断。
/** * 1,校验 patch 文件是否合法,2,启动 Service 去安装 patch */ public class CustomPatchListener extends DefaultPatchListener { private String currentMD5; public void setCurrentMD5(String currentMD5) { this.currentMD5 = currentMD5; } public CustomPatchListener(Context context) { super(context); } @Override protected int patchCheck(String path, String patchMd5) { if (Utils.isFileMD5Matched(path,currentMD5)){ return ShareConstants.ERROR_PATCH_DISABLE; } return super.patchCheck(path, patchMd5); } }
同样的要将初始化方法中第四个参数改为这个类对象
3,其他的自定义
//加载补丁文件加载时的异常监听 LoadReporter loadReporter = new DefaultLoadReporter(applicationLike.getApplication()); //补丁文件合成阶段的异常监听, PatchReporter patchReporter = new DefaultPatchReporter(applicationLike.getApplication()); PatchListener patchListener = new DefaultPatchListener(applicationLike.getApplication()); AbstractPatch upgradePatchProcessor = new UpgradePatch(); //完成 tinker 初始化 TinkerInstaller.install(applicationLike, loadReporter, patchReporter, patchListener, CustomResultService.class, upgradePatchProcessor);
通过继承 loadReporter 和 patchReporter 可以实现异常的监听。当然还有一下其他的,可以去官网查看。
总结一下
热修复这块基本的用法基本都已经掌握了。总结如下:
在学习任何一个东西时一定要先看官方的文档,不然吃大亏,就和我一样,搞了好多天。。如果项目中需要用到热修复,不要着急的直接选使用哪个,要从需求和一下客观因素来觉得要使用那种,在满足需求的条件下哪个学习的成本低,就学哪个。学习成本差不多的建议选择大公司的解决方案。
关于tinker的使用,例如上面这种就比较麻烦。但是有了 bugly 以后感觉挺简单的。毕竟也是免费的。tinker 使用的是冷启动,下发一次补丁需要十多分钟,撤回的话没怎么测。但是配合 walle 一起使用效果还是挺好的。
在使用 bugly 的时候发现在有些手机上使用失败的问题。例如我的小米6。。。在 bugly 后台上传 补丁包一直显示 版本对应不上。我还以为是代码问题,结果改代码改了一两天。。最后用了一个模拟器居然成功了。。无奈啊
注意热修复的作用不是替代版本的迭代。他只是为了在线上的 app 出现问题后的一种解决方式。按照一般流程重新进行版本的迭代就会浪费大量的时间,而且还指不定用户会不会更新。因为如此,所以才有了热修复。但是不能将他和迭代混为一谈。
关于上面 tinker 的用法我写了一个demo,demo中也集成了walle和友盟统计。bugly就不传了,官方文档非常详细。但是注意文档上的依赖版本不是最新的。这个亏我已经吃了。。。
其实学完以后才发现这种用法已经比较老了。所以我还是觉定用 bugly。他内部也是用的 tinker ,使用起来比较简单,当前一定要认真看文档,比如说我。。。吃了不少亏