安卓之插件化開發使用 DexClassLoader&AssetManager 來更換皮膚

dangdang 7年前發布 | 5K 次閱讀 安卓開發 Android開發 移動開發

這篇文章主要使用DexClassLoader來實現插件化更換皮膚,即將皮膚獨立出來做成一個皮膚插件apk,當用戶想使用該皮膚時需下載(不需要安裝)對應的皮膚插件apk

效果圖

【為方便測試,主要通過改變背景圖來簡單地展示皮膚更換】

一、DexClassLoader

如果使用DexClassLoader來實現插件化皮膚更換,我們需要去下載(不需安裝)我們的皮膚插件apk:

  1. DexClassLoader 可以加載外部的 apk、jar 或 dex文件,并且會在指定的 outpath 路徑存放其 dex 文件。

  2. 構造函數:

    DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

    dexPath:需被解壓的apk路徑,不能為空。

    optimizedDirectory:解壓后的.dex文件的存儲路徑,不能為空。這個路徑強烈建議使用應用程序的私有路徑,不要放到sdcard上,否則代碼容易被注入攻擊。

    libraryPath:c/c++庫的路徑,可以為null,若有相關庫,須填寫。

    parent:父親加載器,一般為context.getClassLoader(),使用當前上下文的類加載器。

  3. 下面為什么要使用到擴展DexClassLoader?:

    這里使用DexClassLoader是為了加載 插件apk 中的dex文件,加載dex文件后系統就可以在dex中找到我們要使用的class類R.java,在R.java中包含著資源等的id,通過id我們可以獲取到資源。

二、主應用apk的邏輯

  1. 為了方便測試,我們將插件apk存放在SD卡中,主應用apk再去獲取。所以在主應用中需要讀寫SD卡內容的權限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  2. 使用SharedPreferences來記錄皮膚的改變:

    SharedPreferences skinType; 
    
    skinType = getPreferences(Context.MODE_PRIVATE);
    String skin = skinType.getString("skin",null);
    
    if(skin!=null) installSkin(skin);
  3. 按鈕點擊事件的響應:

    public void changeSkin1(View view) {
        installSkin("dog");
    }
    
    public void changeSkin2(View view) {
        installSkin("girl");
    }
  4. 所以我們更好皮膚的重點在 installSkin 函數中:

    public void installSkin(String skinName) {
    
        // 通過皮膚名字查找皮膚apk是否存在,這里需要注意皮膚apk的命名
        // 存在則返回路徑,否則返回null
        String apkPath = findPlugins(skinName);
    
        if (apkPath == null) {
            // 皮膚不存在時(可以靜默下載皮膚)
            Toast.makeText(this, "請先安裝皮膚", Toast.LENGTH_SHORT).show();
    
            // 皮膚插件被刪除的情況,清空存儲
            if (skinType.getString("skin", skinName).equals(skinName))
                skinType.edit().clear().commit();
    
        } else {
             // 皮膚apk存在,獲取其包名。注意包名與皮膚名的關系
             String apkPackageName = "com.cxmscb.cxm."+skinName;
    
            /**
            *通常我們獲取Resoueces對象使用的是context.getResources
            *但我們無法獲取皮膚apk的context(因為皮膚apk沒有安裝)
            *在這里通過獲取加載插件apk的AssetManager來獲取插件apk的Reources對象
            */
            Resources resources = getSkinApkResource(this,apkPath);
    
            // 獲取背景圖片的id
            int bgId = getSkinBackgroundId(apkPath,skinName,apkPackageName);
    
            //通過插件的Resources對象和id獲取背景圖片
            rl.setBackgroundDrawable(resources.getDrawable(bgId));
    
            //保存記錄
            skinType.edit().putString("skin",skinName).commit();
    
        }
    }
  5. 上面我們是通過findPlugins(String plugName)來查找皮膚插件apk是否存在:

    private String findPlugins(String plugName) {
    
        String apkPath = null;
    
        // 獲取apk的路徑 (為方便測試:將apk存放在SD卡的根目錄下)
        apkPath = Environment.getExternalStorageDirectory()+"/"+ plugName+".apk";
    
        //皮膚apk存在時,才返回路徑
        File file = new File(apkPath);
    
        if (file.exists()) {
    
            return apkPath;
        }
    
    
        return null;
    }
  6. 上面我們是通過getSkinApkResource(this,apkPath)來獲取插件apk的Resources對象的。接下來是對獲取Resources對象的源碼追蹤:

    a. 通常獲取資源時使用getResource獲得Resource對象,通過這個對象我們可以訪問相關資源。通過跟蹤源碼發現,其實 getResource 方法是Context的一個抽象方法。

    /** Return a Resources instance for your application's package. */
    public abstract Resources getResources();

    b. 而getResource的具體實現是在ContextImpl類(Context的實現類)中實現的,獲取的Resource對象是應用的全局變量mResource。

    public Resources getResources(){
        return mResources;
    }

    c. 然后繼續跟蹤ContextImpl類中的全局變量mResource如何實現,發現 mResources 由一個LoadApk對象packageInfo來創建。

    Resources resources = packageInfo.getResources(mainThread);

    接著繼續跟蹤LoadApk這個類中的getResources方法:

    public Resources getResources(ActivityThread mainThread){
    
        if(mResources==null){
                mResources = mainThread.getTopLevelResources(mResDir,mSplitResDirs....)
        }
    
        return mResources;
    }

    d. 接著繼續跟蹤 ActivityThread 這個類中的 getTopLevelResources 方法發現調用的是 ResourcesManager 類的 getTopLevelResources 方法。于是繼續追蹤該方法:在這個方法中,有一個Resources對象的弱引用,當弱引用對象被釋放掉時會重新調用 r = new Resources(assets, dm, config); 來創建Resources對象再放入虛引用中。

    其中 AssetManager對象 assets參數 加載了應用的apk路徑:assets.addAssetPath(resDir) ,其中resDir為apk的路徑。dm, config參數可以分別為手機的屏幕信息和手機的配置信息。

    為此我們可以通過 new Resources(assets, dm, config) 來創建加載皮膚插件apk資源的Resources

    //ResourcesManager
    
    public Resources getTopLevelResources(String resDir, int displayId,  
            Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {  
        final float scale = compatInfo.applicationScale;  
        ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale,  
                token);  
        Resources r;  
        synchronized (this) {  
            // Resources is app scale dependent.  
            if (false) {  
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);  
            }  
            WeakReference<Resources> wr = mActiveResources.get(key);  
            r = wr != null ? wr.get() : null;  
            //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());  
            if (r != null && r.getAssets().isUpToDate()) {  
                if (false) {  
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir  
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);  
                }  
                return r;  
            }  
        }  
    
        //if (r != null) {  
        //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "  
        //            + r + " " + resDir);  
        //}  
    
        AssetManager assets = new AssetManager();  
        if (assets.addAssetPath(resDir) == 0) {  
            return null;  
        }  
    
        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);  
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);  
        Configuration config;  
        boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);  
        final boolean hasOverrideConfig = key.hasOverrideConfiguration();  
        if (!isDefaultDisplay || hasOverrideConfig) {  
            config = new Configuration(getConfiguration());  
            if (!isDefaultDisplay) {  
                applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);  
            }  
            if (hasOverrideConfig) {  
                config.updateFrom(key.mOverrideConfiguration);  
            }  
        } else {  
            config = getConfiguration();  
        }  
        r = new Resources(assets, dm, config);  
        if (false) {  
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "  
                    + r.getConfiguration() + " appScale="  
                    + r.getCompatibilityInfo().applicationScale);  
        }  
    
        synchronized (this) {  
            WeakReference<Resources> wr = mActiveResources.get(key);  
            Resources existing = wr != null ? wr.get() : null;  
            if (existing != null && existing.getAssets().isUpToDate()) {  
                // Someone else already created the resources while we were  
                // unlocked; go ahead and use theirs.  
                r.getAssets().close();  
                return existing;  
            }  
    
            // XXX need to remove entries when weak references go away  
            mActiveResources.put(key, new WeakReference<Resources>(r));  
            return r;  
        }  
    }

    e. 因此我們可以通過 new Resources(assets, dm, config) 來創建加載 皮膚插件apk資源的Resources

    private Resources getSkinApkResource(Context context, String apkPath) {
    
        // 獲取加載插件apk的AssetManager
        AssetManager assetManager = createSkinApkAssetManager(apkPath);
    
        // 創建創建插件apk資源的Resources對象
        return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
    
    }

    f. 創建插件皮膚apk資源的Resources需要 AssetManager 對象,而AssetManager對象無法通過AssetManager類的構造方法來創建,于是采用反射機制來創建,并調用 addAssetPath 加載皮膚插件apk路徑:

    private AssetManager createSkinApkAssetManager(String apkPath) {
        AssetManager assetManager = null;
        try {
            assetManager = AssetManager.class.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        try {
            AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
                    assetManager, apkPath);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return assetManager;
    }
  7. 獲取加載皮膚插件apk資源的 Resources對象 后,我們還需要獲取皮膚背景圖的 id

    private int getSkinBackgroundId(String apkPath, String skinName,String apkPackageName) {
    
        int id = 0;
        try {
    
            /**使用DexClassLoader可以加載未安裝的apk中的dex
            * 構造方法的參數可參考文章前面的介紹
            */
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath,this.getDir(skinName,Context.MODE_PRIVATE).getAbsolutePath(),null,this.getClassLoader());
    
            // 運用反射:在皮膚插件R文件的drawable類中尋找插件資源的id 
            Class<?> forName = dexClassLoader.loadClass(apkPackageName+".R$drawable");
    
            // 獲取成員變量的值
            for (Field field : forName.getDeclaredFields()) {
    
                // 獲取包含“main_bg"名的資源id
                if (field.getName().contains("main_bg")) {
                    id = field.getInt(R.drawable.class);
                    return id;
                }
    
            }
    
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return id;
    }

三、皮膚插件apk的邏輯

  1. 注意皮膚插件的包名的設置,要與主應用的邏輯一致,可通過皮膚名獲取到包名。例:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.cxmscb.cxm.girl"
    >
  2. 皮膚插件不需要啟動Activity:可以清除Activity、其布局文件及其注冊。

  3. 在子程序的drawable中添加皮膚資源文件(注意文件名的設置與主應用的邏輯一致)。例:

后續問題:

1.在apk打包后可能會對皮膚插件進行混淆,混淆后的資源id會被更換,這樣會導致資源無法被主應用反射到。

2.上述主應用的邏輯并未完整,為了方便演示省去了皮膚插件的下載(不需要安裝)

  1. 皮膚插件apk最好存放在較私密的地方

Github : Github

參考:

https://yq.aliyun.com/articles/8129

ANDROID應用程序插件化研究之DEXCLASSLOADER

 

來自:http://blog.csdn.net/cxmscb/article/details/52448139

 

 本文由用戶 dangdang 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!