Java_ClassLoader(ClassLoader / 自定义ClassLoader / DexClassLoader)
本文由 Luzhuo 编写,转发请保留该信息.
原文: http://blog.youkuaiyun.com/rozol/article/details/77758194
ClassLoader 用来动态加载class文件到内存中使用的
ClassLoader 使用双亲委托模型
来搜索类的
ClassLoader 在Android中有两个子类来加载.dex文件, 分别是 DexClassLoader / PathClassLoader
DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex
PathClassLoader 只可以加载 系统中已安装的apk
原理
/**
* ClassLoader
* ClassLoader用来动态加载class文件到内存中使用的;
* JVM并不会一次性加载程序的所有class文件, 而是根据所需加载.
*
*
* Java内置的ClassLoader
* BootStrap ClassLoader 启动类加载器 (c++编写, 嵌于JVM内核), 负责加载jdk的核心库类 (@Code = printBootStrapClassPath())
* Extension ClassLoader 扩展类加载器 (Java编写, 继承于ClassLoader), 负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar (@Code = printExtensionClassPath())
* App ClassLoader 应用程序(系统)类加载器 (Java编写, 继承于ClassLoader), 负责加载应用程序classpath目录下的所有jar和class文件 (@Code = printAppClassPath())
*
*
* ClassLoader原理 (@Code = classLoaderMod())
* 使用 `双亲委托模型` 来搜索类
* 每个ClassLoader都有一个父类加载器的引用, 如下所示, BootStrap ClassLoader 没有父类加载器, 可作为其他ClassLoader的父类加载器
*
* 启动类加载器 (BootStrap ClassLoader)
* ↑
* 扩展类加载器 (Extension ClassLoader)
* ↑
* 应用程序(系统)类加载器 (App ClassLoader)
* ↑
* 自定义类加载器 (<? extend ClassLoader>)
*
* 当ClassLoader需要加载某个类时, 从 BootStrap ClassLoader 开始检索, 如果没有找到则从Extension ClassLoader 检索, 同理依次传递, 如果都没有, 则麻烦委托者自己从文件或网络检索, 还是没有则抛 ClassNotFoundException 异常.
* 如果某个类加载器找到该类文件, 则将其生成类的定义, 加载到内存当中, 返回实例对象.
* 如果该类已经存在缓存中, 类加载器将不再去检索, 而是将缓存中的类返回.
*
* 该模式的好处: 从上到下依次检索, 避免了 恶意代码冒充系统类代码 安全隐患;
* 因为类加载器只要从自家的类加载中检索到所需的系统代码, 就不会执行用户写的类加载器, 从而避免被调包
*
*
* 判断两个类是否相同?
* 如果两个类的 全类名 相同, 且被同一个类加载器加载的, 即为同一个class;
* 如果两个类的 全类名 相同, 但被非同一个类加载器加载的, 即为非同一个class.
*
* @author Luzhuo
*/
public class Test{
public static void main(String[] args) throws ClassNotFoundException, Exception {
// 输出 BootStrap ClassLoader 加载的文件路径
printBootStrapClassPath();
// 输出 Extension ClassLoader 加载的文件路径
printExtensionClassPath();
// 输出 App ClassLoader 加载的文件路径
printAppClassPath();
// 双亲委托模型
classLoaderMod();
}
/**
* 输出BootStrap ClassLoader 加载的文件路径
*/
private static void printBootStrapClassPath(){
// 方式1: 源码中的文件
final String bootClassPath = System.getProperty("sun.boot.class.path");
System.out.println("BootStrap ClassLoader - path: " + bootClassPath);
// 方式2: 类提供的方法
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
}
/**
* 输出 Extension ClassLoader 加载的文件路径
*/
private static void printExtensionClassPath(){
final String extensionClassPath = System.getProperty("java.ext.dirs");
System.out.println("Extension ClassLoader - path: " + extensionClassPath);
}
/**
* 输出 App ClassLoader 加载的文件路径
*/
private static void printAppClassPath(){
final String appClassPath = System.getProperty("java.class.path");
System.out.println("App ClassLoader - path: " + appClassPath);
}
/**
* 双亲委托模型
* ClassLoader原理 (Code = classLoaderMod())
*/
private static void classLoaderMod(){
ClassLoader loader = Test.class.getClassLoader();
while(loader != null) {
System.out.println("classLoaderMod: " + loader);
loader = loader.getParent(); // 获取父类加载器
}
// classLoaderMod: sun.misc.Launcher$AppClassLoader@73d16e93
// classLoaderMod: sun.misc.Launcher$ExtClassLoader@15db9742
// BootStrap ClassLoader 为C++所写, 故未打印
}
}
源码
双亲委托模型
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// ↓↓↓
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// ↓↓↓
c = parent.loadClass(name, false);
} else {
// ↓↓↓
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// ↓↓↓
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}
从 ClassLoader 源码可知, 这是个抽象类, 首先调用
Class<?> c = findLoadedClass(name)
去检查该类是否已经被加载, 如果已被加载则返回被加载的类, 如果未被加载则调用c = parent.loadClass(name, false);
去父加载器检索, 如果父加载器为null则说明父加载器是BootStrap ClassLoader
则调用c = findBootstrapClassOrNull(name);
去查找该类, 如果BootStrap ClassLoader
没有检索到, 则调用Class<?> findClass(String name)
方法, 按自己的规则去检索.ClassLoader 的子类
- 其中
MyClassLoader
是自定义ClassLoader, 继承ClassLoader, 复写findClass(String name)
方法
- 其中
示意图如下, 双亲委托模型 容易让人联想到责任链设计模式 (但是他们不一样)
自定义ClassLoader
自定义ClassLoader: 继承 java.lang.ClassLoader ;复写 Class
/**
* 自定义类加载器
* 需求: 加载网络上的Class文件, 并生成对象
*
* 自定义类加载器步骤: 继承 java.lang.ClassLoader ;复写 Class<?> findClass(String name) 方法
* @author Luzhuo
*
*/
public class MyClassLoader extends ClassLoader{
private String classUrl;
public MyClassLoader(String url) {
this.classUrl = url;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
// 根据名字, 获取Class文件的字节码数据
byte[] classData = getClassData(name);
if (classData == null) throw new ClassNotFoundException(name);
// 将Class文件的字节码数据生成Class类实例
clazz = defineClass(name, classData, 0, classData.length);
return clazz;
}
/**
* 根据名字, 获取Class文件的字节码数据
* @param name
* @return
*/
private byte[] getClassData(String name) {
InputStream is = null;
try {
// http://me.luhzuo/me.luzhuo.classloader.MyClassLoader.class => http://me.luhzuo/me/luzhuo/classloader/MyClassLoader.class
String classNetPath = classUrl + "/" + name.replace(".", "/") + ".class";
is = new URL(classNetPath).openStream(); // TODO 如果这个类
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buff = new byte[1024];
int len = -1;
while((len = is.read(buff)) != -1) {
baos.write(buff, 0 ,len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
MyClass.java: 存在于网络上的class文件
public class MyClass {
/**
* 获取简短类名
* @return
*/
public String getName(){
return MyClass.class.getSimpleName();
}
}
Test.java: 测试文件
public class Test{
public static void main(String[] args) throws ClassNotFoundException, Exception {
// 测试自定义类加载器
Class<?> clazz = testMyClassLoader("http://luzhuo.me/code/ClassLoader/bin", "me.luzhuo.MyClass");
// 调用方法
Method getName = clazz.getMethod("getName");
String name = (String) getName.invoke(clazz.newInstance());
System.out.println("TestMyClassLoader - getName: " + name);
System.out.println("TestMyClassLoader - classLoader: " + clazz.getClassLoader());
}
/**
* 测试自定义类加载器
* @param url
* @param className
* @return
* @throws ClassNotFoundException
*/
private static Class<?> testMyClassLoader(String url, String className) throws ClassNotFoundException{
MyClassLoader classLoader = new MyClassLoader(url);
Class<?> clazz = classLoader.loadClass(className);
return clazz;
}
}
ClassLoader在Android中的运用
Java的 ClassLoader 不能直接在Android中使用, 因为Android用的是.dex文件, 是将java代码重新整合后的文件; 因此在Android中分别有两个主要的类 DexClassLoader 和 PathClassLoader, 他们都继承ClassLoader是对其的拓展.
DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex
PathClassLoader 只可以加载 系统中已安装的apk
使用
完整代码
Plugin.java: 插件类
public class Plugin {
/**
* 普通方法
*/
public String getName(){
return "Name is " + Plugin.class.getSimpleName();
}
}
创建一个插件工程, 并创建一个插件类(me.luzhuo.testapk.Plugin
), 然后在资源目录mipmap下放上一张图片 testapk_bg.png
.
接下来就需要把该工程生成一个插件apk, 直接点run即可生成apk文件.
DexClassLoaderTest.java: DexClassLoader 的使用代码
/**
* DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex
*/
public class DexClassLoaderTest {
/**
* 从apk文件中加载Class字节码对象
* @param context
* @param apkPath apk文件路径
* @param libPath .so文件路径
* @param className 要加载的全类名
* @return 找到的类定义
* @exception FileNotFoundException apk文件不存在
* @exception ClassNotFoundException 没有找到指定的类
*/
@NonNull
public Class<?> dexClassLoder(@NonNull Context context, @NonNull String apkPath, @Nullable String libPath, @NonNull String className) throws ClassNotFoundException, FileNotFoundException {
if (!new File(apkPath).exists()) throw new FileNotFoundException("要查找的文件 " + apkPath + " 没有找到.");
String dexOutputDir = context.getApplicationInfo().dataDir;
// apk文件路径, 解压后.dex存储路径, .os文件路径, 父类加载器
DexClassLoader classLoader = new DexClassLoader(apkPath, dexOutputDir, libPath, this.getClass().getClassLoader());
// 从加载进来的apk中获取指定的类, 并用反射调用其方法
Class<?> clazz = classLoader.loadClass(className); // me.luzhuo.testapk.Plugin
return clazz;
}
}
PathClassLoaderTest.java: PathClassLoader 的使用代码
/**
* PathClassLoader 只可以加载 系统中已安装的apk
*/
public class PathClassLoaderTest {
/**
* 从已安装的apk中获取Class字节码对象
* @param apkPath 系统中安装的apk路径
* @param libPath .os文件路径
* @param className 全类名
* @return 找到的类定义
* @exception FileNotFoundException apk文件不存在
* @exception ClassNotFoundException 没有找到指定的类
*/
@NonNull
public Class<?> pathClassLoader(@NonNull String apkPath, @Nullable String libPath, @NonNull String className) throws FileNotFoundException, ClassNotFoundException {
if (!new File(apkPath).exists()) throw new FileNotFoundException("要查找的文件 " + apkPath + " 没有找到.");
// apk文件路径, .os文件路径, 父类加载器
PathClassLoader classLoader = new PathClassLoader(apkPath, libPath, this.getClass().getClassLoader());
Class<?> clazz = classLoader.loadClass(className);
return clazz;
}
}
从上述代码就可看出, PathClassLoader 的使用只是比 DexClassLoader 的使用上了一个dex文件的输出路径(dexOutputDir)而已, 这个会在后面分析源码时会讲到.
APkUtils.java: getAPKInfoByLocal() 方法用于查找并获取已安装的应用信息
public class APkUtils {
/**
* 从本地获取指定包名的的apk信息
* @param context
* @param packName 包名
* @return 返回String[4], 0: 包名; 1: apk路径; 2: .so文件路径
*/
@NonNull
public static String[] getAPKInfoByLocal(@NonNull Context context, @NonNull final String packName) throws FileNotFoundException {
Intent intent = new Intent(Intent.ACTION_MAIN, null);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
PackageManager pm = context.getPackageManager();
// 从已安装的应用中找出满足 'ACTION_MAIN' 和 'CATEGORY_LAUNCHER' 条件的应用
final List<ResolveInfo> plugins = pm.queryIntentActivities(intent, 0);
ResolveInfo tagResolveInfo = null;
if(plugins != null){
for(int i = 0;i < plugins.size();i++){
ResolveInfo resolveInfo = plugins.get(i);
ActivityInfo activityInfo = resolveInfo.activityInfo;
String packageName = activityInfo.packageName; // 获取应用的包名
if(packageName.equals(packName)){
tagResolveInfo = plugins.get(i);
break;
}
}
}
if(tagResolveInfo == null) throw new FileNotFoundException("要查找应用 " + packName + " 没有找到.");
ActivityInfo activityInfo = tagResolveInfo.activityInfo;
String packageName = activityInfo.packageName;
String apkPath = activityInfo.applicationInfo.sourceDir; // /data/app/me.luzhuo.testapk-1.apk
String dexOutputDir = context.getApplicationInfo().dataDir; // /data/data/me.luzhuo.classloader_android
String libPath = activityInfo.applicationInfo.nativeLibraryDir; // /data/app-lib/me.luzhuo.testapk-1
return new String[]{packageName, apkPath, libPath};
}
}
MainActivity: 测试代码
/**
* ClassLoader在Android中的运用
*
* DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex
* PathClassLoader 只可以加载 系统中已安装的apk
*
* Android程序与Java程序最大的区别在于 上下文环境(Context) 不同,
* 而这个不同也将导致Java在Android中运行会面临几个问题:
* 1. 组件需要注册才能使用
* 2. 程序获取敏感信息需要权限
* 3. 加载进来的代码需要的资源id, 未必在现有的Resource实例中
*/
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
callClassLoader();
}
/**
* 使用的案例代码
*/
private final void callClassLoader(){
try {
// 方式1: 加载外部的apk文件
String apkPath = Environment.getExternalStorageDirectory() + File.separator + "testapk.apk";
String libPath = null;
String className = "me.luzhuo.testapk" + ".Plugin";
// 方式2: 加载已安装的apk文件
String packageName = "me.luzhuo.testapk";
String[] apkinfos = getAPKInfoByLocal(this, packageName); // 0: 包名; 1: apk路径; 2: .so文件路径
apkPath = apkinfos[1];
libPath = apkinfos[2];
className = packageName + ".Plugin";
// 加载apk文件, 获取Class
// 方式1: DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex
DexClassLoaderTest dexClassLoader = new DexClassLoaderTest();
Class<?> clazz = dexClassLoader.dexClassLoder(this, apkPath, libPath, className);
// 方式2: PathClassLoader 只可以加载 系统中已安装的apk
PathClassLoaderTest pathClassLoader = new PathClassLoaderTest();
clazz = pathClassLoader.pathClassLoader(apkPath, libPath, className);
// 调用插件里的方法
Object obj = clazz.newInstance();
Method getName = clazz.getMethod("getName");
String name = (String) getName.invoke(obj);
Log.e(TAG, "Plugin: " + name);
// 获取插件里的资源文件
// 插件的上下文
Context plugContext = this.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
String packageName_mipmap = packageName + ".R$mipmap";
Class<?> resDrawable = pathClassLoader.pathClassLoader(apkPath, null, packageName_mipmap); // pathClassLoader
resDrawable = dexClassLoader.dexClassLoder(this, apkPath, libPath, packageName_mipmap); // dexClassLoader
for (Field resField : resDrawable.getDeclaredFields()){
// 查找指定图片名的字段
String resName = resField.getName();
if (!resName.equals("testapk_bg")) continue;
int resId = resField.getInt(R.mipmap.class); // 获取该资源的id值
iv.setImageDrawable(plugContext.getResources().getDrawable(resId)); // 设置图片
Log.e(TAG, "resId: " + id);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上就是完整的代码了.
代码解析
在获取apk文件的方式分为两种: 第一种是从sd卡等外部存储设备上获取; 第二种则是从已安装的内部存储获取
第一种, 从外部文件获取, DexClassLoader 可以使用, PathClassLoader 不可以使用
String apkPath = Environment.getExternalStorageDirectory() + File.separator + "testapk.apk"; String libPath = null; String className = "me.luzhuo.testapk" + ".Plugin";
第二种, 从已安装在手机的内部存储区获取, 主要是把手机里已安装的包都遍历一遍 DexClassLoader 可以使用, PathClassLoader 可以使用
@NonNull public static String[] getAPKInfoByLocal(@NonNull Context context, @NonNull final String packName) throws FileNotFoundException { Intent intent = new Intent(Intent.ACTION_MAIN, null); intent.addCategory(Intent.CATEGORY_LAUNCHER); PackageManager pm = context.getPackageManager(); // 从已安装的应用中找出满足 'ACTION_MAIN' 和 'CATEGORY_LAUNCHER' 条件的应用 final List<ResolveInfo> plugins = pm.queryIntentActivities(intent, 0); ResolveInfo tagResolveInfo = null; if(plugins != null){ for(int i = 0;i < plugins.size();i++){ ResolveInfo resolveInfo = plugins.get(i); ActivityInfo activityInfo = resolveInfo.activityInfo; String packageName = activityInfo.packageName; // 获取应用的包名 if(packageName.equals(packName)){ tagResolveInfo = plugins.get(i); break; } } } if(tagResolveInfo == null) throw new FileNotFoundException("要查找应用 " + packName + " 没有找到."); ActivityInfo activityInfo = tagResolveInfo.activityInfo; String packageName = activityInfo.packageName; String apkPath = activityInfo.applicationInfo.sourceDir; // /data/app/me.luzhuo.testapk-1.apk String dexOutputDir = context.getApplicationInfo().dataDir; // /data/data/me.luzhuo.classloader_android String libPath = activityInfo.applicationInfo.nativeLibraryDir; // /data/app-lib/me.luzhuo.testapk-1 return new String[]{packageName, apkPath, libPath}; }
在 DexClassLoader 和 PathClassLoader 的使用上, 除了需要主要apk文件的来源外, 主要是少个一个dex解压路径, 其他没有区别
DexClassLoader
// apk文件路径, 解压后.dex存储路径, .so文件路径, 父类加载器 DexClassLoader classLoader = new DexClassLoader(apkPath, dexOutputDir, libPath, this.getClass().getClassLoader()); Class<?> clazz = classLoader.loadClass(className);
PathClassLoader
// apk文件路径, .so文件路径, 父类加载器 PathClassLoader classLoader = new PathClassLoader(apkPath, libPath, this.getClass().getClassLoader()); Class<?> clazz = classLoader.loadClass(className);
调用插件里的方法
// 调用插件里的方法 Object obj = clazz.newInstance(); Method getName = clazz.getMethod("getName"); String name = (String) getName.invoke(obj); Log.e(TAG, "Plugin: " + name);
获取插件里的资源文件
// 获取插件里的资源文件 // 插件的上下文 Context plugContext = this.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE); String packageName_mipmap = packageName + ".R$mipmap"; Class<?> resDrawable = pathClassLoader.pathClassLoader(apkPath, null, packageName_mipmap); // pathClassLoader resDrawable = dexClassLoader.dexClassLoder(this, apkPath, libPath, packageName_mipmap); // dexClassLoader for (Field resField : resDrawable.getDeclaredFields()){ // 查找指定图片名的字段 String resName = resField.getName(); if (!resName.equals("testapk_bg")) continue; int resId = resField.getInt(R.mipmap.class); // 获取该资源的id值 iv.setImageDrawable(plugContext.getResources().getDrawable(resId)); // 设置图片 Log.e(TAG, "resId: " + id); }