效能優化系列
程式碼傳送陣
熱修復的背景
- 剛釋出的版本出現了嚴重的 bug ,需要開發者去解決 bug,然後在測試打包重新發布,這會耗費大量的人力,物力,代價比較大。
- 如果當前的 bug 不影響使用者使用也不會崩潰,但是了下個版本是大版本,那麼兩個版本之間間隔時間會很長,這樣要等到下個大版本釋出在修復 bug , 而之前版本的 bug 還存在,雖說不影響使用,但是是一個潛在的 bug。
- 版本升級率不高,並且需要長時間來完成版本迭代,前版本的 bug 就會一直影響不升級的使用者。
- 有一些小但是很重要的功能需要在短時間內完成版本迭代,比如假日活動。
..等等, 這裡只是拿幾個常見的舉例說明。
熱修復的效率
熱修復框架對比
框架名稱 | 所屬公司 | 是否開源 | 修復方式 |
---|---|---|---|
Dexposed | alibaba | 開源 | 實時修復 |
Andfix | alibaba | 開源 | 實時修復 |
Hotfix | alibaba | 暫未開源 | 實時修復 |
Qzone 超級補丁 | QQ 空間 | 暫未開源 | 冷啟動修復 |
QFix | 手 Q 團隊 | 開源 | 冷啟動修復 |
Robust | 美團 | 開源 | 實時修復 |
Nuwa | 大眾點評 | 開源 | 冷啟動修復 |
RocooFix | 百度金融 | 開源 | 冷啟動修復 |
Aceso | 美麗說蘑菇街 | 開源 | 實時修復 |
Amigo | 餓了麼 | 開源 | 冷啟動修復 |
Tinker | 微信 | 開源 | 冷啟動修復 |
Sophix | alibaba | 未開源 | 實時修復 + 冷啟動修復 |
程式碼修復(今日主題 - 類載入方式)
底層替換方式
- 在已載入的類中直接替換原有方法,是在原有類的基礎上進行修改,無法實現對原有類進行方法和欄位的增減,這樣會破壞原有類的結構。
- 不穩定。直接修改 JVM 方法實體的具體欄位來實現的。Android 是開源的,不同的手機廠商開源對程式碼進行修改,所以像 Andfix 就會出現在部分機型上的修復失敗的現象。
ClassLoader 類載入方式
-
APP 重新啟動後,讓 ClassLoader 去載入新的類。
-
class 暫未被載入到系統中,收到推送利用插樁原理讓 ClassLoader 優先載入修復好的 dex 。
實現自己的熱修復框架
Dex 分包
65536 限制
com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
複製程式碼
當應用程式報 65536 錯誤的根本原因是,應用的方法數量超過了最大數 65536 個,因為 DVM Bytecode 的限制, DVM 指令集的方法呼叫指令 invoke-kind 索引為 16 bits, 最多能引用 65535 個方法
LinearAlloc 限制
INSTALL_FAILED_DEXOPT
複製程式碼
在安裝應用時可能會提示 上面的錯誤,產生的原因是 LinearAlloc 限制。 DVM 中的 LinearAlloc 是一個固定的快取區,當方法數超出快取區的大小時會報錯。
解決
為了解決 65536 限制和 LinearAlloc 限制,從而產生了 Dex 分包機制。 Dex 分包方案主要做的時在打包時將應用程式碼分成多個 Dex,將應用啟動時必須用到的類和這些類的直接引用類放到主 Dex 中,其它程式碼放到次 Dex 中。當應用啟動時先載入主 Dex,等到應用啟動後再動態地載入次Dex,從而緩解了主 Dex 的 65536 限制和 LinearAlloc 限制
-
gradle 配置
android { compileSdkVersion 26 defaultConfig { applicationId "com.ykun.hotfix" minSdkVersion 15 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" // 開啟分包 multiDexEnabled true // 設定分包配置檔案 multiDexKeepFile file('multidex.keep') } dexOptions { javaMaxHeapSize "4g" preDexLibraries = false additionalParameters = [ // 配置multidex引數 '--multi-dex', // 多dex分包 '--set-max-idx-number=50000', // 每個包內方法數上限 '--main-dex-list=' + '/multidex.keep', // 打包到主classes.dex的檔案列表 '--minimal-main-dex' ] } } 複製程式碼
-
配置 multidex.keep 將指定的 class 放入 class.dex 中
格式:
//參考 com/ykun/hotfix/BaseActivity.class com/ykun/hotfix/BaseApplication.class com/ykun/hotfix/MainActivity.class 複製程式碼
-
效果
什麼是插樁?
原始碼:
/**遍歷需要找到需要載入的 class */
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
複製程式碼
插樁原理:
通過原始碼得知 findClass 是通過遍歷 dexElements 來找到 class, 如果我們反射得到 DexPathList 的私有陣列 dexElements,我們外部改變這個陣列內部順序索引,將修復好的 dex 放入 [0] 的位置,那麼是不是能夠優先使用修復好的 dex 勒? 很明顯,是成立的。下面開始擼程式碼吧。
程式碼實現
-
接收來至伺服器發來的補丁包,如果修復包已經存在則刪除,copy 到私有目錄防止使用者不小心刪除。
/**這裡模擬已經下載好的 dex 補丁包*/ private void downloadPatch() { //1 從伺服器下載dex檔案 比如v1.1修復包檔案(classes2.dex) File sourceFile = new File(Environment.getExternalStorageDirectory(), "classes2.dex"); // 目標路徑:私有目錄 //getDir("odex", Context.MODE_PRIVATE) data/user/0/包名/app_odex File targetFile = new File(getDir("hotfix", Context.MODE_PRIVATE).getAbsolutePath() + File.separator + "classes2.dex"); if (targetFile.exists()) { targetFile.delete(); } try { // 複製dex到私有目錄 FileUtils.copyFile(sourceFile, targetFile); Toast.makeText(this, "Bug 修復成功!", Toast.LENGTH_SHORT).show(); FixDexUtils.loadFixedDex(this); } catch (IOException e) { e.printStackTrace(); } } 複製程式碼
-
建立修復包的類載入器 DexClassLoader (通過原始碼得知是繼承的 BaseDexClassLoader)
/** * 建立類載入器 * * @param context * @param fileDir */ private static void createDexClassLoader(Context context, File fileDir) { String optimizedDirectory = fileDir.getAbsolutePath() + File.separator + "opt_dex"; File fOpt = new File(optimizedDirectory); if (!fOpt.exists()) { fOpt.mkdirs(); } DexClassLoader classLoader; for (File dex : loadedDex) { //初始化類載入器 classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDirectory, null, context.getClassLoader()); //熱修復 hotFix(classLoader, context); } } 複製程式碼
-
獲取系統的 PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); 複製程式碼
-
獲取修復包的 dexElements
Object pathList = ReflectUtils.reflect(myClassLoader).field("pathList").get(); Object myDexElements = ReflectUtils.reflect(pathList).field("dexElements").get(); 複製程式碼
-
獲取系統的 dexElements
Object sysPathList = ReflectUtils.reflect(pathClassLoader).field("pathList").get(); Object sysDexElements = ReflectUtils.reflect(sysPathList).field("dexElements").get(); 複製程式碼
-
將系統的 dexElements 和 修復包的 dexElements merge 成新的 dexElements
// 合併,這裡利用插樁原理進行合併陣列,將修復好的 class2.dex 放入第一位,優先加入就行了 Object dexElements = ArrayUtils.combineArray(myDexElements, sysDexElements); 複製程式碼
-
重新賦值給 DexPathList 的 dexElements 屬性
//重新賦值 ReflectUtils.reflect(sysPathList).field("dexElements", dexElements); 複製程式碼
熱修復未來發展
-
熱修復 = “黑科技”?
-
熱修復不同於國內 APP 程式保活這種 “黑科技”,讓 app 常駐後臺,既耗電又佔用記憶體,浪費很多手機資源。還有 APP 的推送服務,無節操地對使用者進行資訊轟炸。還有更無節操的全家桶 app。導致 Android手機卡頓不堪,這些所謂的 “黑科技” 都是為了手機廠商的利益而損害使用者的體驗。
-
而熱修復是能夠讓開發者和使用者雙贏的。不僅廠商能快速迭代更新 app,使功能儘快上線,而且熱更新過程使用者無感知,節省大量更新時間,提高使用者體驗。更重要的能保證 app 的功能穩定,bug 能及時修復。
-
-
IOS 封殺了熱修復功能,Android 的熱修復也會被 pass 掉嗎?
-
google 和 apple 公司在中國的 diwei 不一樣
-
Android 和 IOS 的開放性不同
-
-
熱修復未來發展前景是很樂觀的。