###1. ClassLoader
“类装载器”(ClassLoader),顾名思义,就是用来动态装载class文件的。标准的Java SDK中有个ClassLoader类,借助此类可以装载需要的class文件,前提是ClassLoader类初始化必须指定class文件的路径。
每一个ClassLoader必须有一个父ClassLoader,在装载Class文件时,子ClassLoader会先请求其父ClassLoader加载该文件,只有当其父ClassLoader找不到该文件时,子ClassLoader才会继承装载该类。这是一种安全机制。对于Android而言,最终的apk文件包含的是dex类型的文件,dex文件是将class文件重新打包,打包的规则又不是简单地压缩,而是完全对class文件内部的各种函数表,变量表进行优化,产生一个新的文件,即dex文件。因此加载这种特殊的Class文件就需要特殊的类加载器。 android中提供了如下的2个类加载器:
- DexClassLoader :可以加载文件系统上的jar、dex、apk
- PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk
- URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用,尽管还有这个类
jar必须转换成dalvik所能识别的字节码文件,转换工具可以使用android sdk中platform-tools目录下的dx, 转换命令示例:
dx --dex --output=dest.jar src.jar
利用这种技术, 可以实现apk插件, 被加载的apk称之为插件,因为机制类似于生物学的”寄生”,加载了插件的应用也被称为宿主。
###2. DexClassLoader
DexClassLoader的原型如下:
DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
形参的意义如下:
- dexPath 需要装载的APK或者Jar文件的路径。包含多个路径用File.pathSeparator间隔开,在Android上默认是 “:”
- optimizedDirectory 优化后的dex文件存放目录,不能为null
- libraryPath 目标类中使用的C/C++库的列表,每个目录用File.pathSeparator间隔开; 可以为 null
- parent 该类装载器的父装载器,一般用当前执行类的装载器, 即
this.getClass().getClassLoader()
DexClassLoader的基本使用流程如下:
- 通过PacageMangager获得指定的apk的安装的目录,dex的解压缩目录,c/c++库的目录
- 创建一个 DexClassLoader实例
- 加载指定的类返回一个Class
- 然后使用反射来调用这个Class
需要注意的是optimizedDirectory参数, 不要把优化优化后的classes文件存放到外部存储设备上,防代码注入攻击
###3. DexClassLoader使用示例
####3.1 插件apk
使用Android Studio建立一个app, 使用“Add No Activity”模板(当然也可以使用带Activity的模板), 然后添加一个plug类
package com.example.sven.plug1;
/**
* Created by sven on 16-3-2.
*/
public class plug {
public String whoAmI()
{
return getClass().getName();
}
public int add(int a, int b)
{
return a + b;
}
}
编译后, 将apk安装到android设备上, 由于使用的是“Add No Activity”模板, 因此launcher并不显示图标
####3.2 宿主apk
使用Android Studio建立一个app, 使用“Blank Activity”模板(当然也可以使用带Activity的模板), 然后在floatActionBar的点击事件中动态加2载插件apk
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String result = "null";
ApplicationInfo appInfo = null;
String packageName = "com.example.sven.plug1";
try {
appInfo = getPackageManager().getApplicationInfo(packageName, 0);
String sourceDir = appInfo.sourceDir;
String outDir = getApplicationInfo().dataDir;
String libraryDir = appInfo.nativeLibraryDir;
DexClassLoader dexcl = new DexClassLoader(sourceDir, outDir,
libraryDir, this.getClass().getClassLoader());
Class<?> loadClass = dexcl.loadClass(packageName + ".plug");
Object instance = loadClass.newInstance();
Method methodHello = loadClass.getMethod("whoAmI", new Class[0]);
Method methodAdd = loadClass.getMethod("add", new Class[]{Integer.TYPE, Integer.TYPE});
result = (String)methodHello.invoke(instance) + " : " + ((int)methodAdd.invoke(instance, 3, 5));
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
Snackbar.make(view, result, Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
.....
}
###4. 动态加载apk的缺陷
上述只是一个简单的动态加载apk示例, 事实上, 动态加载apk还涉及到几个难点:
- 资源的访问 :因为将apk加载到宿主程序中去执行,就无法通过宿主程序的Context去取到apk中的资源,比如图片,文本, layout等(因此, 插件apk中若涉及到ui, 需要在java code中绘制),这是很好理解的,因为apk已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文,用别人的Context是无法得到自己的资源的,这个问题貌似可以这么解决:将apk中的资源解压到某个目录,然后通过文件去操作资源,这只是理论上可行,实际上还是会有很多的难点的。
- activity的生命周期,因为apk被宿主程序加载执行后,它的activity其实就是一个普通的类,正常情况下,activity的生命周期是由系统来管理的,现在被宿主程序接管了以后,如何替代系统对apk中的activity的生命周期进行管理是有难度的,这个问题比资源的访问好解决一些,比如我们可以在宿主程序中模拟activity的生命周期并合适地调用apk中activity的生命周期方法