Android之ClassLoader类加载器(MultiDex、动态加载dex)

本文介绍了Android中的类加载机制,重点关注PathClassLoader、DexClassLoader和BaseDexClassLoader的作用。讨论了解决方法数超出限制的问题,通过MultiDex支持在API 4-20上加载多个dex文件。此外,还提及了动态加载dex的概念,类似QQ空间热修复方案。

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

android中的类加载

先看个简单的打印日志,直接将代码贴到Activity#onCreate()中即可:

protected void onCreate(Bundle savedInstanceState) {
    //...其他省略

    Log.e(TAG, "classLoader -> " + getClassLoader());
    String classPath = System.getProperty("java.class.path", ".");
    String librarySearchPath = System.getProperty("java.library.path", "");
    Log.e(TAG, "classPath -> " + classPath);
    Log.e(TAG, "librarySearchPath -> " + librarySearchPath);
}
//打印日志  
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: classLoader -> dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.tv189.dl-1/base.apk"],nativeLibraryDirectories=[/data/app/com.tv189.dl-1/lib/arm64, /vendor/lib64, /system/lib64]]]
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: classPath -> .
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: librarySearchPath -> /vendor/lib64:/system/lib64

说明:
dalvik.system.PathClassLoader 为android虚拟机类加载器
System.getProperty("java.class.path", ".")查看当前虚拟机中加载类的路径
System.getProperty("java.library.path", "") 查看当前虚拟机中加载的动态链接库的路径
显然这里android-VM中是通过PathClassLoader来加载类的。

android中类加载器的类图如下:
这里写图片描述

涉及的类
  • PathClassLoader.java
    PathClassLoader作用于本地文件系统中的文件列表和目录,而不能从网络加载classes。

  • DexClassLoader.java
    从包含classes.dex的jar/apk文件中加载class,该类可以加载未打包到apk中的代码。所以可以用来动态加载dex等文件中的classes。
    该加载器需要传入一个应用私有的可写入的目录来缓存优化过的dex,如File dexOutputDir = context.getDir("dex", 0);

  • BaseDexClassLoader.java
    在android中使用该类加载系统classes和app中的classes。可以加载的文件类型为包含classes的jar/apk文件,也支持多个文件同时加载,使用File.pathSeparator 把文件路径连接即可并且android中默认为冒号: ,如path/to/a.jar:path/to/b.apk。dex文件实际被加在到private DexPathList pathList,而findClass(name) 也是从pathList 中来寻找已被加载的class的。

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
  • DexPathList.java
    主要是类加载器把dex/resource保存在private final Element[] dexElements; 数组中的,每个Element 对应一个dex文件; 而BaseDexClassLoader#findClass 加载类时会从dexElements 中遍历获得,如果找到了Class就终止遍历。
注意这一段说明

/**
 * A pair of lists of entries, associated with a {@code ClassLoader}.
 * One of the lists is a dex/resource path &mdash; typically referred
 * to as a "class path" &mdash; list, and the other names directories
 * containing native code libraries. Class path entries may be any of:
 * a {@code .jar} or {@code .zip} file containing an optional
 * top-level {@code classes.dex} file as well as arbitrary resources,
 * or a plain {@code .dex} file (with no possibility of associated
 * resources).
 *
 * <p>This class also contains methods to use these lists to look up
 * classes and resources.</p>
 */
/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";

    /** class definition context */
    private final ClassLoader definingContext;

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private final Element[] dexElements;

    /** List of native library directories. */
    private final File[] nativeLibraryDirectories;

解决android中方法数超出问题

由于android系统的bug,一个dex文件中方法数最大值为65536;所以随着应用代码的膨胀必然会导致程序dex中的方法数超过最大值的,要解决这个问题的方案就是把dex进行切分为多个dex,但是android5.0之前dalvik虚拟机系统只加载一个dex,从android5.0开始系统使用art虚拟机已支持加载多个dex文件;google官方也出了补丁(使用multidex加载多个dex)来修复这个bug;所以如果app要适配android5.0以下的系统程序中就要使用MultiDex方案,这个补丁支持api4 ~ api20(android5.0以下)。

涉及的类

主要类在 android.support.multidex包中

  • MultiDex.java
    加载多个dex,主要过程:

    1. 删除旧的secondarydex缓存 clearOldDexDir
    2. 尝试加载已存在的secondarydexes,失败则从新解压apk然后加载secondarydex;
  • MultiDexExtractor .java
    解压apk,并从中提取dexes

动态加载dex

参考QQ空间热修复方案,在app启动时把补丁dex加载到Element[] 数组的前端(参见classloader类图);

测试时可以先修改某个类,然后把这个修改过的类转为jar,再把jar转为dex;
再把修改的内容改回去;
把下面代码放入项目中去加载dex;

import android.content.Context;

import java.lang.reflect.Array;
import java.lang.reflect.Field;

import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;

/**
 * 加载补丁dex文件,并插入系统dex数组中的第一个位置
 * <p>
 * Created by June on 2017/7/14.
 */

public class PatchDexLoader {

    private Context mContext;

    public PatchDexLoader(Context context) {
        this.mContext = context;
    }

    public void load(String path) {
        try {
            // 已加载的dex
            Object dexPathList = getField(BaseDexClassLoader.class, "pathList", mContext.getClassLoader());
            Object dexElements = getField(dexPathList.getClass(), "dexElements", dexPathList);

            // patchdex
            String dexOptDir = mContext.getDir("patchDex_optDir", 0).getAbsolutePath();
            DexClassLoader dcl = new DexClassLoader(path, dexOptDir, null, mContext.getClassLoader());
            Object patchDexPathList = getField(BaseDexClassLoader.class, "pathList", dcl);
            Object patchDexElements = getField(patchDexPathList.getClass(), "dexElements", patchDexPathList);

            // 将patchdex和已加载的dexes数组拼接连接
            Object concatDexElements = concatArray(patchDexElements, dexElements);

            // 重新给dexPathList#dexElements赋值
            setField(dexPathList.getClass(), "dexElements", dexPathList, concatDexElements);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }


    /**
     * @param cls       被访问对象的class
     * @param fieldName 对象的成员变量名
     * @param object    被访问对象
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public Object getField(Class<?> cls, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
        Field field = cls.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }


    /**
     * @param cls       被访问对象的class
     * @param fieldName 对象的成员变量名
     * @param object    被访问对象
     * @param value     赋值给成员变量
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public void setField(Class<?> cls, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = cls.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }


    /**
     * 连接两个数组(指定位置)
     *
     * @param left  连接后在新数组的左侧
     * @param right 连接后在新数组的右侧
     * @return
     */
    public Object concatArray(Object left, Object right) {
        int len1 = Array.getLength(left);
        int len2 = Array.getLength(right);
        int totalLen = len1 + len2;
        Object concatArray = Array.newInstance(left.getClass().getComponentType(), totalLen);
        for(int i = 0; i < len1; i++) {
            Array.set(concatArray, i, Array.get(left, i));
        }
        for(int j = 0; j < len2; j++) {
            Array.set(concatArray, len1 + j, Array.get(right, j));
        }
        return concatArray;
    }
}

refrence

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值