在此之前一直在苦逼的coding因悲,不斷的增加功能,迭代中贝,這是當前移動互聯(lián)網(wǎng)初創(chuàng)團隊的標準模式囤捻。雖然我們不是創(chuàng)業(yè)公司,但是我們的團隊就是傳統(tǒng)公司的移動互聯(lián)網(wǎng)創(chuàng)業(yè)團隊,在移動互聯(lián)網(wǎng)保險沒有發(fā)展起來的時期快速上線蝎土,快速迭代视哑,搶占市場。
從去年11月上線到現(xiàn)在8個月的時間app從無到有誊涯,從0個用戶到現(xiàn)在360+萬挡毅,從0元保費到現(xiàn)在1500+萬。app算是步入正規(guī)暴构,當前首要任務是保證app線上的穩(wěn)定跪呈,雖然我們不管是開發(fā)人員還是測試人員都非常努力的去找bug,但是還是不能避免線上問題取逾,bug是永遠消滅不完的耗绿。
我們?yōu)榱司€上穩(wěn)定決定增加熱修復框架,很多人都去自己開發(fā)砾隅,但是我覺得專業(yè)的事情還是交給專業(yè)的人误阻,這樣我們可以專注的干對于團隊更重要的事。如下是我調(diào)研的目前主流的熱修復框架(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庫不需要插入代碼,如下的配置項請保留肩碟,還可以根據(jù)各個APP的情況執(zhí)行添加-->
<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中,合成補丁吨拍、下載補丁包褪猛、補丁包存放,補丁包安全驗證羹饰、補丁包版本管理都需要開發(fā)者進行管理伊滋,如下代碼
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聯(lián)網(wǎng)獲取最新的補丁
* @param context
*
* @return
*/
@Override
protected List<Patch> fetchPatchList(Context context) {
//將app自己的robustApkHash上報給服務端,服務端根據(jù)robustApkHash來區(qū)分每一次apk build來給app下發(fā)補丁
//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
//在這里去聯(lián)網(wǎng)獲取補丁列表
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下的補丁加載完畢就刪除馍资,保證安全性
//這里面需要設置一些補丁的信息筒主,主要是聯(lián)網(wǎng)的獲取的補丁信息。重要的如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
注解或者在修改的方法里面調(diào)用RobustModify.modify()(針對Lambda表達式)
@Modify
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
} //或者是被修改的方法里面調(diào)用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焕盟,出現(xiàn)類似于如下的提示秋秤,表示補丁生成成功 [圖片上傳失敗...(image-446c06-1514882647844)]
以上是接入和使用robust的簡要步驟詳細步驟請點擊這里。
微信的tinker接入性和易用性也很復雜請看這里脚翘,但是由于bugly對他進行了封裝灼卢,接入性和易用性增加了不少,而且增加了補丁包的管理后臺来农,后臺管理有很多配置可以設置鞋真,例如版本管理、根據(jù)手機系統(tǒng)版本進行下發(fā)補丁包等沃于。
如何接入Tinker如下步驟:
1.添加插件依賴
工程根目錄下“build.gradle”文件中添加:
buildscript {
repositories {
jcenter()
}
dependencies {
// tinkersupport插件, 其中l(wèi)astest.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'//其中l(wèi)atest.release指代最新版本號繁莹,也可以指定明確的版本號檩互,例如2.3.2
compile 'com.tencent.bugly:crashreport_upgrade:1.3.1'
compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中l(wèi)atest.release指代最新版本號,也可以指定明確的版本號咨演,例如2.2.0
}
在app module的“build.gradle”文件中添加:
// 依賴插件腳本
apply from: 'tinker-support.gradle'
tinker-support.gradle內(nèi)容請點擊(示例配置)
3.Java代碼初始化SDK
我使用的是enableProxyApplication = true 的情況闸昨,無須你改造Application,主要是為了降低接入成本雪标,我們插件會動態(tài)替換AndroidMinifest文件中的Application為我們定義好用于反射真實Application的類(需要您接入SDK 1.2.2版本 和 插件版本 1.0.3以上)零院。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 這里實現(xiàn)SDK初始化,appId替換成你的在Bugly平臺申請的appId
// 調(diào)試時村刨,將第三個參數(shù)改為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.執(zhí)行構建補丁包的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使用到以下權限
<! -- 網(wǎng)絡權限 -->
<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" />
<! -- 外部存儲讀權限募疮,調(diào)試工具加載本地補丁需要 -->
<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的調(diào)用應該盡可能的早,必須在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) {
// 補丁加載回調(diào)通知
if (code == PatchStatus.CODE_LOAD_SUCCESS) {
// 表明補丁加載成功
} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
// 表明新補丁生效需要重啟. 開發(fā)者可提示用戶或者強制重啟;
// 建議: 用戶可以監(jiān)聽進入后臺事件, 然后應用自殺
} else if (code == PatchStatus.CODE_LOAD_FAIL) {
// 內(nèi)部引擎異常, 推薦此時清空本地補丁, 防止失敗補丁重復加載
// SophixManager.getInstance().cleanPatches();
} else {
// 其它錯誤信息, 查看PatchStatus類說明
}
}
}).initialize();
// queryAndLoadNewPatch不可放在attachBaseContext 中卸耘,否則無網(wǎng)絡權限退敦,建議放在后面任意時刻,如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
調(diào)試工具地址: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環(huán)境且在JDK7或以上才能正常使用锭魔。
2.生成Patch
- 舊包:<必填> 選擇基線包路徑(有問題的APK)例证。
- 新包:<必填> 選擇新包路徑(修復過該問題APK)。
- 日志:打開日志輸出窗口赂毯。
- 高級:高級選項
- 設置:配置其他信息战虏。
- GO!:開始生成補丁。
3.上傳補丁包到后臺
以上是接入和使用Sophix的簡要步驟詳細步驟請點擊這里党涕。
總結:
bugly+tinker烦感、robust、sophix膛堤。
robust是沒有后臺的需要自己開發(fā)而且接入和使用比較復雜手趣,所以沒有選擇济炎。
sophix還是內(nèi)測版驰后,我用的是內(nèi)測賬號雖然很好用但是將來需要收費而且沒有正式發(fā)布所以放棄了烙心。
最終選擇了bugly+tinker微信上10億用戶驗證過而且后臺使用也比較方便款票,順便還能管理bug卵沉。
遇到的問題:
深坑一
在使用Bugly的時候發(fā)現(xiàn)我提交的補丁包版本是2.4.0.214它抱,但是我過幾天再次登錄查看發(fā)現(xiàn) 變成了2.6.0.226了這是我最新的測試包的版本如下圖:
這還了得影響了幾百萬用戶配阵,于是馬上聯(lián)系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,除了發(fā)現(xiàn)上面的問題郭膛,還發(fā)現(xiàn)看日志補丁包下載成功然后合并失敗晨抡,報錯如下:
遇見這個錯誤我將這個
libgetuiext2.so
屏蔽了不生成補丁包,接著報如下錯誤:這兩個錯誤原因是则剃,安裝到手機上的apk是我用git代碼直接編譯生成的耘柱,但是補丁包生成使用的是之前發(fā)布的apk所以造成基準安裝包不一致,一直在報錯忍级。
搞明白這個原因之后帆谍,我用發(fā)布加固之前的apk安裝在手機上,在用這個apk生成的補丁包確實合并成功了轴咱,但是又出現(xiàn)下一個問題汛蝙,在應用市場上下載的apk也是合并失敗,這回日志就更少了朴肺,因為是release包窖剑,想了很長時間,晚上加班搞到半夜也沒發(fā)現(xiàn)原因戈稿。第二天早上繼續(xù)看文檔發(fā)現(xiàn)我為了屏蔽
.so
文件將tinkerSupport{ overrideTinkerPatchConfiguration = false }
設置成false
西土,也就是使用tinkerPatch{}
中的配置,但是這個配置沒有設置加固屬性isProtectedApp = true
,抱著最后的希望在tinkerPath{ buildConfig{ isProtectedApp = true}}
增加了isProtectedApp = true
鞍盗,然后發(fā)布補丁包需了,居然好使了fuck,折騰到半夜加上一上午總結出兩個坑
- 手機安裝的apk和生成補丁的apk一定要是一個
- 加固之后發(fā)布的apk一定要設置
isProtectedApp = true