在此之前一直在苦逼的coding,不断的增加功能,迭代,这是当前移动互联网初创团队的标准模式。虽然我们不是创业公司,但是我们的团队就是传统公司的移动互联网创业团队,在移动互联网保险没有发展起来的时期快速上线,快速迭代,抢占市场。
从去年11月上线到现在8个月的时间app从无到有,从0个用户到现在360+万,从0元保费到现在1500+万。app算是步入正规,当前首要任务是保证app线上的稳定,虽然我们不管是开发人员还是测试人员都非常努力的去找bug,但是还是不能避免线上问题,bug是永远消灭不完的。
我们为了线上稳定决定增加热修复框架,很多人都去自己开发,但是我觉得专业的事情还是交给专业的人,这样我们可以专注的干对于团队更重要的事。如下是我调研的目前主流的热修复框架(robust,bugly+tinker,sophix)。
在此我只是写如何选择热修复框架,具体的接入文档我觉得在这里写没有任何意义,官方的接入文档比我讲的详细多了,如果你官方的文档都看不懂我在这里更加说不清楚。
阿里云对热修复框架的比较如下图:
我觉得目前,只看bugly+tinker、robust、sophix这三大类就可以了,其他的兼容性都会有些问题。
接入复杂性和易用性:
美团的Robust接入复杂性最高,需要修改的地方很多而且零散,而且对于差异化的代码还需要使用注释来标注一下,这样易用性就下降了不少。
如何接入Robust如下步骤:
1.在App的build.gradle,加入如下依赖
apply plugin: 'com.android.application'
//制作补丁时将这个打开,auto-patch-plugin紧跟着com.android.application
//apply plugin: 'auto-patch-plugin'
apply plugin: 'robust'
compile 'com.meituan.robust:robust:0.4.5'
2.在整个项目的build.gradle加入classpath
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.meituan.robust:gradle-plugin:0.4.5'
classpath 'com.meituan.robust:auto-patch-plugin:0.4.5'
}
}
3.需要在项目的src同级目录下配置部分配置robust.xml文件,具体项请参考app/robust.xml,在这里面有多个配置项。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<switch>
<!--true代表打开Robust,请注意即使这个值为true,Robust也默认只在Release模式下开启-->
<!--false代表关闭Robust,无论是Debug还是Release模式都不会运行robust-->
<turnOnRobust>true</turnOnRobust>
<!--<turnOnRobust>false</turnOnRobust>-->
<!--是否开启手动模式,手动模式会去寻找配置项patchPackname包名下的所有类,自动的处理混淆,然后把patchPackname包名下的所有类制作成补丁-->
<!--这个开关只是把配置项patchPackname包名下的所有类制作成补丁,适用于特殊情况,一般不会遇到-->
<!--<manual>true</manual>-->
<manual>false</manual>
<!--是否强制插入插入代码,Robust默认在debug模式下是关闭的,开启这个选项为true会在debug下插入代码-->
<!--但是当配置项turnOnRobust是false时,这个配置项不会生效-->
<!--<forceInsert>true</forceInsert>-->
<forceInsert>false</forceInsert>
<!--是否捕获补丁中所有异常,建议上线的时候这个开关的值为true,测试的时候为false-->
<catchReflectException>true</catchReflectException>
<!--<catchReflectException>false</catchReflectException>-->
<!--是否在补丁加上log,建议上线的时候这个开关的值为false,测试的时候为true-->
<!--<patchLog>true</patchLog>-->
<patchLog>false</patchLog>
<!--项目是否支持progaurd-->
<proguard>true</proguard>
<!--<proguard>false</proguard>-->
</switch>
<!--需要热补的包名或者类名,这些包名下的所有类都被会插入代码-->
<!--这个配置项是各个APP需要自行配置,就是你们App里面你们自己代码的包名,
这些包名下的类会被Robust插入代码,没有被Robust插入代码的类Robust是无法修复的-->
<packname name="hotfixPackage">
<name>com.meituan</name>
<name>com.sankuai</name>
<name>com.dianping</name>
<name>com.pa.health</name>
</packname>
<!--不需要Robust插入代码的包名,Robust库不需要插入代码,如下的配置项请保留,还可以根据各个APP的情况执行添加-->
<exceptPackname name="exceptPackage">
<name>com.meituan.robust</name>
</exceptPackname>
<!--补丁的包名,请保持和类PatchManipulateImp中fetchPatchList方法中设置的补丁类名保持一致( setPatchesInfoImplClassFullName("com.meituan.robust.patch.PatchesInfoImpl")),
各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是如下的配置项,类名必须是:PatchesInfoImpl-->
<patchPackname name="patchPackname">
<name>com.pa.health</name>
</patchPackname>
<!--自动化补丁中,不需要反射处理的类,这个配置项慎重选择-->
<noNeedReflectClass name="classes no need to reflect">
</noNeedReflectClass>
</resources>
4.需要保存打包时生成的mapping文件以及build/outputs/robust/methodsMap.robust文件。
5.在Java中,合成补丁、下载补丁包、补丁包存放,补丁包安全验证、补丁包版本管理都需要开发者进行管理,如下代码
new PatchExecutor(getApplicationContext(), new PatchManipulateImp(), new RobustCallBack() {
@Override
public void onPatchListFetched(boolean result, boolean isNet) {
}
@Override
public void onPatchFetched(boolean result, boolean isNet, Patch patch) {
}
@Override
public void onPatchApplied(boolean result, Patch patch) {
File file = new File(patch.getLocalPath());
file.delete();
}
@Override
public void logNotify(String log, String where) {
}
@Override
public void exceptionNotify(Throwable throwable, String where) {
}
}).start();
public class PatchManipulateImp extends PatchManipulate {
/***
* connect to the network ,get the latest patches
* l联网获取最新的补丁
* @param context
*
* @return
*/
@Override
protected List<Patch> fetchPatchList(Context context) {
//将app自己的robustApkHash上报给服务端,服务端根据robustApkHash来区分每一次apk build来给app下发补丁
//apkhash is the unique identifier for apk,so you cannnot patch wrong apk.
//String robustApkHash = RobustApkHashUtils.readRobustApkHash(context);
//connect to network to get patch list on servers
//在这里去联网获取补丁列表
Patch patch = new Patch();
patch.setName("123");
//we recommend LocalPath store the origin patch.jar which may be encrypted,while TempPath is the true runnable jar
//LocalPath是存储原始的补丁文件,这个文件应该是加密过的,TempPath是加密之后的,TempPath下的补丁加载完毕就删除,保证安全性
//这里面需要设置一些补丁的信息,主要是联网的获取的补丁信息。重要的如MD5,进行原始补丁文件的简单校验,以及补丁存储的位置,这边推荐把补丁的储存位置放置到应用的私有目录下,保证安全性
patch.setLocalPath(Environment.getExternalStorageDirectory().getPath() + File.separator + "robust" + File.separator + "pahealth" + File.separator + "patch");
//setPatchesInfoImplClassFullName 设置项各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是和xml配置项patchPackname保持一致,而且类名必须是:PatchesInfoImpl
//请注意这里的设置
patch.setPatchesInfoImplClassFullName("com.pa.health.PatchesInfoImpl");
List patches = new ArrayList<Patch>();
patches.add(patch);
return patches;
}
/**
* @param context
* @param patch
* @return you can verify your patches here
*/
@Override
protected boolean verifyPatch(Context context, Patch patch) {
//do your verification, put the real patch to patch
//放到app的私有目录
patch.setTempPath(context.getCacheDir() + File.separator + "robust" + File.separator + "patch");
//in the sample we just copy the file
try {
copy(patch.getLocalPath(), patch.getTempPath());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("copy source patch to local patch error, no patch execute in path " + patch.getTempPath());
}
return true;
}
public void copy(String srcPath, String dstPath) throws IOException {
File src = new File(srcPath);
if (!src.exists()) {
throw new RuntimeException("source patch does not exist ");
}
File dst = new File(dstPath);
if (!dst.getParentFile().exists()) {
dst.getParentFile().mkdirs();
}
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
// Transfer bytes from in to out
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
/**
* @param patch
* @return you may download your patches here, you can check whether patch is in the phone
*/
@Override
protected boolean ensurePatchExist(Patch patch) {
Log.e("ensurePatchExist","ensurePatchExist");
return true;
}
}
如何使用Robust如下步骤:
1.使用插件时,需要把auto-patch-plugin放置在com.android.application插件之后,其余插件之前。
apply plugin: 'com.android.application'
apply plugin: 'auto-patch-plugin'
2.将保存下来的mapping文件和methodsMap.robust文件放在app/robust/文件夹下。
3.修改代码,在改动的方法上面添加@Modify
注解或者在修改的方法里面调用RobustModify.modify()(针对Lambda表达式)
@Modify
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
} //或者是被修改的方法里面调用RobustModify.modify()方法 protected
void onCreate(Bundle savedInstanceState)
{
RobustModify.modify()
super.onCreate(savedInstanceState);
}
新增的方法和字段使用@Add注解
//增加方法
@Add
public String getString() {
return "Robust";
}
//增加类
@Add
public class NewAddCLass {
public static String get() {
return "robust";
}
}
4.运行和生成线上apk同样的命令,即可生成补丁,补丁目录app/build/outputs/robust/patch.jar
5.补丁制作成功后会停止构建apk,出现类似于如下的提示,表示补丁生成成功 [图片上传失败...(image-446c06-1514882647844)]
以上是接入和使用robust的简要步骤详细步骤请点击这里。
微信的tinker接入性和易用性也很复杂请看这里,但是由于bugly对他进行了封装,接入性和易用性增加了不少,而且增加了补丁包的管理后台,后台管理有很多配置可以设置,例如版本管理、根据手机系统版本进行下发补丁包等。
如何接入Tinker如下步骤:
1.添加插件依赖
工程根目录下“build.gradle”文件中添加:
buildscript {
repositories {
jcenter()
}
dependencies {
// tinkersupport插件, 其中lastest.release指拉取最新版本,也可以指定明确版本号,例如1.0.4
classpath "com.tencent.bugly:tinker-support:1.0.8"
}
}
2.集成SDK
在app module的“build.gradle”文件中添加(示例配置):
android {
defaultConfig {
ndk {
//设置支持的SO库架构
abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
}
dependencies {
compile "com.android.support:multidex:1.0.1" // 多dex配置
//注释掉原有bugly的仓库
//compile 'com.tencent.bugly:crashreport:latest.release'//其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.3.2
compile 'com.tencent.bugly:crashreport_upgrade:1.3.1'
compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.2.0
}
在app module的“build.gradle”文件中添加:
// 依赖插件脚本
apply from: 'tinker-support.gradle'
tinker-support.gradle内容请点击(示例配置)
3.Java代码初始化SDK
我使用的是enableProxyApplication = true 的情况,无须你改造Application,主要是为了降低接入成本,我们插件会动态替换AndroidMinifest文件中的Application为我们定义好用于反射真实Application的类(需要您接入SDK 1.2.2版本 和 插件版本 1.0.3以上)。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
// 调试时,将第三个参数改为true
Bugly.init(this, "900029763", false);
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// you must install multiDex whatever tinker is installed!
MultiDex.install(base);
// 安装tinker
Beta.installTinker();
}
}
4.配置AndroidManifest.xml
- 权限配置
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- Activity配置
<activity
android:name="com.tencent.bugly.beta.ui.BetaActivity"
android:configChanges="keyboardHidden|orientation|screenSize|locale"
android:theme="@android:style/Theme.Translucent" />
5.混淆
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
如何使用Tinker如下步骤:
1.打基准包
1.主要是修改tinker-support.gradle中的tinkerId = "base-1.0.1"
2.正常打包生成基准包
2.打补丁包
1.修改代码
2.修改tinker-support.gradle中的tinkerId = "patch-1.0.1"
3.修改tinker-support.gradle中的baseApkDir = "app-0705-18-32-19",这个目录是基准包生成的目录
如下图:
4.执行构建补丁包的task
5.生成的补丁包在build/outputs/patch目录下:
6.上传补丁包到bugly
以上是接入和使用Tinker+bugly的简要步骤详细步骤请点击这里。
阿里的sophix绝对是傻瓜式的接入和使用方式,生成补丁包也是图形化界面,oldapk和newapk上传图形工具生成补丁包。
如何接入Sophix如下步骤:
1.gradle远程仓库依赖, 打开项目找到app的build.gradle文件,添加如下配置:
添加maven仓库地址:
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/repositories/releases"
}
}
添加gradle坐标版本依赖:
compile 'com.aliyun.ams:alicloud-android-hotfix:3.0.5'
2.权限说明
Sophix SDK使用到以下权限
<! -- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<! -- 外部存储读权限,调试工具加载本地补丁需要 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
3.配置AndroidManifest文件
<meta-data
android:name="com.taobao.android.hotfix.IDSECRET"
android:value="App ID" />
<meta-data
android:name="com.taobao.android.hotfix.APPSECRET"
android:value="App Secret" />
<meta-data
android:name="com.taobao.android.hotfix.RSASECRET"
android:value="RSA密钥" />
4.混淆配置
#基线包使用,生成mapping.txt
-printmapping mapping.txt
#生成的mapping.txt在app/buidl/outputs/mapping/release路径下,移动到/app路径下
#修复后的项目使用,保证混淆结果一致
#-applymapping mapping.txt
#hotfix
-keep class com.taobao.sophix.**{*;}
-keep class com.ta.utdid2.device.**{*;}
#防止inline
-dontoptimize
5.Java接入范例
initialize的调用应该尽可能的早,必须在Application.attachBaseContext()或者Application.onCreate()的最开始进行SDK初始化操作,否则极有可能导致崩溃。而查询服务器是否有可用补丁的操作可以在后面的任意地方。
// initialize最好放在attachBaseContext最前面
SophixManager.getInstance().setContext(this)
.setAppVersion(appVersion)
.setAesKey(null)
.setEnableDebug(true)
.setPatchLoadStatusStub(new PatchLoadStatusListener() {
@Override
public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
// 补丁加载回调通知
if (code == PatchStatus.CODE_LOAD_SUCCESS) {
// 表明补丁加载成功
} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
// 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;
// 建议: 用户可以监听进入后台事件, 然后应用自杀
} else if (code == PatchStatus.CODE_LOAD_FAIL) {
// 内部引擎异常, 推荐此时清空本地补丁, 防止失败补丁重复加载
// SophixManager.getInstance().cleanPatches();
} else {
// 其它错误信息, 查看PatchStatus类说明
}
}
}).initialize();
// queryAndLoadNewPatch不可放在attachBaseContext 中,否则无网络权限,建议放在后面任意时刻,如onCreate中
SophixManager.getInstance().queryAndLoadNewPatch();
如何使用Sophix如下步骤:
1.下载打包工具
patch补丁包生成需要使用到打补丁工具SophixPatchTool, 如还未下载打包工具,请前往下载Android打包工具。
Mac版本打包工具地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/SophixPatchTool_macos.zip
Windows版本打包工具地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/SophixPatchTool_windows.zip
Linux版本打包工具地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/SophixPatchTool_linux.zip
调试工具地址:http://ams-hotfix-repo.oss-cn-shanghai.aliyuncs.com/hotfix_debug_tool-release.apk
该工具提供了Windows和macOS和Linux版本,Windows下运行SophixPatchTool.exe,macOS下运行SophixPatchTool.app,Linux下(Ubuntu 16.04 64bit最佳)运行SophixPatchTool。并且需要安装Java环境且在JDK7或以上才能正常使用。
2.生成Patch
- 旧包:<必填> 选择基线包路径(有问题的APK)。
- 新包:<必填> 选择新包路径(修复过该问题APK)。
- 日志:打开日志输出窗口。
- 高级:高级选项
- 设置:配置其他信息。
- GO!:开始生成补丁。
3.上传补丁包到后台
以上是接入和使用Sophix的简要步骤详细步骤请点击这里。
总结:
bugly+tinker、robust、sophix。
robust是没有后台的需要自己开发而且接入和使用比较复杂,所以没有选择。
sophix还是内测版,我用的是内测账号虽然很好用但是将来需要收费而且没有正式发布所以放弃了。
最终选择了bugly+tinker微信上10亿用户验证过而且后台使用也比较方便,顺便还能管理bug。
遇到的问题:
深坑一
在使用Bugly的时候发现我提交的补丁包版本是2.4.0.214,但是我过几天再次登录查看发现 变成了2.6.0.226了这是我最新的测试包的版本如下图:
这还了得影响了几百万用户,于是马上联系bugly的客服是如下解释的:
这个问题原因 是您测试时候使用了2.6.0版本和你当前2.4.0版本的tinkerid一样导致
也就是我versionName和versionCode从2.4.0-214,改成了2.6.0-226了,但是我的tinkerid和原来一样没有改变,这就导致bugly后台的目标版本号改成了2.6.0-226,好在这两个版本的apk都可以收到这个补丁包没有影响。
这个问题说明了versionName、versionCode和tinkerid一定要同事改掉。
深坑二
昨天又使用Bugly+Tinker,除了发现上面的问题,还发现看日志补丁包下载成功然后合并失败,报错如下:
遇见这个错误我将这个
libgetuiext2.so
屏蔽了不生成补丁包,接着报如下错误:这两个错误原因是,安装到手机上的apk是我用git代码直接编译生成的,但是补丁包生成使用的是之前发布的apk所以造成基准安装包不一致,一直在报错。
搞明白这个原因之后,我用发布加固之前的apk安装在手机上,在用这个apk生成的补丁包确实合并成功了,但是又出现下一个问题,在应用市场上下载的apk也是合并失败,这回日志就更少了,因为是release包,想了很长时间,晚上加班搞到半夜也没发现原因。第二天早上继续看文档发现我为了屏蔽
.so
文件将tinkerSupport{ overrideTinkerPatchConfiguration = false }
设置成false
,也就是使用tinkerPatch{}
中的配置,但是这个配置没有设置加固属性isProtectedApp = true
,抱着最后的希望在tinkerPath{ buildConfig{ isProtectedApp = true}}
增加了isProtectedApp = true
,然后发布补丁包,居然好使了fuck,折腾到半夜加上一上午总结出两个坑
- 手机安装的apk和生成补丁的apk一定要是一个
- 加固之后发布的apk一定要设置
isProtectedApp = true