这篇文章主要使用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.上述主应用的逻辑并未完整,为了方便测试省去了皮肤插件的下载及安装