轉載自: https://blog.csdn.net/wang_yong_hui_1234/article/details/79657246
阿裡熱修複最新版本
熱修複技術現在已經很成熟了,至今還沒有用過。雖然架構很多,但這裡隻介紹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