安卓之插件化开发使用 PathClassLoader 来动态更换皮肤
NellyUHMZ
8年前
<p>这篇文章主要使用PathClassLoader来实现插件化更换皮肤</p> <p>(将皮肤独立出来做成一个皮肤插件apk,当用户想使用该皮肤时需下载对应的皮肤插件)</p> <p>效果图:</p> <p><img src="https://simg.open-open.com/show/dcd911f44c9edb5f114ea2858d67702e.gif"></p> <p>【主要通过改变背景图来简单地展示皮肤更换】</p> <h3>一、PathClassLoader</h3> <p>如果使用PathClassLoader来实现插件化皮肤更换,我们需要去下载并 <strong>安装</strong> 我们的皮肤插件apk:</p> <ol> <li> <p>Android中有两个ClassLoader分别为 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader。</p> </li> <li> <p>PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk(因为安装过的 apk 在 cache 【 /data/dalvik-cache】中存在缓存的 dex 文件)。</p> </li> <li> <p>DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。</p> </li> </ol> <h3>二、主应用apk的逻辑</h3> <ol> <li> <p>在清单文件中设置sharedUserId:</p> <p>设置Shared User id:拥有同一个User id的多个APK可以配置成运行在同一个进程中.所以默认就是可以互相访问任意数据.</p> <pre> <code class="language-java"><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.cxmscb.cxm.intalledplugdemo" android:sharedUserId="cxm.scb.skin" > ... ...</code></pre> <p>实际上,与插件apk设置用一个sharedUserId后,可以获取插件apk的上下文Context,获取懂到上下文后就可以做很多事了:</p> <pre> <code class="language-java">//获取皮肤插件apk的上下文,同时忽略安全警告且可访问代码 Context plugContext = this.createPackageContext("插件apk包名",Context.CONTEXT_IGNORE_SECURITY|Context.CONTEXT_INCLUDE_CODE);</code></pre> </li> <li> <p>使用SharedPreferences来记录皮肤的改变:</p> <pre> <code class="language-java">SharedPreferences skinType; skinType = getPreferences(Context.MODE_PRIVATE); String skin = skinType.getString("skin",null); if(skin!=null) installSkin(skin);</code></pre> </li> <li> <p>点击事件的响应:</p> <pre> <code class="language-java">public void changeSkin1(View view) { installSkin("Dog"); } public void changeSkin2(View view) { installSkin("Girl"); }</code></pre> </li> <li> <p>重点在 <strong>installSkin</strong> 函数中:</p> <pre> <code class="language-java">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(); } } }</code></pre> <p>上述查找皮肤插件apk是否已被安装的函数findPlugins如下:</p> <pre> <code class="language-java">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; }</code></pre> <p>上述获取皮肤插件中的资源文件id的函数getSkinBackgroundId如下:</p> <p>获取插件资源id:</p> <p>R.java:R文件中包含着一个应用的基本资源id.可以通过使用PathClassLoader加载插件apk的dex文件,通过反射来获取R这个类的信息。</p> <pre> <code class="language-java">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; }</code></pre> </li> </ol> <h3>二、皮肤插件apk的逻辑</h3> <ol> <li> <p>在清单文件中设置sharedUserId:使皮肤插件与主插件运行在同一进程</p> <pre> <code class="language-java"><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.cxmscb.cxm.girl" android:sharedUserId="cxm.scb.skin" ></code></pre> </li> <li> <p>皮肤插件不需要启动Activity:可以清除Activity、其布局文件及其注册。</p> <pre> <code class="language-java"><application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> </application></code></pre> </li> <li> <p>设置皮肤插件apk的label名:</p> <p>在主应用中是通过sharedUserId和label应用名来得到皮肤插件apk的包名的</p> <p>需要将label修改为我们设置的皮肤名字:</p> <pre> <code class="language-java">android:label="@string/app_name"</code></pre> </li> <li> <p>在子程序的drawable中添加背景文件(注意文件名的设置):</p> <p><img src="https://simg.open-open.com/show/56eaea80621f3c42a58dc2aa99243d12.jpg"></p> </li> </ol> <h3>后续问题:</h3> <pre> <code class="language-java">> 1.在apk打包后可能会对皮肤插件进行混淆,混淆后的资源id会被更换,这样会导致资源无法被主应用反射到。如果没必要,可以不要对资源id进行混淆。。 > 2.上述主应用的逻辑并未完整,为了方便测试省去了皮肤插件的下载及安装</code></pre> <h3>Github: <a href="/misc/goto?guid=4959740255747384516" rel="nofollow,noindex">Github</a></h3> <p> </p> <p>来自:http://blog.csdn.net/cxmscb/article/details/52435389</p> <p> </p>