安卓之插件化開發使用 DexClassLoader&AssetManager 來更換皮膚
這篇文章主要使用DexClassLoader來實現插件化更換皮膚,即將皮膚獨立出來做成一個皮膚插件apk,當用戶想使用該皮膚時需下載(不需要安裝)對應的皮膚插件apk
效果圖
【為方便測試,主要通過改變背景圖來簡單地展示皮膚更換】
一、DexClassLoader
如果使用DexClassLoader來實現插件化皮膚更換,我們需要去下載(不需安裝)我們的皮膚插件apk:
-
DexClassLoader 可以加載外部的 apk、jar 或 dex文件,并且會在指定的 outpath 路徑存放其 dex 文件。
-
構造函數:
DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
dexPath:需被解壓的apk路徑,不能為空。
optimizedDirectory:解壓后的.dex文件的存儲路徑,不能為空。這個路徑強烈建議使用應用程序的私有路徑,不要放到sdcard上,否則代碼容易被注入攻擊。
libraryPath:c/c++庫的路徑,可以為null,若有相關庫,須填寫。
parent:父親加載器,一般為context.getClassLoader(),使用當前上下文的類加載器。
-
下面為什么要使用到擴展DexClassLoader?:
這里使用DexClassLoader是為了加載 插件apk 中的dex文件,加載dex文件后系統就可以在dex中找到我們要使用的class類R.java,在R.java中包含著資源等的id,通過id我們可以獲取到資源。
二、主應用apk的邏輯
-
為了方便測試,我們將插件apk存放在SD卡中,主應用apk再去獲取。所以在主應用中需要讀寫SD卡內容的權限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
-
使用SharedPreferences來記錄皮膚的改變:
SharedPreferences skinType; skinType = getPreferences(Context.MODE_PRIVATE); String skin = skinType.getString("skin",null); if(skin!=null) installSkin(skin);
-
按鈕點擊事件的響應:
public void changeSkin1(View view) { installSkin("dog"); } public void changeSkin2(View view) { installSkin("girl"); }
-
所以我們更好皮膚的重點在 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(); } }
-
上面我們是通過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; }
-
上面我們是通過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; }
-
獲取加載皮膚插件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的邏輯
-
注意皮膚插件的包名的設置,要與主應用的邏輯一致,可通過皮膚名獲取到包名。例:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.cxmscb.cxm.girl" >
-
皮膚插件不需要啟動Activity:可以清除Activity、其布局文件及其注冊。
-
在子程序的drawable中添加皮膚資源文件(注意文件名的設置與主應用的邏輯一致)。例:
后續問題:
1.在apk打包后可能會對皮膚插件進行混淆,混淆后的資源id會被更換,這樣會導致資源無法被主應用反射到。
2.上述主應用的邏輯并未完整,為了方便演示省去了皮膚插件的下載(不需要安裝)
- 皮膚插件apk最好存放在較私密的地方
Github : Github
參考:
https://yq.aliyun.com/articles/8129
ANDROID應用程序插件化研究之DEXCLASSLOADER
來自:http://blog.csdn.net/cxmscb/article/details/52448139