目錄
1、tinker的class文件修復
2、tinker的資源文件修復
3、幾種熱修復方案對比
1、tinker的class文件修復
1.1、先說dex文件的加載和類的查找過程
1.1.1、dex文件的加載過程
Java層通過我們會通過創建一個DexClassLoader來加載我們的dex,下面就以此爲切入點進行
dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());
//查看DexClassLoader的構造方法。
public class DexClassLoader extends BaseDexClassLoader {
// dexPath:是加載apk/dex/jar的路徑
// optimizedDirectory:是優化dex後得到的.odex文件的輸出路徑
// libraryPath:是加載的時候需要用到的so庫
// parent:給DexClassLoader指定父加載器
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
//可以看到它調用的是父類的構造函數,所以直接來看BaseDexClassLoader的構造函數。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
//創建了一個DexPathList實例,下面來看看DexPathList的構造函數。
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
}
//它調用的是makeDexElements方法來創建一個Element數組來存放Element對象,每個Element對象包含一個DexFile對象。
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
// 如果是一個dex文件
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
// 如果是一個apk或者jar或者zip文件
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
try {
// 1、調用loadDexFile加載dex文件,得到一個DexFile對象
loadDexFile通過c++層native方法去加載dex文件
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
// 2、把DexFile對象封裝到Element對象中,然後將Element對象加入Element數組
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
dex文件的加載流程:我們會使用DexClassLoader去加載dex文件,DexClassLoader會將這個任務委派給DexPathList中的makeDexElements方法,在makeDexElements中調用了native層的 c++方法去真正的加載dex文件,然後返回DexFile的對象,通過這個對象構建一個Element的對象,然後將這個Element添加到dexElements的數組中。
1.1.2、class文件的查找過程
//DexClassLoader間接調用父類findClass方法,findClass方法中調用DexPathList中的DexPathList方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
//看DexPathList中的findClass方法,可以看到它是遍歷dexElements數組,到每個dex文件去尋找當前需要的類,找到之後直接返回不往下找了
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
類的查找過程:DexClassLoader通過findClass去查找一個類,同樣它也是委派給DexPathList的findClass去查找,在DexPathList的findClass中會去遍歷我們上面創建的dexElements數組,然後在每個dex中去查找相應的類,找到之後就返回,不再向後查找。
1.2、Tinker中class修復過程
1.2.1、先看tinker-補丁包合成流程圖
補丁包的合成流程:當tinker收到補丁包bug path後,它會開啓一個service,和當前有問題的bug dex和成爲一個新的fixed dex文件,然後置於tinker dex文件加載路徑。
1.2.2、再看tinker-合成後的補丁包加載流程
補丁包的加載流程:獲取到fixed dex文件後,通過反射DexPathList中的dexElements數組,將fixed dex插入到dexElements數組的最前面。classloader在尋找bug class的時候,找到的就是最前面的dex文件中我們已修復的fixed class。
2、tinker的資源文件修復
2.1、Context.getResources()
我們訪問資源的時候通過Context.getResources(),獲取Resources對象,然後通過Resources對象就可以訪問各種資源了。
Context.getResources()流程
Context.getResources()獲取的ContextImpl中的mResources對象。
mResources對象的獲取過程
mResources是在activitythread初始化的時候獲取的,具體是通過
ResourcesManager.getInstance().getResources()去得到一個Resources。
這個方法的思想是這樣的:在ResourcesManager中,所有的資源對象都被存儲在ArrayMap中,首先根據當前的請求參數去查找資源,如果找到了就返回,否則就創建一個Resources對象返回,並且放到ArrayMap中。
爲什麼會有多個資源對象,因爲不同分辨率、不同系統版本所對應的資源文件可以不同,比如摺疊屏手機就有兩個不同分辨率的屏幕對應,drawable-hdpi和drawable-xhdpi。
Resources對象的創建過程
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
if (key.mResDir != null) {
if (assets.addAssetPath(key.mResDir) == 0) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
創建一個resource需要一個AssetManager, 然後通過AssetManager的addAssetPath去加載資源文件。
然後我們就可以通過resource訪問各種資源文件了。
2.2、tinker的資源文件修復
public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
final Field[] packagesFields;
if (Build.VERSION.SDK_INT < 27) {
packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
} else {
packagesFields = new Field[]{packagesFiled};
}
//首先將activityThread中所有LoadApk中的resDir的值替換成新合成的資源文件路徑
for (Field field : packagesFields) {
final Object value = field.get(currentActivityThread);
for (Map.Entry<String, WeakReference<?>> entry
: ((Map<String, WeakReference<?>>) value).entrySet()) {
final Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
final String resDirPath = (String) resDir.get(loadedApk);
if (appInfo.sourceDir.equals(resDirPath)) {
resDir.set(loadedApk, externalResourceFile);
}
}
}
// Create a new AssetManager instance
//創建一個新的AssetManager
newAssetManager = (AssetManager) findConstructor(assets).newInstance();
// 並把資源補丁apk加載進新的 AssetManager 中
if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
//循環替換ResourcesManager中所有Resources對象的AssetManager。
for (WeakReference<Resources> wr : references) {
final Resources resources = wr.get();
if (resources == null) {
continue;
}
try {
// 把原來 resources 的 mAssets 屬性替換成新的 AssetManager 對象
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
}
clearPreloadTypedArrayIssue(resources);
// 最後調用 updateConfiguration 方法來確保資源更新了
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
1、首先將activityThread中所有LoadApk中的resDir的值替換成新合成的資源文件路徑(獲取Resources時,會以LoadApk中的resDir作爲key去ResourcesManager中獲取)
2、創建一個新的AssetManager,並把資源補丁apk加載進新的 AssetManager 中
3、將ResourcesManager中所有Resources對象中AssetManager替換成我們新建的AssetManager,那麼所有的Resources對象獲取到的都是新合成的資源文件。
3、幾種熱修復方案對比
Andfix、QQ控件熱修復、Tinker。
3.1、Andfix
aandfix功能比較單一,只能修復方法,大致原理就是在運行時通過在native層去將bug方法替換成修復的方法。這樣導致了兩個問題:1、由於運行時,class已經加載,其field數值無法改變(要是能改變,通過這個class創建的對象就失效了),故其不能增加或者減少成員變量;2、因爲是動態的,跳過了類的初始化,所以對於靜態方法、靜態成員變量、構造方法處理可能會有問題,另外增加類也是不可能的;3、由於其採用在native層去進行方法替換,不同廠商手機可能對native層代碼做修改,故其兼容性可能較差。基於以上3個缺點,由於我們的app對兼容性要求較高,且可能對方法之外的地方做修改,所以Andfix是不滿足我們的需求的。
andfix修復方法的前提是對bug方法修復之後使用註解進行標記(bug類名和bug方法名),然後將這個補丁文件記錄在一個patch.mf的文件中。
3.2、QQ空間的熱修復方案
QQ空間的熱修復方案和Tinker方案較爲類似,都是通過操作dexElements數組替換有問題的class來實現的,不同的是Tinker時將差分包和有問題的dex文件合成一個新的 fix dex插入到dexElements數組前面的,而QQ空間的熱修復方案是直接將差分包,插入到dexElements的前面。會導致引用類和直接被引用類不在同一個dex的錯誤:
dex文件轉化成odex文件期間會經歷兩個階段:pre_verified和optimize,pre_verified會判斷一個類的直接引用類是否和它在同一個dex文件,如果是,這個類就會被打上CLASS_ISPREVERIFIED,在optimize會根據這個標記去對這個類做指令優化。但是在類加載的時候,也會去判斷這個類和其直接引用類是否在同一個dex,如果不在則報錯。
如果一個直接被引用類是我們修復的方法,按照qq空間的修復方案,它和其引用類就會處於不同的dex文件,那麼類加載的時候就會報錯。
dex文件在dalvik執行之前會轉化成odex文件,在這個過程中會對dex中每個類做一個檢驗:如果引用類(A)的直接引用的類(B)和這個類(A)在同一個dex,這個引用類(A)會被打上CLASS_ISPREVERIFIED標記,在類加載的時候,就會去判斷類(A)和被引用類是否在一個dex,不在,則報錯。
如果這個被引用類(B)是我們修復的方法,它就會和其引用類(A)處於不同的dex文件,那麼類加載的時候就會報錯。
QQ空間的熱修復方案的解決方法是,使用aop在每個類的構造函數,引用了一個特殊的類(C),在編譯的時候將這個類打進一個單獨dex文件,這樣所有的類在dex轉化成odex的過程中都不會被打上標記,這樣就避免了上述錯誤的發生。
但是這樣有一個問題,如果在dex轉化成odex期間,不做pre_verified和optimize兩步,那麼這這兩步將推遲到類加載的階段,會拖慢類加載的速度。
另外採用javassist進行字節碼操作速度慢,會拖慢編譯速度。
故淘汰了QQ空間的熱修復方案。
3.3、Tinker3.4、tinker的其他優點
1、Tinker的覆蓋比較全面,對class、資源文件、so文件都支持,
2、另外它比較容易擴展,在修復的各個階段都提供了listener,我們可以重寫這個listener,去監聽修復的各個階段。
3.4、Tinker的缺點
它的問題在於
1、由於Tinker採用的將fix dex 插入到dexElements最前面的方式去修復bug,所有它需要重啓,不能即時生效。
2、Tinker在下發path的時候需要啓動一個service去將path和有問題的dex文件合成一個新的dex文件,這個過程是對性能影響較大的,但是合成只需要一次,後續就不需要了,這樣的性能損耗還是可以接受的;