安卓之插件化開發使用 PathClassLoader 來動態更換皮膚

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

這篇文章主要使用PathClassLoader來實現插件化更換皮膚

(將皮膚獨立出來做成一個皮膚插件apk,當用戶想使用該皮膚時需下載對應的皮膚插件)

效果圖:

【主要通過改變背景圖來簡單地展示皮膚更換】

一、PathClassLoader

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

  1. Android中有兩個ClassLoader分別為 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader。

  2. PathClassLoader 不能直接從 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已經安裝過的 apk(因為安裝過的 apk 在 cache 【 /data/dalvik-cache】中存在緩存的 dex 文件)。

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

二、主應用apk的邏輯

  1. 在清單文件中設置sharedUserId:

    設置Shared User id:擁有同一個User id的多個APK可以配置成運行在同一個進程中.所以默認就是可以互相訪問任意數據.

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.cxmscb.cxm.intalledplugdemo"
        android:sharedUserId="cxm.scb.skin"
    >
    ...
    ...

    實際上,與插件apk設置用一個sharedUserId后,可以獲取插件apk的上下文Context,獲取懂到上下文后就可以做很多事了:

    //獲取皮膚插件apk的上下文,同時忽略安全警告且可訪問代碼
    Context plugContext = this.createPackageContext("插件apk包名",Context.CONTEXT_IGNORE_SECURITY|Context.CONTEXT_INCLUDE_CODE);
  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){
    
        //查找該皮膚插件是否已被安裝
        String packageName = findPlugins(skinName);
        if (packageName==null) {
    
            // 找不到皮膚時。
            //【這里應該有一個下載安卓皮膚apk的邏輯,為了演示方便則省去】
            Toast.makeText(this, "請先安裝皮膚", Toast.LENGTH_SHORT).show();
            // 皮膚插件安裝后被刪除的情況,清空存儲
            if (skinType.getString("skin", skinName).equals(skinName))
                skinType.edit().clear().commit();
        }
        else { //皮膚插件已被安裝
            try {
    
                //獲取皮膚插件apk的上下文,同時忽略安全警告且可訪問代碼
                Context plugContext = this.createPackageContext(packageName,Context.CONTEXT_IGNORE_SECURITY|Context.CONTEXT_INCLUDE_CODE);
    
                //獲取插件背景的資源文件id
                int bgId = getSkinBackgroundId(packageName,plugContext);
    
                //設置背景且保存皮膚設置
                rl.setBackgroundDrawable(plugContext.getResources().getDrawable(bgId));
                skinType.edit().putString("skin",skinName).commit();
    
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
    
    
        }
    }

    上述查找皮膚插件apk是否已被安裝的函數findPlugins如下:

    private String findPlugins(String plugName) {
    
        PackageManager pm = this.getPackageManager();
        //獲取全部安裝包包名:
        List<PackageInfo> installedPackages = pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);
    
        //通過shareduserid查找插件包信息:
    
        for (PackageInfo info : installedPackages) {
            String packageName = info.packageName;
            String sharedUserId = info.sharedUserId;
            if (sharedUserId == null || !sharedUserId.equals("cxm.scb.skin") || packageName.equals(getPackageName())) {
                //sharedUserId不對或者包名為主程序相同時:跳過
                continue;
            }
            // 符合條件:獲取插件應用名:
            String appLabel = pm.getApplicationLabel(info.applicationInfo).toString();
    
            // 應用名匹配:返回插件的包名
            if (appLabel.equals(plugName)) {
                return info.packageName;
            }
        }
        // 找不到返回null
        return null;
    }

    上述獲取皮膚插件中的資源文件id的函數getSkinBackgroundId如下:

    獲取插件資源id:

    R.java:R文件中包含著一個應用的基本資源id.可以通過使用PathClassLoader加載插件apk的dex文件,通過反射來獲取R這個類的信息。

    private int getSkinBackgroundId(String packageName,Context plugContext) {
    
        int id = 0;
        try {
            // 在插件R文件中尋找插件資源的id 
            PathClassLoader pathClassLoader = new PathClassLoader(plugContext.getPackageResourcePath(),ClassLoader.getSystemClassLoader());
            // plugContext.getPackageResourcePath() 獲取安裝過的apk路徑:/data/app/包名-1.apk
            // 運用反射機制來獲取到R文件中的drawble靜態類:
            Class<?> forName = Class.forName(packageName + ".R$drawable", true, pathClassLoader);
    
            // 獲取drawble類中的成員變量的值
            for (Field field:forName.getDeclaredFields()){
                if(field.getName().contains("main_bg")){
    
                   // 查找到背景圖的名字時獲取id值
                   id = field.getInt(R.drawable.class);
                   return id;
                }
            }
    
        }catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        //返回0 
        return id;
    }

二、皮膚插件apk的邏輯

  1. 在清單文件中設置sharedUserId:使皮膚插件與主插件運行在同一進程

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
    </application>
  3. 設置皮膚插件apk的label名:

    在主應用中是通過sharedUserId和label應用名來得到皮膚插件apk的包名的

    需要將label修改為我們設置的皮膚名字:

    android:label="@string/app_name"
  4. 在子程序的drawable中添加背景文件(注意文件名的設置):

后續問題:

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

> 2.上述主應用的邏輯并未完整,為了方便測試省去了皮膚插件的下載及安裝

Github: Github

 

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

 

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