動態載入的一些坑

妖怪來了發表於2019-03-13

背景

前一段時間,做了一個需求,需要動態載入一個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中才能使用。

總結

動態載入過程中,資源問題是最令人頭痛的一個地方,好在也會有各種各樣的辦法去修復他。歡迎大家一起交流。

相關文章