zoukankan      html  css  js  c++  java
  • class, classloder, dex 详解

    class与dex文件

    什么是class文件

    class文件是一种能够被JVM识别,加载并且执行的文件格式。

    class文件的作用

    class文件的作用是记录一个类文件的所有信息。

    例如记住了当前类的引用this、父类super等等。class文件记录的信息往往比java文件多。

    class文件的结构

    • 8位字节的二进制流文件
    • 各个数据紧密排列,无间隙,减少了文件体积,加快加载速度
    • 每个类或者接口单独占据一个class文件,每个类单独管理,没有交叉

    class文件的弊端

    • 内存占用大,不适合于移动端
    • 堆栈的加载模式导致加载速度慢
    • 文件IO操作多,类查找慢

    什么是dex文件

    能够被DVM或者Art虚拟机执行并且加载的文件格式。

    dex文件的作用

    dex文件的作用是记录整个工程(通常是一个Android工程)的所有类文件的信息。

    dex文件的结构

    • 8位字节的二进制流文件
    • 各个数据紧密排列,无间隙,减少了文件体积,加快加载速度
    • 整个工程的类信息都存放在一个dex文件中(不考虑dex分包的情况下)

    class文件与dex文件的比较

    • 本质上都是一样的,都是二进制流文件格式,dex文件是从class文件演变而来的
    • class文件存在冗余信息,dex文件则去掉了冗余,并且整合了整个工程的类信息。

    结构对比图如下:

     

    类加载机制是做热修复以及插件化的很重要的理论基础。

    Java中的ClassLoader回顾

    如下图所示:

     

    ClassLoader的特性

    ClassLoader的主要特性是双亲委托机制,即加载一个类的时候,先判断已经存在的类是否被加载过,如果没有,先去委托父亲去加载。如果父亲都没有加载成功的话,那么最终由自己加载。最终这个类最终没有合适的CLassLoader加载,那么就会抛出异常,相关的ClassLoader源码如下:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 先判断这个类是否已经被加载过
            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
                }
            }
            return c;
    }
    其中CLassLoader的findClass方法是空实现,需要自己继承然后实现:
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

    这种双亲委托机制有两种好处:

    1. 实现类加载的共享功能,提升类加载的效率。

    2. 实现类加载的隔离功能,提升系统的安全性。比如,通过这种方式,系统的String类只能由系统的ClassLoader加载。

    Android中的ClassLoader整体概述

    Android中的ClassLoader的整体架构继承关系如下:

     

    其中,ClassLoader分别为:

    • BootClassLoader:与Java中的Bootstrap ClassLoader类似,主要加载Android Framework中的字节码文件。
    • PathClassLoader:与Java中的App ClassLoader类似,主要加载已经安装到系统中的APK中的字节码文件。
    • DexClassLoader:与Java中的Customer ClassLoader类似,主要加载自定义路径下的APK或者JAR中的字节码文件(Android中主要是指dex文件,即classes.dex)。

    其中,BaseDexClassLoader是PathClassLoader以及DexClassLoader的父类,PathClassLoader以及DexClassLoader的逻辑都在这个父类中实现。

    BootClassLoader和PathClassLoader是一个普通的APP所需要的(定制ROM另外说),最基本的ClassLoader,通过下面的代码可以证明:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        ClassLoader cl = this.getClassLoader();
        Log.e(TAG, "onCreate: " + cl);
        while (cl.getParent() != null) {
            cl = cl.getParent();
            Log.e(TAG, "onCreate: " + cl);
        }
    
    }

    打印结果只有BootClassLoader和PathClassLoader:

    09-06 14:29:01.571 3165-3165/com.nan.loadapkdemo E/MainActivity: onCreate: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.nan.loadapkdemo-1/base.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_dependencies_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_0_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_1_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_2_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_3_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_4_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_5_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_6_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_7_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_8_apk.apk", zip file "/data/app/com.nan.loadapkdemo-1/split_lib_slice_9_apk.apk"],nativeLibraryDirectories=[/data/app/com.nan.loadapkdemo-1/lib/x86, /system/lib, /vendor/lib]]]
    09-06 14:29:01.571 3165-3165/com.nan.loadapkdemo E/MainActivity: onCreate: java.lang.BootClassLoader@b173b34

    上面就分析完了classes.dex文件是如何通过ClassLoader的初始化装载进来的,下面继续分析一个类是如何通过PathClassLoader或者DexClassLoader进行加载的,这时候就需要看父类BaseDexClassLoader的findClass方法了:

    @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;
    }

    这里面看到,实际上就是调用了PathList的findClass方法:

    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
    
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

    这个方法就是遍历初始化创建好的内部Element[]数组里面的DexFile对象,最终是通过DexFile的loadClassBinaryName进行加载的:

    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }

    defineClass的实现如下:

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

    到最后,defineClassNative是一个native方法:

    private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)

    defineClassNative 是通过C/C++实现,这个方法去dex文件中查找指定name的类,并且拼接成Class字节码,返回给Java层。通过native实现是为了提高效率。

    整个加载流程一气呵成,如下图所示:

    实现动态加载

    如下面所示,演示了最简单的动态加载的方案:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            String apkPath = getExternalCacheDir().getAbsolutePath() + "/bundle.apk";
            loadApk(apkPath);
        }
    
        private void loadApk(String apkPath) {
            File optFile = getDir("opt", MODE_PRIVATE);
            // 通过DexClassLoader加载制定的APK文件
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optFile.getAbsolutePath(), null, getClassLoader());
            try {
                // 通过反射去使用对象
                Class clz = dexClassLoader.loadClass("com.loubinfeng.www.boundle.Printer");
                if (clz != null) {
                    Object instance = clz.newInstance();
                    Method method = clz.getMethod("print");
                    method.invoke(instance);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

     

    实现动态加载的难点

    实现动态加载难点比较多:

    • 资源的访问问题
    • 四大组件的生命周期管理问题
    • 插件ClassLoader的管理
    • so库的动态加载等等

    参考文章:

    http://www.jianshu.com/p/37cad7a901b1

    http://www.jianshu.com/p/2eb518941681

  • 相关阅读:
    0302思考并回答一些问题
    0104 自下而上
    1203 有穷自动机的构造
    11 10我的评论
    1029 C语言文法翻译(2)
    1014 对编译程序的心得与总结
    我们小组的编译程序
    0921 词法分析程序
    0909 关于编译原理的思考
    数据库设计之数据库设计三大范式
  • 原文地址:https://www.cnblogs.com/huansky/p/8038950.html
Copyright © 2011-2022 走看看