Android类加载器

JVM之类加载机制

理解类加载

Eclipse使用第三方的插件其实就是动态加载Jar包里的Class字节码进行工作。这其实非常好理解,编写程序就是编写类,运行程序也就是运行类(Class字节码)。程序运行在虚拟机上时,虚拟机把需要的Class加载进来才能创建实例对象并工作,而完成这一个加载工作的角色就是ClassLoader。

Android类加载器

BootClassLoader

BootClassLoader实例在Android系统启动的时候被创建,用于加载一些Android系统框架的类,其中就包括APP用到的一些系统类。

PathClassLoader

在应用启动的时候创建PathClassLoader实例,只能加载系统中已经安装过的apk;

Provides a simple ClassLoader
implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s).

这是官方文档对PathClassLoader描述,大概意思是:
PathClassLoader简单实现了ClassLoader,可以操作本地文件系统的文件和目录,但是不能从网络中加载类。Android使用PathClassLoader作为系统类加载器和应用程序类加载器。

** 项目验证代码:(app中的类由PathClassLoader加载)**

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ClassLoader classLoader = getClassLoader();

        if (classLoader != null) {
            Log.i(TAG, " classLoader " + classLoader.toString());
            while (classLoader.getParent() != null) {
                classLoader = classLoader.getParent();
                Log.i(TAG, " classLoader " + classLoader.toString());
            }
        }
    }
}

打印结果为:

classLoader dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.yeoggc.myapplication-2/base.apk"],nativeLibraryDirectories=[/data/app/com.yeoggc.myapplication-2/lib/x86, /vendor/lib, /system/lib]]]
classLoader java.lang.BootClassLoader@1c1f72e

从打印结果来看,我们可以知道:

  • 类MainActivity是由PathClassLoader负责加载的
  • App启动时,Android系统启动时创建的BootClassLoader被传入进来,并且作为PathClassLoader的父加载器。

DexClassLoader

可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;

A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getDir(String, int) to create such a directory:

   File dexOutputDir = context.getDir("dex", 0);

Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection attacks.

这是官方文档对DexClassLoader描述。

对PathClassLoader和DexClassLoader源码分析

// PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

从它们两源码可知:

  • 代码行数少,只是调用父构造函数传入的参数不一样而已,具体逻辑实现都没改变,都是在BaseDexClassLoader。
  • PathClassLoader的optimizedDirectory传入的是null。

接下来查看BaseDexClassLoader代码:

BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
 }
Android类加载器_第1张图片
BaseDexClassLoader构造方法参数官方解释
  • dexPath :指目标类所在的apk或jar文件的路径,比如像这样的:"/data/app/com.yeoggc.myapplication-1/base.apk"。如果要包含多个路径,路径之间必须使用特定的分割符分隔。

dex和odex区别:

其实一个APK是一个程序压缩包,里面有个执行程序包含dex文件,ODEX优化就是把包里面的执行程序提取出来,就变成ODEX文件。因为你提取出来了,系统第一次启动的时候就不用去解压程序压缩包,少了一个解压的过程。这样的话系统启动就加快了。为什么说是第一次呢?是因为DEX版本的也只有第一次会解压执行程序到 /data/dalvik-cache(针对PathClassLoader)或者optimizedDirectory(针对DexClassLoader)目录,之后也是直接读取目录下的的dex文件,所以第二次启动就和正常的差不多了。当然这只是简单的理解,实际生成的ODEX还有一定的优化作用。ClassLoader只能加载内部存储路径中的dex文件,所以这个路径必须为内部路径。

  • optimizedDirectory:类加载器把dexPath路径上的文件,进行ODEX优化到内部存储路径,该路径就是由optimizedDirectory指定的。

  • libraryPath:指目标类中所使用到的C/C++库存放的路径。

  • parent:是指该类加载器的父类加载器。

再看DexPathList代码:

    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        ……
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
    }

     /**
     *
     * @param files 多个dexPath字符串路径的File形式
     * @param optimizedDirectory 优化后路径的File形式
     * @return
     */
    private static Element[] makeDexElements(ArrayList files,
                                             File optimizedDirectory) {
        ArrayList elements = new ArrayList();
        //循环遍历每个dexPath
        for (File file : files) {
            ZipFile zip = null;
            DexFile dex = null;
            String name = file.getName();
            //对dexPath路径下的文件进行判断
            if (name.endsWith(DEX_SUFFIX)) {//文件后缀名为.dex的,直接调用loadDexFile
                dex = loadDexFile(file, optimizedDirectory);
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {//文件后缀名为.apk、.jar、.zip的,先创建一个ZipFile对象
                zip = new ZipFile(file);
            }
            ...
            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }


    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }

    /**
     * Converts a dex/jar file path and an output directory to an
     * output file path for an associated optimized dex file.
     */
    private static String optimizedPathFor(File path,
            File optimizedDirectory) {
        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }
        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }

我们查看DexPathList代码可知道,

  • optimizedDirectory是用来缓存我们需要加载的dex文件的。如果它为null,使用dex文件原有的路径来创建DexFile对象。

我们可以得到如下结论:PathClassLoader和DexClassLoader区别在于是否指定了optimizedDirectory。

加载类的过程

跟Java类加载器加载类类似,都是通过ClassLoader用loadClass方法来加载我们需要的类,但是它们两的代码实现不太一样。

大家去查看Android的Android/sdk/sources/android-25/java/lang/ClassLoader.java和Java的Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/src.zip!/java/lang/ClassLoader.java发现loadClass方法的逻辑一样,都是体现双亲委托机制。值得注意的是loadClass中会执行findBootstrapClassOrNull,Android的ClassLoader返回null的。

下面是Android中ClassLoader的loadClass方法代码

public Class loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

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

从loadClass可以看出,与Java类似,都是使用双亲委托机制来加载类。

加载过程:

  1. 会先查找当前ClassLoader是否加载过此类,有就返回;
  2. 如果没有,查询父ClassLoader是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
  3. 如果整个类加载器体系上的ClassLoader都没有加载过,才由当前ClassLoader加载,整个过程类似循环链表一样。

双亲委托机制的作用:

  1. 共享功能,一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
  2. 隔离功能,保证java/Android核心类库的纯净和安全,防止恶意加载。

使用ClassLoader一些需要注意的问题

我们都知道,我们可以通过动态加载获得新的类,从而升级一些代码逻辑,这里有几个问题要注意一下。

如果你希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader会一直优先使用旧类。

如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的ClassLoader没有树的继承关系的另一个ClassLoader来加载新类,因为ClassLoader只会检查其Parent有没有加载过当前要加载的类,如果两个ClassLoader没有继承关系,那么旧类和新类都能被加载。

不过这样一来又有另一个问题了,在Java中,只有当两个实例的类名、包名以及加载其的ClassLoader都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。

同一个Class = 相同的 ClassName + PackageName + ClassLoader
以上问题在采用动态加载功能的开发中容易出现,请注意。

Android动态加载基础 ClassLoader工作机制
Android动态加载入门 简单加载模式

你可能感兴趣的:(Android类加载器)