Android ClassLoader源码解析

提起热修复以及插件化,相信大家肯定不陌生,而无论是热修复还是插件化,其理论依据就是Android 类加载机制。今天我们从源码的角度一起学习下。

简单来讲,Android中的ClassLoader主要分为BootClassLoader、PathClassLoader和DexClassLoader这三种类型。BootClassLoader:主要负责加载Android FrameWork层中的字节码文件; PathClassLoader:负责加载已经安装到系统APK文件中的字节码文件;DexClassLoader:负责加载指定目录中的字节码文件;我们先来看下其源码实现:

#BootClassLoader 
class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null);
    }
    ...
}

由上述代码可以看出,BootClassLoader 继承自ClassLoader抽象类,实现方式为单例模式,需要注意的是BootClassLoader的访问修饰符是默认的,只有在同一个包中才可以访问,所以我们在应用程序中是无法直接调用到的。

#PathClassLoader 
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
    }

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

PathClassLoader继承自BaseDexClassLoader ,由上述代码,很显然,PathClassLoader中的方法实现都在其父类BaseDexClassLoader 中,在这里我们分析下PathClassLoader构造方法中各个参数的含义:

dexPath:dex文件以及包含dex的apk文件或jar文件的路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’。
librarySearchPath:所使用到的C/C++库存放的路径
parent:该ClassLoader所对应的父ClassLoader

#DexClassLoader 
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
    }
}

同样,DexClassLoader 也是继承自BaseDexClassLoader ,相比较PathClassLoader而言,DexClassLoader的构造方法中多了一个参数optimizedDirectory,我们看下这个参数的含义:

optimizedDirectory:Android系统将dex文件进行优化后所生成的ODEX文件的存放路径,该路径必须是一个内部存储路径。PathClassLoader中使用默认路径“/data/dalvik-cache”,而DexClassLoader则需要我们指定ODEX优化文件的存放路径。

和Java中的ClassLoader类似,Android中的ClassLoader同样遵循双亲委托机制。上述三种ClassLoader中,PathClassLoader的parent为BootClassLoader,DexClassLoader的parent同样为BootClassLoader,下面我们来验证下:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ClassLoader classLoader = getClassLoader();
        if (classLoader != null){
            Log.e("MainActivity", classLoader.toString());
            while (classLoader.getParent() != null){
                classLoader = classLoader.getParent();
                Log.e("MainActivity", classLoader.toString());
            }
        }
        
        Log.e("MainActivity", "------------------");
        
        TextView mText = findViewById(R.id.tv_text);
        Log.e("MainActivity-TextView", mText.getClass().getClassLoader().toString());
    }
}

输出日志为:

11-24 22:34:44.205 20274-20274/com.example.administrator.mdtest E/MainActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.administrator.mdtest-2/base.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_dependencies_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_0_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_1_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_2_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_3_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_4_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_5_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_6_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_7_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_8_apk.apk", zip file "/data/app/com.example.administrator.mdtest-2/split_lib_slice_9_apk.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
11-24 22:34:44.205 20274-20274/com.example.administrator.mdtest E/MainActivity: java.lang.BootClassLoader@245c18f6
11-24 22:34:44.205 20274-20274/com.example.administrator.mdtest E/MainActivity: ------------------
11-24 22:34:44.205 20274-20274/com.example.administrator.mdtest E/MainActivity-TextView: java.lang.BootClassLoader@245c18f6

由上述输出日志,我们不仅可以验证,PathClassLoader的parent为BootClassLoader,同时还验证了我们文章开始所说的应用程序的ClassLoader为PathClassLoader,FrameWork层的ClassLoader为BootClassLoader。

照例我们打开源码,看下BootClassLoader是在哪里作为parent参与构建PathClassLoader对象的:

#ClassLoader
private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");

        // TODO Make this a java.net.URLClassLoader once we have those?
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }

没错,正是在ClassLoader类中的createSystemClassLoader方法中。

好了,我们既然知道Android的ClassLoader遵循双亲委托机制,那么肯定要看下ClassLoader类中的loadClass方法了:

#ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                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.
                    c = findClass(name);
                }
            }
            return c;
    }

关于双亲委托机制,相信大家都了解,在这里我就不详细介绍了,需要注意的是,在Java中,类加载是通过defineClass方法,而在Android中,类加载则是通过findClass方法,我们跟进去findClass方法看下类加载的过程(由于BaseDexClassLoader对findClass方法进行了重写,所以我们需要跟进到BaseDexClassLoader类中,而Android Studio中无法查看到BaseDexClassLoader的具体源码,所以笔者在这里通过源码在线查看网站:https://www.androidos.net.cn/sourcecode):

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

可以看到,在BaseDexClassLoader的findClass方法中直接调用到pathList的findClass方法进行类加载操作,pathList是个什么东东呢?我们看下它的定义:

    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);

        if (reporter != null) {
            reporter.report(this.pathList.getDexPaths());
        }
    }

由上述代码可以看到,pathList被定义为final类型,其对象是在BaseDexClassLoader的构造方法中创建的,也就是说在PathClassLoader对象创建的时候就创建了DexPathList对象,并将相应参数传入。我们跟进去看下DexPathList的构造方法:

 //定义所要加载文件后缀
 private static final String DEX_SUFFIX = ".dex";
 //构造方法中传入的ClassLoader
 private final ClassLoader definingContext;
 //Element为 DexPathList 中的内部类,其主要的成员变量为 dexFile
 //DexFile:dex文件在安卓虚拟机中的具体实现
 private Element[] dexElements;

 public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {

        异常判断操作...
        //接收classloader对象
        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader  
        //重点,通过 makeDexElements 方法初始化 dexElements数组
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);

        ...
    }

我们跟进去看下makeDexElements方法的实现:

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * 遍历所有的dex文件
       */
      for (File file : files) {
          if (file.isDirectory()) {    //判断file是否为文件夹
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {          //判断file是否为文件
              //获取文件名称
              String name = file.getName();
              //判断文件名称是否以“.dex”结尾
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      //将dex文件转换为DexFile对象
                      DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          //创建Element对象,将DexFile对象作为参数传入,
                          //并将该Element对象添加到elements数组中
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  DexFile dex = null;
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

由上述代码,我们可以知道makeDexElements方法的主要作用为:遍历指定路径下的所有文件,将其中的.dex文件转换成DexFile对象,最终存储到elements数组中。

由上述分析,我们知道类加载操作最终是由pathList的findClass方法来实现的,我们继续跟进去pathList的findClass方法看下:

 #DexPathList
 public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

可以看到, DexPathList 的 findClass方法中简单粗暴,对dexElements数组进行遍历,调用element的findClass方法来寻找当前需要的class字节码,简单来讲就是Android在进行类加载的时候,会遍历我们的每一个dex文件,来寻找所需的Class。
我们接着跟进去element的findClass方法去看下:

#Element
public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

可以看到,最终是调用到 DexFile 的loadClassBinaryName方法,我们接着跟:

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

 ...

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

  ...

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

可以看到最终是通过DexFile类中的defineClassNative方法来完成所需Class的查找。

好了,Android ClassLoader源码解析到这里就结束了,欢迎大家一起交流。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容