安卓之插件化开发使用 PathClassLoader 来动态更换皮肤

NellyUHMZ 7年前
   <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>