背景
前一段時間,做了一個需求,需要動態載入一個so,還有一個classes.dex,還有一些資源。看上去是一個還行的需求,原理就是通過 classloader 進行動態載入,知易行難,真正做起來,還是遇到了下面的這些坑。
問題
0x01類衝突
什麼是類衝突呢?就是說我們的程式碼中可能有兩個一模一樣的類,包名,類名都一模一樣。有人可能會問,怎麼會有這種情況呢?因為模組走的動態載入,沒有走統一編譯,這種問題就會變得無法避免。難免有人腦子想到一起,就產生了重複的類了。
眾所周知,java是通過classloader進行類載入的,類載入機制就是著名的雙親委派,不太瞭解的同學,我簡單描述一下就是:如果有一家三代,就先去爺爺那裡找有沒有這個類,如果沒有就去爸爸那裡找,爸爸找不到就從兒子這裡找,兒子找不到就 ClassNotFoundException 了。 所以,當我們進行動態載入的時候,一般都是使用 DexClassLoader (關於如何動態載入,這裡不多說,網上文章很多),這個DexClassLoader會把引數裡面的路徑下的dex檔案載入起來,那麼你的類就可以通過這個 classloader 進行載入了。
這個時候,問題就來了,如果有重名的類,已經載入過了,那麼,你肯定就載入不到你自己的類了,這樣載入到的類就不是你想要的那個類,錯誤就產生了。如何避免呢?先看如下程式碼:
public class CustomClassLoader extends DexClassLoader {
private ClassLoader mParentClassLoader;
public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return null;
}
});
mParentClassLoader = parent;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
try {
clazz = super.loadClass(name);
} catch (Exception ex) {
// ignore
}
if (clazz != null) {
return clazz;
}
if (mParentClassLoader == null) {
return null;
}
return mParentClassLoader.loadClass(name);
}
//....
}
複製程式碼
可以看到,我們自定義了一個CustomClassLoader繼承自DexClassLoader,有兩個重點:
- super() 呼叫的第三個引數傳了一個重寫了 loadClass 的方法,裡面直接返回null,這個引數是父classloader的一起,這裡就是把爸爸設定成一個什麼都沒有的 classloader。如果不設定,在安卓6.0以下都會報一個錯誤,父classloader不能為null的錯誤。
- loadClass() 方法,先呼叫super.loadClass() 方法,出異常再呼叫傳遞進來的真正的爸爸classloader載入。
通過這樣一個邏輯,就能保證先載入自己的類,再去載入爺爺和爸爸那裡的類了。這樣即使記憶體裡面已經有了這個類,通過這個載入邏輯也能載入成功自己的類了。不過這樣就違背了java的雙親委派機制,不過這也是沒有辦法的事情,java自己也違背過,哈哈哈。
0x02 資源載入不起來
我們的classes.dex 和資原始檔不是同一個apk,也就是說他們不是一起進行打包的,這就帶來了另外一個問題,兩邊分開進行打包,資源id對不上。要解決這個問題,就要把我們的資源apk路徑載入到系統尋找資源的路徑上面來,關鍵方法如下:
public static boolean addResource(Context context, String apkDir) {
if (TextUtils.isEmpty(apkDir)) {
return false;
}
try {
Method m = getAddAssetPathMethod();
Log.e("getAddAssetPathMethod m = " + m);
if (m != null) {
int ret = (int) m.invoke(context.getAssets(), apkDir);
Log.e("invoke ret = " + ret);
return ret > 0;
}
} catch (Exception e) {
Log.d("invoke method error ! ", e.toString());
}
return false;
}
private static Method getAddAssetPathMethod() {
Method m = null;
Class c = AssetManager.class;
if (Build.VERSION.SDK_INT >= 24) {
try {
m = c.getDeclaredMethod("addAssetPathAsSharedLibrary", String.class);
m.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return m;
}
try {
m = c.getDeclaredMethod("addAssetPath", String.class);
m.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return m;
}
複製程式碼
然後自己構造一個 ContextThemeWrapper類,進行資源的查詢。大致實現如下:
public class ResourcesContext extends ContextThemeWrapper {
private final ClassLoader mNewClassLoader;
Resources mNewResources;
public ResourcesContext(Context base, int themeres, ClassLoader cl, Resources r) {
super(base, themeres);
mNewResources = r;
mNewClassLoader = cl;
}
@Override
public Resources getResources() {
if (mNewResources != null) {
return mNewResources;
}
return super.getResources();
}
}
複製程式碼
通過傳遞進來的 mNewResources 進行資源的查詢。最終使用這個類進行資源的查詢,通過context去查詢資源的方法如下:
resourceContext.getString(R.xxx);
複製程式碼
必須通過這個resourceContext進行資源的查詢。
這樣我們就解決了資源查詢的問題,還有一個問題,就是資源id錯亂對不上的問題。這個解決比較簡單,就是把所有的id在初始化的時候統一進行一次重新賦值,讓dex中的id都被賦值為資源apk中的id值。
0x03 資源錯亂
在demo中執行良好,興高采烈去客戶端進行整合。一整合完畢,就發現app莫名奇妙的崩潰,很多資源找不到, 而且基本是什麼資源都會崩潰。找了很久問題的根源,發現是資源id衝突。看來只能在我們自己編譯資源apk的時候,進行資源id的修改了。那麼aapt這個工具就閃亮登場了。在build.gradle中的android節點加入:
aaptOptions {
additionalParameters "--package-id", "0x66","--allow-reserved-package-id"
}
buildToolsVersion '28.0.3'
複製程式碼
0x66 是自己定義的id,這樣我們生成的資源就都是0x66開頭的了,而系統預設都是 0x7f開頭。注意此工具必須在高版本的gradle中才能使用。
總結
動態載入過程中,資源問題是最令人頭痛的一個地方,好在也會有各種各樣的辦法去修復他。歡迎大家一起交流。