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

这篇文章主要使用PathClassLoader来实现插件化更换皮肤
(将皮肤独立出来做成一个皮肤插件apk,当用户想使用该皮肤时需下载对应的皮肤插件)

效果图:

【主要通过改变背景图来简单地展示皮肤更换】

一、PathClassLoader

如果使用PathClassLoader来实现插件化皮肤更换,我们需要去下载并安装我们的皮肤插件apk:

  1. Android中有两个ClassLoader分别为 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader。

  2. PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk(因为安装过的 apk 在 cache 【 /data/dalvik-cache】中存在缓存的 dex 文件)。

  3. DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。

二、主应用apk的逻辑

  1. 在清单文件中设置sharedUserId:

    设置Shared User id:拥有同一个User id的多个APK可以配置成运行在同一个进程中.所以默认就是可以互相访问任意数据.

    
    ...
    ...
    

    实际上,与插件apk设置用一个sharedUserId后,可以获取插件apk的上下文Context,获取懂到上下文后就可以做很多事了:

    //获取皮肤插件apk的上下文,同时忽略安全警告且可访问代码
    Context plugContext = this.createPackageContext("插件apk包名",Context.CONTEXT_IGNORE_SECURITY|Context.CONTEXT_INCLUDE_CODE);
    
  2. 使用SharedPreferences来记录皮肤的改变:

    SharedPreferences skinType; 
    
    skinType = getPreferences(Context.MODE_PRIVATE);
    String skin = skinType.getString("skin",null);
    
    if(skin!=null) installSkin(skin);
    
  3. 点击事件的响应:

    public void changeSkin1(View view) {
        installSkin("Dog");
    }
    
    public void changeSkin2(View view) {
        installSkin("Girl");
    }
    
  4. 重点在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 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的逻辑

  1. 在清单文件中设置sharedUserId:使皮肤插件与主插件运行在同一进程

    
    
  2. 皮肤插件不需要启动Activity:可以清除Activity、其布局文件及其注册。

    
    
    
  3. 设置皮肤插件apk的label名:

    在主应用中是通过sharedUserId和label应用名来得到皮肤插件apk的包名的
    需要将label修改为我们设置的皮肤名字:

    android:label="@string/app_name"
    
  4. 在子程序的drawable中添加背景文件(注意文件名的设置):

    这里写图片描述

后续问题:

> 1.在apk打包后可能会对皮肤插件进行混淆,混淆后的资源id会被更换,这样会导致资源无法被主应用反射到。如果没必要,可以不要对资源id进行混淆。。

> 2.上述主应用的逻辑并未完整,为了方便测试省去了皮肤插件的下载及安装

Github:Github

你可能感兴趣的:(Android学习)