Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android插件化探索(二)資源加載

Android插件化探索(二)資源加載

編輯:關於Android編程

前情提要

在探索資源加載方式之前,我們先來看看上一篇中沒細講的東西。

PathClassLoader和DexClassLoader的區別

DexClassLoader的源碼如下:

public class DexClassLoader extends BaseDexClassLoader {
    //支持從任何地方的apk/jar/dex中讀取
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

PathClassLoader的源碼如下,沒有指定optimizedDirectory所以只能加載已安裝的APK,因為已安裝的APK會將dex解壓到了/data/dalvik-cache/目錄下,PathClassLoader會到這裡去找。

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

但是本人本著不作不死的性格,修改Plugin類如下:

    private void useDexClassLoader(String path){

        //創建類加載器,把dex加載到虛擬機中
        PathClassLoader calssLoader = new PathClassLoader(path, null,
                this.getClass().getClassLoader());

        //利用反射調用插件包內的類的方法
        try {
            Class clazz = calssLoader.loadClass("com.maplejaw.hotplugin.PluginClass");
            Comm obj = (Comm)clazz.newInstance();
            Integer ret = obj.function( 12,21);
            Log.d("JG", "返回的調用結果: " + ret);

        } catch (Exception e) {
            e.printStackTrace();
        } 
    }

在4.4和5.0上分別做了試驗從SD卡上加載dex,發現提示略有差別。
Android 4.4:直接提示找不到該類

java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginClass" on path: DexPathList[[zip file "/storage/sdcard/2.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]

Android 5.0:可以發現在加載該類之前,系統嘗試將dex寫到/data/dalvik-cache/下,由於權限問題而失敗。

05-25 04:07:02.291 31699-31699/com.maplejaw.hotfix E/dalvikvm: Dex cache directory isn't writable: /data/dalvik-cache
05-25 04:07:02.291 31699-31699/com.maplejaw.hotfix I/dalvikvm: Unable to open or create cache for /storage/sdcard/2.apk (/data/dalvik-cache/storage@[email protected]@classes.dex)
05-25 04:07:02.291 31699-31699/com.maplejaw.hotfix W/System.err: java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginClass" on path: DexPathList[[zip file "/storage/sdcard/2.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]

但是!!!筆者此時默默的掏出了大紅米(Android 5.0)測試了一番。居然發現可以調用正常,也就是說,MIUI成功將dex寫到了/data/dalvik-cache/下,所以如果你手持MIUI發現PathClassLoader可以加載外部dex時,務必冷靜,用模擬器試試。

雙親委托

什麼叫雙親委托?
為了更好的保證 JAVA 平台的安全。在此模型下,當一個裝載器被請求加載某個類時,先委托自己的 parent 去裝載,如果 parent 能裝載,則返回這個類對應的 Class 對象,否則,遞歸委托給父類的父類裝載。當所有父類裝載器都裝載失敗時,才由當前裝載器裝載。在此模型下,用戶自定義的類裝載器,不可能裝載應該由父親裝載的可靠類,從而防止不可靠甚至惡意的代碼代替本應該由父親裝載器裝載的可靠代碼。

在JVM中預定義了的三種類型類加載器:

啟動(Bootstrap)類加載器:是用本地代碼實現的類裝入器,它負責將 /lib下面的類庫加載到內存中(比如rt.jar)。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。 標准擴展(Extension)類加載器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將< Java_Runtime_Home >/lib/ext或者由系統變量 java.ext.dir指定位置中的類庫加載到內存中。開發者可以直接使用標准擴展類加載器。 系統(System)類加載器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫加載到內存中。開發者可以直接使用系統類加載器。

委派機制:自定義的ClassLoader->AppClassLoader->ExtClassLoader->BootstrapClassLoader

那麼Android中的委派機制是怎麼樣的呢?

BootClassLoader: 加載系統類庫 PathClassLoader: 加載已安裝apk中的dex中的類 DexClassLoader: 加載外部和內部apk中的類

委派機制:DexClassLoader->PathClassLoader->BootClassLoader。

我們可以打印出其委派機制:

 ClassLoader classLoader=new DexClassLoader(apkPath,getApplicationInfo().dataDir,libPath,getClassLoader());
  try {
            Class clazz = calssLoader.loadClass("com.maplejaw.hotplugin.PluginClass");
            Comm obj = (Comm)clazz.newInstance();
            Integer ret = obj.function( 12,21);

            while(classLoader != null){
                Log.d("JG", "類加載器:"+classLoader);
                classLoader = classLoader.getParent();
            }
              Log.d("JG", "返回的調用結果: " + ret);

結果如下:

05-25 09:09:11.330 9100-9100/com.maplejaw.hotfix D/JG: 初始化PluginClass

05-25 09:09:11.330 9100-9100/com.maplejaw.hotfix D/JG: 類加載器:dalvik.system.DexClassLoader[DexPathList[[zip file "/data/app/com.maplejaw.hotplugin-2/base.apk"],nativeLibraryDirectories=[/data/app/com.maplejaw.hotplugin-2/lib/x86_64, /vendor/lib64, /system/lib64]]]

05-25 09:09:11.330 9100-9100/com.maplejaw.hotfix D/JG: 類加載器:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.maplejaw.hotfix-2/base.apk"],nativeLibraryDirectories=[/data/app/com.maplejaw.hotfix-2/lib/x86_64, /vendor/lib64, /system/lib64]]]

05-25 09:09:11.330 9100-9100/com.maplejaw.hotfix D/JG: 類加載器:java.lang.BootClassLoader@7cd98df

05-25 09:09:11.330 9100-9100/com.maplejaw.hotfix D/JG: 返回的調用結果: 33

看到這裡,應該可以明白上一篇中提到的java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation中因為同一加載器加載不同dex中相同的類引發的錯誤了吧。

資源加載

在上一篇中,提到了通過資源名字,然後獲取id,最後獲取資源的思路。先來回顧一下。

getResourcesForApplication

//首先,通過包名獲取該包名的Resources對象
Resources res= pm.getResourcesForApplication(packageName);
//根據約定好的名字,去取資源id;
int id=res.getIdentifier("a","drawable",packageName);//根據名字取id
//根據資源id,取出資源
Drawable drawable=res.getDrawable(id)

這種方式有個特點,就是得清楚每一個資源的名字。但也從側面提現了,這種方式不夠靈活。那麼,有沒有一種方法,可以簡化這一過程呢?

根據這種方式的特點,嘗試著寫了幾個測試例子。

不就是要資源嗎,直接在插件中把資源提供給宿主不就行了。修改PluginClass如下:

 public Drawable getImage(Context context){
       return context.getResources().getDrawable(R.drawable.a);
    }

宿主核心代碼修改如下。

 Class clazz = classLoader.loadClass(packageName+".PluginClass");
 Comm obj = (Comm) clazz.newInstance();
 Drawable drawable=obj.getImage(this)
 mImageView.setImageDrawable(drawable);

運行測試,沒有報錯,反而讀到了宿主APK下的一個圖片,應該是資源id和宿主中的id重復了。於是多拷了幾個圖片到插件drawable目錄,仍然什麼都讀不到。於是仔細回頭看了看代碼。發現了這一句context.getResources(),可以看出拿到的是宿主APK的Resources對象,懷著好奇心,順籐摸瓜找到了源碼。在ContextImpl的源碼中,發現如下:
可以從字面意思看出一個包一個Resources。


    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, ...) {
            //...
            //省略了部分源碼
        Resources resources = packageInfo.getResources(mainThread);
        mResources = resources;
          //...
            //省略了部分源碼
        }


    public Resources getResources() {
        return mResources;
    }

既然一個包一個Resources,那我們換種思路,就用插件包的Resources去加載資源。修改PluginClass如下:

     public Drawable getImage(Resources res){
       return res.getDrawable(R.drawable.a);
    }

然後修改宿主的核心代碼,測試通過。

  Class clazz = classLoader.loadClass(packageName+".PluginClass");
  Comm obj = (Comm) clazz.newInstance();
  Resources res=pm.getResourcesForApplication(packageName);
  Drawable drawable=obj.getDrawable(res)

可以看出,這種方法比第一種方法要靈活不少,不需要知道每張圖片的名字,只需插件實現相關接口即可。

AssetManager

但是,有沒有別的方式了?答案當然是肯定的,Android裡有一個類叫AssetManager!在介紹AssetManager之前我們來看看getResource的源碼。我們知道,調用getResource,會調用LoadedApk的getResources。

    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, ...) {
            //...
            //省略了部分源碼
        Resources resources = packageInfo.getResources(mainThread);
        mResources = resources;
          //...
         //省略了部分源碼
        }

那麼就來看看LoadedApk的getResources源碼。

    public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
        }
        return mResources;
    }

可以看出,LoadedApk會調用ActivityThread去加載。最終在ResourcesManager中找到了真身。

 Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo) {

        Resources r;

           //...
            //省略了部分源碼
        AssetManager assets = new AssetManager();

        if (resDir != null) {
            if (assets.addAssetPath(resDir) == 0) {
                return null;
            }
        }

        if (splitResDirs != null) {
            for (String splitResDir : splitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    return null;
                }
            }
        }

        if (overlayDirs != null) {
            for (String idmapPath : overlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }

        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (libDir.endsWith(".apk")) {

                    if (assets.addAssetPath(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }

           //...
            //省略了部分源碼
          r = new Resources(assets, dm, config, compatInfo);

            return r;
        }
    }

代碼有點長,但是很顯然,最終的資源加載交給了AssetManager,assets.addAssetPath(libDir)添加資源目錄,然後new了一個Resources對象返回。

那我們現在通過反射來模仿系統的寫法。

    protected void loadResources(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexPath);
            Resources superRes = super.getResources();
            mResources = new Resources(assetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

然後重寫getResources方法:


    @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }

核心代碼修改如下,

 //先加載插件資源
   loadResources(apkPath);

 //核心代碼調用
   Class clazz = classLoader.loadClass(packageName+".PluginClass");
   Comm obj = (Comm) clazz.newInstance();
   mImageView.setImageDrawable(obj.getDrawable(getgetResources));

成功將資源加載到宿主APK,測試通過。
當然,為了使通用性更強,不因主題的差異而導致效果不一樣,上面的代碼一般會這麼寫

    //反射加載資源
    private AssetManager mAssetManager;
    private Resources mResources;
    private Resources.Theme mTheme;
    protected void loadResources(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }
        Resources superRes = super.getResources();
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }


  //重寫方法
    @Override
    public AssetManager getAssets() {
        return mAssetManager == null ? super.getAssets() : mAssetManager;
    }
    @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }
  @Override
    public Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

換皮膚原理

換皮膚一般有兩種方式。

約定好資源名字

這種方式非常簡單,基本不需要修改什麼代碼。只需兩部就來完成。
假如我們有一個圖片菜單,在宿主和插件中都叫a.png。

設置菜單圖片的代碼如下。

mImageMenu.setImageDrawable(getResources().getDrawable(R.drawable.a));
重寫getResources
   @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }
獲取Resources對象
loadResources或getResourcesForApplication

這樣在需要加載皮膚的地方loadResources,然後重寫加載就能實現換膚功能。

不約定資源名字

這種方式主要通過接口的方式進行調用。讓不同的皮膚插件進行調用。

實現插件接口
public Drawable getImageMenu(Resources res){
       return res.getDrawable(R.drawable.a);
    }
重寫getResources
   @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }
獲取Resources對象
loadResources或getResourcesForApplication
設置菜單圖片
  Class clazz = classLoader.loadClass(packageName+".PluginClass");
  Comm obj = (Comm) clazz.newInstance();

  mImageMenu.setImageDrawable(obj.getImageMenu(getResources()));

從上面可以看出,約定資源名字這種方法,可以少寫好多接口。

最後

由於本人水平有限,如有錯誤敬請指出,萬分感謝。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved