Java

xiaoxiao2021-02-28  229

Java_ClassLoader(ClassLoader / 自定义ClassLoader / DexClassLoader)


本文由 Luzhuo 编写,转发请保留该信息. 原文: http://blog.csdn.net/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); }
转载请注明原文地址: https://www.6miu.com/read-60492.html

最新回复(0)