插件化入门

1,131 阅读3分钟

1、什么是动态加载技术?

动态加载技术就是使用类加载器加载相应的apk、dex、jar(必须含有dex文件),再通过反射获得该apk、dex、jar内部的资源(class、图片、color等等)进而供宿主app使用。

2、关于动态加载使用的类加载器

使用动态加载技术时,一般需要用到这两个类加载器:

  • PathClassLoader - 只能加载已经安装的apk,即/data/app目录下的apk。
  • DexClassLoader - 能加载手机中未安装的apk、jar、dex,只要能在找到对应的路径。 这两个加载器分别对应使用的场景各不同,所以接下来,分别讲解它们各自加载相同的插件apk的使用。

3、PathClassLoader加载已安装的apk插件

3.1、首先我们需要知道一个manifest中的属性:SharedUserId

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.ireadygo.app.dynamicload"
          android:sharedUserId="com.ireadygo.app.share">

3.2、通过PackageInfo.sharedUserId来获取手机安装的插件APK

public static List<PluginBean> findAllPlugin(Context context) {
        List<PluginBean> pluginBeanList = new ArrayList<>();
        PackageManager packageManager = context.getPackageManager();
        List<PackageInfo> packageInfos = packageManager.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES);
        for (PackageInfo info : packageInfos) {
            String pkgName = info.packageName;
            String shareUserId = info.sharedUserId;
            if (shareUserId != null && shareUserId.equals("com.ireadygo.app.share") && !pkgName.equals(context.getPackageName())) {
                String label = packageManager.getApplicationLabel(info.applicationInfo).toString();//得到插件apk的名称
                PluginBean bean = new PluginBean(label, pkgName);
                pluginBeanList.add(bean);
            }
        }
        return pluginBeanList;
}

3.3、通过插件包名创建插件Context

public static Drawable dynamicLoadResource(Context context, String pkgName) throws PackageManager.NameNotFoundException {
        // 1. 通过插件包名创建插件的Context
        Context pluginContext = context.createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
        // 2. 通过PathClassLoader加载插件资源id
        int pluginResourceId = dynamicLoadResourceId(pkgName, pluginContext);
        if (pluginResourceId == -1) {
            return null;
        }
        // 3. 通过插件的Context获取资源;必须使用插件上下文加载,否则Resources$NotFoundException
        Drawable drawable = pluginContext.getResources().getDrawable(pluginResourceId);
        return drawable;
}

3.4、通插PathClassLoader加载插件资源

private static int dynamicLoadResourceId(String pkgName, Context pluginContext) {
        //第一个参数为包含dex的apk或者jar的路径,第二个参数为父加载器
        PathClassLoader pathClassLoader = new PathClassLoader(pluginContext.getPackageResourcePath(), ClassLoader.getSystemClassLoader());
        try {
            Class<?> clazz = Class.forName(pkgName + ".R$mipmap", true, pathClassLoader);
            Field field = clazz.getDeclaredField("one");
            int resourceId = field.getInt(R.mipmap.class);
            return resourceId;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return -1;
}

4、DexClassLoader加载未安装的apk,提供资源供宿主app使用

关于动态加载未安装的apk,思路如下:
首先我们得到事先知道我们的插件apk存放在哪个目录下,然后分别得到插件apk的信息(名称、包名等),然后显示可用的插件,最后动态加载apk获得资源。 按照上面这个思路,我们需要解决几个问题:
1、怎么得到未安装的apk的信息
2、怎么得到插件的context或者Resource,因为它是未安装的不可能通过createPackageContext(...);方法来构建出一个context,所以这时只有在Resource上下功夫。

4.1、通过PackageManager.getPackageArchiveInfo()获取未安装的apk信息

public String[] getUninstallApkInfo(Context context, String archiveFilePath) {
        String[] info = new String[2];
        PackageManager pm = context.getPackageManager();
        PackageInfo packageInfo = pm.getPackageArchiveInfo(archiveFilePath, PackageManager.MATCH_UNINSTALLED_PACKAGES);
        if (packageInfo != null) {
            ApplicationInfo applicationInfo = packageInfo.applicationInfo;
            info[0] = pm.getApplicationLabel(applicationInfo).toString();
            info[1] = applicationInfo.packageName;
            Log.i("lmq", "packageName = " + info[1]);
        } else {
            Log.w("lmq", "packageInfo = " + packageInfo);
        }
        return info;
}

4.2、通过反射获取未安装插件apk的Resources对象

public static Resources getPluginResource(Context context, String apkDir, String apkName) {
        try {
            AssetManager manager = AssetManager.class.newInstance();
            Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);
            //反射调用方法addAssetPath(String path)
            //第二个参数是apk的路径:Environment.getExternalStorageDirectory().getPath()+File.separator+"plugin"+File.separator+"apkplugin.apk"
            
            //将未安装的Apk文件的添加进AssetManager中,第二个参数为apk文件的路径带apk名
            addAssetPath.invoke(manager, apkDir + File.separator + apkName);   
            
            Resources superResources = context.getResources();
            Resources pluginResources = new Resources(manager, superResources.getDisplayMetrics(), superResources.getConfiguration());
            return pluginResources;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
}

通过反射获取AssetManager中的内部方法addAssetPath,将未安装的apk路径传入从而添加进assetManager中,然后通过new Resource把assetManager传入构造方法中,进而得到未安装apk对应的Resource对象。

4.3、通过DexClassLoader加载插件apk中的资源

public static int loadUninstallPluginResId(Context context, String apkDir, String apkName, String pkgName) {
        File optimizedDirectory = context.getDir("dex", Context.MODE_PRIVATE);//在应用安装目录下创建一个名为app_dex文件夹目录,如果已经存在则不创建
        Log.i("lmq", optimizedDirectory.getPath().toString());// /data/data/com.example.dynamicloadapk/app_dex

        //参数:1、包含dex的apk文件或jar文件的路径,2、apk、jar解压缩生成dex存储的目录,3、本地library库目录,一般为null,4、父ClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkDir + File.separator + apkName,
                optimizedDirectory.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());

        //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
        Class<?> clazz = null;
        try {
            clazz = dexClassLoader.loadClass(pkgName + ".R$mipmap");
            Field field = clazz.getDeclaredField("two");
            int resId = field.getInt(R.id.class);
            return resId;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return -1;
}

总结: 插件化开发是插件与宿主app进行解耦了,即使在没有插件情况下,也不会对宿主app有任何影响,而有的话就供用户选择性使用了。