Android ClassLoader介绍

文章目录

Android ClassLoader介绍

什么是class loader,对于Android来说,class loader主要做两件事情

  • 加载dex文件
  • 根据class path加载并返回对应的Class<?>对象

BaseDexClassLoader

Android类加载的基础实现类其实是BaseDexClassLoader,看其构造函数

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
  • dexPath 要加载的dex文件列表,多个文件路径用 : 分割
  • optimizedDirectory 存放dex优化后的odex文件的目录
  • libraryPath 库路径,多个也是用 :分割,如果app没有native library,则为null
  • parent 父class loader,用于双亲委派(parent delegation)

在构造函数内,基于如上参数创建DexPathList,DexPathList内部主要做了:

  • 调用makeDexElements,根据上面提供的dex文件列表依次创建DexFile和Element,并保存到数组

BaseDexClassLoader创建完成后,就可以调用loadClass按如下顺序加载对应的class了

  • 如果parent delegation不为null,尝试调用parent.loadClass(name, false)获取
  • 接着调用self.findClass
  • 继续调用this.pathList.findClass, pathList会依次从dexElements获取DexFile来尝试获取class,如果成功就返回

PathClassLoader和DexClassLoader

这两个类都派生子BaseDexClassLoader,有什么区别?看构造函数就知道了

   //PathClassLoader constructor
   public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    //DexClassLoader constructor
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }

二者主要区别是,而DexClassLoader可以设置optimizedDirectory,而PathClassLoader不能,必须使用默认的,也就是系统默认路径/data/dalvik-cache

所以,DexClassLoader能加载并optimize任意的dex文件,PathClassLoader则用于加载系统和已安装app的dex文件

为什么要支持MultiDex?

看了上面的介绍,我们知道android class loader默认是支持从多个dexes文件列表中依次尝试加载class类的,为什么要支持multi dex加载呢?

这个是历史遗留问题

Unable to execute dex: method ID not in [0, 0xffff]: 65536
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

Android VM在加载并解析dex文件后,在执行method时所需的某个变量(具体我也不清楚)类型为short,也就是16bit, 这就限制了dex包含的method数量为65536,超过就不行了(是不是觉得好坑?你只能说近十来年,硬件的进化远远超出了当时设计者的想象)

随着app功能越来越多,生成的dex method总数终归是要超过64k的,所以,为了解决这个限制,必须要将生成的dex做拆分,这就是class loader支持MultiDex的原因

MultiDex打包

Dex打包拆分目前通用的解决方案是使用官方的multidex support library

defaultConfig {
    ...
    // Enabling multidex support.
    multiDexEnabled true
}   

添加依赖

implementation com.android.support:multidex:1.0.2

将multiDexEnabled开关打开,这样在编译打包时,如果生成的dex method数量超过65536,会自动将其拆分,然后打包到apk的根目录

  • classes.dex
  • classes1.dex
  • classes(1…N).dex

其中classes.dex是main dex,其他的是secondary dexes,很明显,classes.dex会作为入口dex被加载,这里又产生一个疑问,在编译时,如何确定哪些类会被打入classes.dex?

在Android sdk build tool目录下,会存在mainDexClasses脚本,用于配置哪些类需要被打入classes.dex

下面是build-tools/22.0.1/mainDexClasses.rules的内容

  -keep public class * extends android.app.Instrumentation {
    <init>();
  }
  -keep public class * extends android.app.Application {
    <init>();
    void attachBaseContext(android.content.Context);
  }
  -keep public class * extends android.app.Activity {
    <init>();
  }
  -keep public class * extends android.app.Service {
    <init>();
  }
  -keep public class * extends android.content.ContentProvider {
   <init>();
  }
  -keep public class * extends android.content.BroadcastReceiver {
   <init>();
  }
  -keep public class * extends android.app.backup.BackupAgent {
   <init>();
  }
# We need to keep all annotation classes because proguard does not trace annotation attribute
# it just filter the annotation attributes according to annotation classes it already kept.
  -keep public class * extends java.lang.annotation.Annotation {
   *;
  }

很明显,四大组件和Application等都被打入了main dex

安装加载

在5.0系统之后,Android系统在安装apk时,就会对MultiDex做合成并放置到

/data/dalvik-cache/apk***@classes.dex

这样App启动时就不需要再做MultiDex装载,这在一定程度上会提高App的启动速度

不过在5.0系统以前,如果apk包含多dex,在安装完成后dex分布如下

  • classes.dex main dex文件放置于/data/dalvik-cache/apk***@classes.dex
  • secondary dexes会被放置于 /data/data/package/code_cache/secondary-dexes/下

然后在apk启动时,通过在调用MultiDex.install(this)来完成对secondary dexes的加载

public class HelloMultiDexApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

在MultiDex.install(this)成功之前,任何对secondary-dexes内类的访问,都会触发class not found exception

接着简单看下MultiDex.install(this)实现原理

    public static void install(Context context) {
        Log.i("MultiDex", "Installing application");
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
        } else if (VERSION.SDK_INT < 4) {
            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
        } else {
            try {
                ApplicationInfo applicationInfo = getApplicationInfo(context);
                if (applicationInfo == null) {
                    Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
                    return;
                }

                doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "");
            } catch (Exception var2) {
                Log.e("MultiDex", "MultiDex installation failure", var2);
                throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
            }

            Log.i("MultiDex", "install done");
        }
    }

doInstallation主要调用getDexDir拿到/data/data/package/code_cache目录,接着调用installSecondaryDexes安装该目录下的所有dex,原理也很简单,最终就是通过反射拿到当前class loader的pathList(DexPathList),接着调用其静态函数makeDexElements根据如上dex dir目录下dex list创建Element数组,最后将其追加到pathList已有Element数组

    private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field jlrField = findField(instance, fieldName);
        Object[] original = (Object[])((Object[])jlrField.get(instance));
        Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
        System.arraycopy(original, 0, combined, 0, original.length);
        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
        jlrField.set(instance, combined);
    }

参考文档

Android拆分与加载Dex的多种方案对比

預防 Android Dex 64k Method Size Limit

上一篇:java学习笔记(五)Defining Classes II


下一篇:Django的ORM查询