Java_ClassLoader(ClassLoader / 自定义ClassLoader / DexClassLoader)

本文详细介绍了Java的ClassLoader原理,包括双亲委托模型,以及在Android中的具体运用。重点讲解了如何自定义ClassLoader,并对比了DexClassLoader和PathClassLoader在加载.dex文件时的区别和应用场景。示例代码展示了如何使用这两种ClassLoader加载和调用插件中的类和资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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();
        }
    }
}

以上就是完整的代码了.

代码解析
  1. 在获取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};
      }
      
  2. 在 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);
      
  3. 调用插件里的方法

    // 调用插件里的方法
    Object obj = clazz.newInstance();
    
    Method getName = clazz.getMethod("getName");
    String name = (String) getName.invoke(obj);
    Log.e(TAG, "Plugin: " + name);
    
  4. 获取插件里的资源文件

    // 获取插件里的资源文件
    // 插件的上下文
    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);
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值