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有任何影响,而有的话就供用户选择性使用了。