安卓之插件化開發使用 PathClassLoader 來動態更換皮膚
這篇文章主要使用PathClassLoader來實現插件化更換皮膚
(將皮膚獨立出來做成一個皮膚插件apk,當用戶想使用該皮膚時需下載對應的皮膚插件)
效果圖:
【主要通過改變背景圖來簡單地展示皮膚更換】
一、PathClassLoader
如果使用PathClassLoader來實現插件化皮膚更換,我們需要去下載并 安裝 我們的皮膚插件apk:
-
Android中有兩個ClassLoader分別為 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader。
-
PathClassLoader 不能直接從 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已經安裝過的 apk(因為安裝過的 apk 在 cache 【 /data/dalvik-cache】中存在緩存的 dex 文件)。
-
DexClassLoader 可以加載外部的 apk、jar 或 dex文件,并且會在指定的 outpath 路徑存放其 dex 文件。
二、主應用apk的邏輯
-
在清單文件中設置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);
-
使用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){ //查找該皮膚插件是否已被安裝 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的邏輯
-
在清單文件中設置sharedUserId:使皮膚插件與主插件運行在同一進程
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.cxmscb.cxm.girl" android:sharedUserId="cxm.scb.skin" >
-
皮膚插件不需要啟動Activity:可以清除Activity、其布局文件及其注冊。
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> </application>
-
設置皮膚插件apk的label名:
在主應用中是通過sharedUserId和label應用名來得到皮膚插件apk的包名的
需要將label修改為我們設置的皮膚名字:
android:label="@string/app_name"
-
在子程序的drawable中添加背景文件(注意文件名的設置):
后續問題:
> 1.在apk打包后可能會對皮膚插件進行混淆,混淆后的資源id會被更換,這樣會導致資源無法被主應用反射到。如果沒必要,可以不要對資源id進行混淆。。
> 2.上述主應用的邏輯并未完整,為了方便測試省去了皮膚插件的下載及安裝
Github: Github
來自:http://blog.csdn.net/cxmscb/article/details/52435389