Android熱修復Sophix
阿里熱修復最新版本
熱修復技術現在已經很成熟了,至今還沒有用過。雖然框架很多,但這裡只介紹Sophix,原因不言而喻,對於技術來說誰的好用用誰的。Sophix亮點有一下幾點
- 使用起來配置簡單,傻瓜式的接入
- 功能也比較強大
- 幾乎相容所有機型
- 支援方法,資原始檔,so等替換
- 阿里雲伺服器支援
一,熱修復框架對比
1,各大熱修復框架對比圖,詳細對比請看Android 熱修復調研報告—流行方案選擇
2,Sophix的演化,阿里雲官方文件
二,開始用起來
1,android studio整合方式
gradle遠端倉庫依賴, 開啟專案找到app的build.gradle檔案,新增如下配置:
新增maven倉庫地址:
repositories {
maven {
url "http://maven.aliyun.com/nexus/content/repositories/releases"
}
}
2,新增gradle版本依賴:
compile 'com.aliyun.ams:alicloud-android-hotfix:3.2.0'
3,新增許可權
<! -- 網路許可權 --> <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"/>
4,在AndroidManifest.xml中間的application節點下新增如下配置:
<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金鑰" />
5,SDK介面接入
// initialize必須放在attachBaseContext最前面,初始化程式碼直接寫在Application類裡面,切勿封裝到其他類。
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) {
// 表明新補丁生效需要重啟. 開發者可提示使用者或者強制重啟;
// 建議: 使用者可以監聽進入後臺事件, 然後呼叫killProcessSafely自殺,以此加快應用補丁,詳見1.3.2.3
} else {
// 其它錯誤資訊, 檢視PatchStatus類說明
}
}
}).initialize();
// queryAndLoadNewPatch不可放在attachBaseContext 中,否則無網路許可權,建議放在後面任意時刻,如onCreate中
SophixManager.getInstance().queryAndLoadNewPatch();
三,阿里雲建立應用
1,登入阿里雲官網
2,建立應用
①,登入後進入管理控制檯
②,新增產品,之後新增應用
③,應用新增完成
④,把appKey,AppSecret,RSA對應的值寫入到AndroidManifest.xml中間的application節點下meta-data中,這裡需要注意,如果你遇到這個問題 這就需要通過SDK介面接入的方式寫入appKey,AppSecret,RSA。
通過setSecretMetaData(“App ID”,“App Secret”,“RSA金鑰”)寫入對應的值即可
四,開始編碼
1,生成補丁
修改專案xml中TextView內容,修改前打個包old.apk,修改後打個包new.apk。測試包不用簽名,SDK初始化方法設定為setEnableDebug(true),然後下載補丁生成工具 補丁下載地址下載完成後執行SophixPatchTool.exe
2,本地測試方式
①,補丁生成後是一個jar,把這個jar拷貝到自己手機的skcard中,下載官方測試應用 測試程式
②,安卓6.0手機注意,需要動態新增許可權。
新增動態許可權
/**
* 如果本地補丁放在了外部儲存卡中, 6.0以上需要申請讀外部儲存卡許可權才能夠使用. 應用內部儲存則不受影響
*/
private void requestExternalStoragePermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_EXTERNAL_STORAGE_PERMISSION);
}
}
4,雲端測試方式
用測試應用掃描二維碼,掃描後會下載補丁,會有相應的日誌
注意:如果正式發版,需要SDK初始化方法設定為setEnableDebug(false),生成的補丁需要簽名,用自己應用的簽名
5,異常定位
code異常定位,常見的一些code值。
//相容老版本的code說明
int CODE_LOAD_SUCCESS = 1;//載入階段, 成功
int CODE_ERR_INBLACKLIST = 4;//載入階段, 失敗裝置不支援
int CODE_REQ_NOUPDATE = 6;//查詢階段, 沒有釋出新補丁
int CODE_REQ_NOTNEWEST = 7;//查詢階段, 補丁不是最新的
int CODE_DOWNLOAD_SUCCESS = 9;//查詢階段, 補丁下載成功
int CODE_DOWNLOAD_BROKEN = 10;//查詢階段, 補丁檔案損壞下載失敗
int CODE_UNZIP_FAIL = 11;//查詢階段, 補丁解密失敗
int CODE_LOAD_RELAUNCH = 12;//預載入階段, 需要重啟
int CODE_REQ_APPIDERR = 15;//查詢階段, appid異常
int CODE_REQ_SIGNERR = 16;//查詢階段, 簽名異常
int CODE_REQ_UNAVAIABLE = 17;//查詢階段, 系統無效
int CODE_REQ_SYSTEMERR = 22;//查詢階段, 系統異常
int CODE_REQ_CLEARPATCH = 18;//查詢階段, 一鍵清除補丁
int CODE_PATCH_INVAILD = 20;//載入階段, 補丁格式非法
//查詢階段的code說明
int CODE_QUERY_UNDEFINED = 31;//未定義異常
int CODE_QUERY_CONNECT = 32;//連線異常
int CODE_QUERY_STREAM = 33;//流異常
int CODE_QUERY_EMPTY = 34;//請求空異常
int CODE_QUERY_BROKEN = 35;//請求完整性校驗失敗異常
int CODE_QUERY_PARSE = 36;//請求解析異常
int CODE_QUERY_LACK = 37;//請求缺少必要引數異常
//預載入階段的code說明
int CODE_PRELOAD_SUCCESS = 100;//預載入成功
int CODE_PRELOAD_UNDEFINED = 101;//未定義異常
int CODE_PRELOAD_HANDLE_DEX = 102;//dex載入異常
int CODE_PRELOAD_NOT_ZIP_FORMAT = 103;//基線dex非zip格式異常
int CODE_PRELOAD_REMOVE_BASEDEX = 105;//基線dex處理異常
//載入階段的code說明 分三部分dex載入, resource載入, lib載入
//dex載入
int CODE_LOAD_UNDEFINED = 71;//未定義異常
int CODE_LOAD_AES_DECRYPT = 72;//aes對稱解密異常
int CODE_LOAD_MFITEM = 73;//補丁SOPHIX.MF檔案解析異常
int CODE_LOAD_COPY_FILE = 74;//補丁拷貝異常
int CODE_LOAD_SIGNATURE = 75;//補丁簽名校驗異常
int CODE_LOAD_SOPHIX_VERSION = 76;//補丁和補丁工具版本不一致異常
int CODE_LOAD_NOT_ZIP_FORMAT = 77;//補丁zip解析異常
int CODE_LOAD_DELETE_OPT = 80;//刪除無效odex檔案異常
int CODE_LOAD_HANDLE_DEX = 81;//載入dex異常
// 反射呼叫異常
int CODE_LOAD_FIND_CLASS = 82;
int CODE_LOAD_FIND_CONSTRUCTOR = 83;
int CODE_LOAD_FIND_METHOD = 84;
int CODE_LOAD_FIND_FIELD = 85;
int CODE_LOAD_ILLEGAL_ACCESS = 86;
//resource載入
public static final int CODE_LOAD_RES_ADDASSERTPATH = 123;//新增資源補丁包異常
//lib載入
int CODE_LOAD_LIB_UNDEFINED = 131;//未定義異常
int CODE_LOAD_LIB_CPUABIS = 132;//獲取primaryCpuAbis異常
int CODE_LOAD_LIB_JSON = 133;//json格式異常
int CODE_LOAD_LIB_LOST = 134;//lib庫不完整異常
int CODE_LOAD_LIB_UNZIP = 135;//解壓異常
int CODE_LOAD_LIB_INJECT = 136;//注入異常
6,SDK介面說明
①,initialize方法
initialize(): <必選>
該方法主要做些必要的初始化工作以及如果本地有補丁的話會載入補丁, 但不會自動請求補丁。因此需要自行呼叫queryAndLoadNewPatch方法拉取補丁。這個方法呼叫需要儘可能的早, 必須在Application的attachBaseContext方法的最前面呼叫(在super.attachBaseContext之後,如果有Multidex,也需要在Multidex.install之後), initialize()方法呼叫之前你需要先呼叫如下幾個方法進行一些必要的引數設定, 方法呼叫說明如下:
setContext(application): <必選> 傳入入口Application即可
setAppVersion(appVersion): <必選> 應用的版本號
setSecretMetaData(idSecret, appSecret, rsaSecret): <可選,推薦使用> 三個Secret分別對應AndroidManifest裡面的三個,可以不在AndroidManifest設定而是用此函式來設定Secret。放到程式碼裡面進行設定可以自定義混淆程式碼,更加安全,此函式的設定會覆蓋AndroidManifest裡面的設定,如果對應的值設為null,預設會在使用AndroidManifest裡面的。
setEnableDebug(isEnabled): <可選> isEnabled預設為false, 是否除錯模式, 除錯模式下會輸出日誌以及不進行補丁簽名校驗. 線下除錯此引數可以設定為true, 檢視日誌過濾TAG:Sophix, 同時強制不對補丁進行簽名校驗, 所有就算補丁未簽名或者簽名失敗也發現可以載入成功. 但是正式釋出該引數必須為false, false會對補丁做簽名校驗, 否則就可能存在安全漏洞風險
setAesKey(aesKey): <可選> 使用者自定義aes祕鑰, 會對補丁包採用對稱加密。這個引數值必須是16位數字或字母的組合,是和補丁工具設定裡面AES Key保持完全一致, 補丁才能正確被解密進而載入。此時平臺無感知這個祕鑰, 所以不用擔心阿里雲移動平臺會利用你們的補丁做一些非法的事情。
setPatchLoadStatusStub(new PatchLoadStatusListener()): <可選> 設定patch載入狀態監聽器, 該方法引數需要實現PatchLoadStatusListener介面, 介面說明見1.3.2.2說明
setUnsupportedModel(modelName, sdkVersionInt):<可選> 把不支援的裝置加入黑名單,加入後不會進行熱修復。modelName為該機型上Build.MODEL的值,這個值也可以通過adb shell getprop | grep ro.product.model取得。sdkVersionInt就是該機型的Android版本,也就是Build.VERSION.SDK_INT,若設為0,則對應該機型所有安卓版本。目前控制檯也可以直接設定機型黑名單,更加靈活。
②,queryAndLoadNewPatch方法
該方法主要用於查詢伺服器是否有新的可用補丁. SDK內部限制連續兩次queryAndLoadNewPatch()方法呼叫不能短於3s, 否則的話就會報code:19的錯誤碼. 如果查詢到可用的話, 首先下載補丁到本地, 然後
應用原本沒有補丁, 那麼如果當前應用的補丁是熱補丁, 那麼會立刻載入(不管是冷補丁還是熱補丁). 如果當前應用的補丁是冷補丁, 那麼需要重啟生效.
應用已經存在一個補丁, 請求發現有新補丁後,本次不受影響。並且在下次啟動時補丁檔案刪除, 下載並預載入新補丁。在下下次啟動時應用新補丁。
補丁在後臺釋出之後, 並不會主動下行推送到客戶端, 需要手動呼叫queryAndLoadNewPatch方法查詢後臺補丁是否可用.
只會下載補丁版本號比當前應用存在的補丁版本號高的補丁, 比如當前應用已經下載了補丁版本號為5的補丁, 那麼只有後臺釋出的補丁版本號>5才會重新下載.
同時1.4.0以上版本服務後臺上線了“一鍵清除”補丁的功能, 所以如果後臺點選了“一鍵清除”那麼這個方法將會返回code:18的狀態碼. 此時本地補丁將會被強制清除, 同時不清除本地補丁版本號
③,killProcessSafely方法
可以在PatchLoadStatusListener監聽到CODE_LOAD_RELAUNCH後在合適的時機,呼叫此方法殺死程序。注意,不可以直接Process.killProcess(Process.myPid())來殺程序,這樣會擾亂Sophix的內部狀態。因此如果需要殺死程序,建議使用這個方法,它在內部做一些適當處理後才殺死本程序。
④,cleanPatches()方法
清空本地補丁,並且不再拉取被清空的版本的補丁。正常情況下不需要開發者自己呼叫,因為Sophix內部會判斷對補丁引發崩潰的情況進行自動清空。
⑤, PatchLoadStatusListener介面,
mode: 無實際意義, 為了相容老版本, 預設始終為0
code: 補丁載入狀態碼, 詳情檢視PatchStatus類說明
info: 補丁載入詳細說明
handlePatchVersion: 當前處理的補丁版本號, 0:無 -1:本地補丁 其它:後臺補丁
五,熱修復原理
熱修復方案
市面上流行的熱修復框架主要有三個方案,類載入方案,底層替換方案和Instant Run方案
1,類載入方案
先了解一下Android的ClassLoader
- ClassLoader是一個抽象類,其中定義了ClassLoader的主要功能。BootClassLoader是它的內部類。
- BootClassLoader:啟動了載入器,和Java虛擬機器不同,BootClassLoader是由Java程式碼實現,而不是C++實現。
- BaseDexClassLoader:用於載入dex檔案,PathClassLoader和DexClassLoader是它的兩個實現類。
- DexClassLoader:支援載入APK、dex、jar,也可以從SD卡載入。
- PathClassLoader:該載入器將optomizedDirectory設定為null,預設路徑為/data/dalvik-cache目錄,即載入已經安裝的應用
Java Class的載入原始碼如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);//檢查是否已經載入
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);//委派給雙親
} else {
c = findBootstrapClassOrNull(name);//委派給啟動類
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看出使用雙親委派機制,即先到父載入器進行載入,載入不到才使用自己進行載入。
類載入方案也是基於dex分包方案,由於Android專案有 65535方法限制,從而產生了dex分包方案。Dex分包是在打包時將程式碼分成多個Dex,將應用啟動時必須用到的類和這些類的直接引用類放到主Dex中,其他程式碼放到次Dex中。當應用啟動時先載入主Dex,等到應用啟動後再動態的載入次Dex,從而緩解了主Dex的65536限制。
ClassLoader的載入過程中,會呼叫DexPathList中的findClass的方法程式碼如下:
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Element內部封裝了DexFile用於載入dex檔案,因此每個dex檔案對應一個Element。這就是關鍵地方,我們可以將有bug的類test.class進行修改,然後將test.class打包dex的補丁包test.jar,放在Element陣列dexElements的第一個元素,這樣首先找到test.dex中的test.class去替換之前存在bug的test.class,排在陣列後面的dex檔案中的存在bug的test.class根據ClassLoader的雙親委託模式就不會被載入。
類載入方案需要重啟App才能生效,不能即時生效,因為類無法解除安裝,需要重新載入新類。
2,底層替換方案
底層替換不同的地方是可以及時生效,可以直接在Native層直接修改原類,底層替換方案通過在執行時利用hook操作native指標實現“熱”的特性,底層替換所操作的指標,實際上是ArtMethod,在類被載入,類中的每個方法都會有對應的ArtMethod,它記錄了方法包括所屬類和記憶體地址資訊。
Sophix用的就是此方案,Sophix採用了對舊ArtMethod進行完整替換。底層替換方案雖然能及時生效,但是由於類載入後方法結構已固定,造成使用上的很多限制。因此Sophix採用類載入和底層替換相結合的方案。
3,Instant Run方案
Instant Run是基於多ClassLoader的,每一個patch都有一個ClassLoader,這就意味著如果你想更新patch,它都會建立一個ClassLoader,而在java中不同ClassLoader建立的類被認為是不同的,所以會重新載入新的patch中的補丁類。
附加:如果是系統內建應用,要想使用Sophix必須要把Sophix生成的so檔案拷貝到系統system/lib下。
so查詢方式,生成的apk修改後綴為zip,然後解壓會有對應的so檔案。有更多問題可以加入阿里釘釘群11711603