插件化通常出于模块解耦、动态升级、65535方法数限制、并行开发、节省升级流量等方面的考虑,用户也可以定制的选择需要的模块。插件化指将一个程序划分为不同的部分,如app的主题样式,组件化指提取通用及复用性比较高的构建。目前开源的插件化框架有DynamicLoadApk(很好的解决了资源访问和activity生命周期管理的问题)、AndroidDynamicLoader(使用scheme,像浏览网页一样调用插件)、360 DroidPlugin。插件化的实现,还得从ClassLoader说起。


PathClassLoader和DexClassLoader

在Android中ClassLoader是一个抽象类,在AS中通过ctrl + h可以看到类层级关系,PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,它们的区别是:

  • DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk
  • PathClassLoader只能加载系统中已经安装过的apk
  • BootClassLoader 加载系统类库

实际上两者的实现差别不大,DexClassLoader可以指定dex路径,加载外部存储中的dex,实际上是把dex复制到内部存储中了。DexClassLoader的构造函数中有4个参数:

1
2
3
4
5
6
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
dexPath: 需要装载的apk/jar/dex文件的路径
optimizedDirectory: 优化后的dex文件存放目录,不能为null
libraryPath: 目标类中使用的C/C++库的列表,每个目录用File.pathSeparator间隔开; 可以为 null
parent: 该类装载器的父装载器,一般用当前执行类的装载器

双亲委托机制

为了更好的保证 JAVA 平台的安全,当一个装载器被请求加载某个类时,先委托自己的 parent 去装载,如果 parent 能装载,则返回这个类对应的 Class 对象,否则,递归委托给父类的父类装载。当所有父类装载器都装载失败时,才由当前装载器装载。因此用户自定义的类装载器,不可能装载应该由父亲装载的可靠类,从而防止不可靠甚至恶意的代码代替本应该由父亲装载器装载的可靠代码。

Android中的委派机制是DexClassLoader->PathClassLoader->BootClassLoader。

DexClassLoader的使用过程

  • 通过PacageMangager获得指定的apk的安装的目录,dex的解压缩目录,c/c++库的目录
  • 创建一个 DexClassLoader实例
  • 加载指定的类返回一个Class
  • 然后使用反射调用这个Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void useDexClassLoader(){
//创建一个意图,用来找到指定的apk
Intent intent = new Intent("com.suchangli.android.plugin", null);
//获得包管理器
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
//获得指定的activity的信息
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
//获得包名
String pacageName = actInfo.packageName;
//获得apk的目录或者jar的目录
String apkPath = actInfo.applicationInfo.sourceDir;
//dex解压后的目录,注意,这个用宿主程序的目录,android中只允许程序读取写自己
//目录下的文件
String dexOutputDir = getApplicationInfo().dataDir;
//native代码的目录
String libPath = actInfo.applicationInfo.nativeLibraryDir;
//创建类加载器,把dex加载到虚拟机中
DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutputDir, libPath,
this.getClass().getClassLoader());
//利用反射调用插件包内的类的方法
Class<?> clazz = calssLoader.loadClass(pacageName+".Plugin1");
Object obj = clazz.newInstance();
Class[] param = new Class[2];
param[0] = Integer.TYPE;
param[1] = Integer.TYPE;
Method method = clazz.getMethod("function1", param);
Integer ret = (Integer)method.invoke(obj, 1,12);
}

插件化的坑

Android程序和标准的Java程序最大的区别就在于他们的上下文环境(Context)不同,各种Android动态加载框架核心要解决的东西也正是“如何给外部的新类提供上下文环境”的问题。

  1. 同一个Class = 相同的 ClassName + PackageName + ClassLoader,自定义的ClassLoader其它ClassLoader就算加载的是同一个类文件,也会抛出ClassCastException
  2. Android中许多组件类(如Activity、Service等)是需要在Manifest文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作
  3. res资源是Android开发中经常用到的,而Android是把这些资源用对应的R.id注册好,运行时通过这些ID从Resource实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到R.id的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的Resource实例中保存的资源ID对不上

架构

1. 使用接口Interface

Host宿主和插件Plugin需要定义完全相同的接口类(包名也需一致,ClassLoader也需要保持一致),插件apk的入口需要实现这个接口。

由于插件接口存在于两个不同的dex文件中,插件接口被同一个加载器装载了两次,每个dex文件有一个类型id,检测到不一致所以报错。需要保证插件接口只被装载一次:
可以把插件接口提取出来打包成plugin.jar,在宿主apk中:

1
compile files('libs/plugin.jar')

在插件apk中:

1
provided files('libs/plugin.jar')

2. 获取插件中资源文件(TODO)

1
2
3
4
5
6
7
8
9
public int getResIdFromPlugin(Plugin plugin,String resName,String defType){
try {
Resources res = context.getPackageManager().getResourcesForApplication(plugin.packageName);
return res.getIdentifier(resName,defType,plugin.packageName);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 0;
}

完整代码(待续)

PluginManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class PluginManager {
private List<Plugin> mPlugins = null;
public void addPlugin(Plugin plugin){
if (mPlugins==null) mPlugins = new ArrayList<>();
mPlugins.add(plugin);
}
DexClassLoader classLoader; //TODO
public void activatePlugin(Plugin plugin){
if (plugin==null) return;
try {
Class<?> clazz = classLoader.loadClass(plugin.packageName+"."+plugin.enterClassName);
IPlugin obj = (IPlugin) clazz.newInstance();
obj.init();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
public void activatePlugin(Plugin plugin, Uri uri){
if (plugin==null) return;
try {
Class<?> clazz = classLoader.loadClass(plugin.packageName+"."+plugin.enterClassName);
IPlugin obj = (IPlugin) clazz.newInstance();
obj.init(uri);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
public void getDrawableFromPlugin(){}

IPlugin:

1
2
3
4
5
6
7
public interface IPlugin {
void init();
//由于参数数据类型可能不同,使用uri便于传参
void init(Uri uri);
}

Plugin:

1
2
3
4
5
6
7
public class Plugin {
public int id;
public int desc; //描述
public String packageName; //包名
public String enterClassName; //入口类名
public String url; //dex/jar/apk获取的地址
}

参考