ClassLoader淺析(二) —— Android ClassLoader

  • 本篇是基于上一篇ClassLoader(一) —— Java ClassLoader。
  • Android虛擬機(jī)和JVM一樣,運(yùn)行程序時(shí)首先要將對(duì)應(yīng)的類加載到內(nèi)存中。但是和JVM不同的是Android虛擬機(jī)上運(yùn)行的是Dex字節(jié)碼,因此Android的ClassLoader和Java的ClassLoader有一定不同。

Android 類加載

  • Android中的類加載器有

    1. BootClassLoader
    2. URLClassLoader
    3. PathClassLoader
    4. DexClassLoader
    5. BaseDexClassLoader
    6. ClassLoader

    其中BootClassLoader,PathClassLoader和DexClassLoader是重點(diǎn)。

    看看他們之間的繼承關(guān)系:

    Android ClassLoader 繼承.PNG

BootClassLoader

  • BootClassLoader在Android系統(tǒng)啟動(dòng)的時(shí)候就被創(chuàng)建,它用于加載一些Android系統(tǒng)框架的類,包括APP用到的一些系統(tǒng)類。它是ClassLoader中的內(nèi)部類,由Java實(shí)現(xiàn)。這個(gè)內(nèi)部類是包內(nèi)可見(jiàn),所以我們沒(méi)法使用。

URLClassLoader

  • 它繼承自SecureClassLoader,用來(lái)通過(guò)URl路徑從jar文件和文件夾中加載類和資源。由于 dalvik 不能直接識(shí)別jar,所以在 Android 中無(wú)法使用這個(gè)加載器。

PathClassLoader

  • PathClassLoader是用來(lái)加載Android系統(tǒng)類和應(yīng)用的類。

  • 在Dalvik虛擬機(jī)上PathClassLoader只能加載已安裝的apk的dex文件。但在ART虛擬機(jī)上可以加載未安裝的apk的dex文件。

  • PathClassLoader的源碼,只有2個(gè)構(gòu)造方法:

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

    由于都是只調(diào)用了父類BaseDexClassLoader的構(gòu)造方法,所以每個(gè)參數(shù)的含義將會(huì)留到BaseDexClassLoader再分析。

DexClassLoader

  • DexClassLoader可以加載一個(gè)未安裝的APK,也可以加載其它包含dex文件的JAR/ZIP類型的文件,可以從 SD 卡上加載包含 class.dex 的 .jar 和 .apk 文件,這也是插件化和熱修復(fù)的基礎(chǔ),在不需要安裝應(yīng)用的情況下,完成需要使用的 dex 的加載。

  • 上面說(shuō)dalvik不能直接識(shí)別jar,DexClassLoader卻可以加載jar文件,這難道不矛盾嗎?其實(shí)在BaseDexClassLoader里對(duì)".jar",".zip",".apk",".dex"后綴的文件最后都會(huì)生成一個(gè)對(duì)應(yīng)的dex文件,所以最終處理的還是dex文件,而URLClassLoader并沒(méi)有做類似的處理。

  • DexClassLoader的源碼,只有1個(gè)構(gòu)造方法:

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

    由于只是調(diào)用了父類BaseDexClassLoader的構(gòu)造方法,所以每個(gè)參數(shù)的含義將會(huì)留到BaseDexClassLoader再分析。

BaseDexClassLoader

  • PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,其中的主要邏輯都是在BaseDexClassLoader完成的。

  • 先來(lái)填下上文留下的坑,看看BaseDexClassLoader的構(gòu)造方法:

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

    BaseDexClassLoader的構(gòu)造函數(shù)包含四個(gè)參數(shù),分別為:

    1. dexPath:指目標(biāo)類所在的APK或jar文件的路徑,類裝載器將從該路徑中尋找指定的目標(biāo)類,該類必須是APK或jar的全路徑。如果要包含多個(gè)路徑,路徑之間必須使用特定的分割符分隔,分隔符通常為":"。
    2. optimizedDirectory:由于dex文件被包含在APK或者Jar文件中,因此在裝載目標(biāo)類之前需要先從APK或Jar文件中解壓出dex文件,該參數(shù)就是制定解壓出的dex 文件存放的路徑。如果該參數(shù)為null,則設(shè)置默認(rèn)路徑為/data/dalvik-cache 目錄。
    3. libraryPath:指目標(biāo)類中所使用的C/C++庫(kù)存放的路徑,多個(gè)路徑也是以“:”分隔。
    4. parent:父類加載器,遵從雙親委派。
  • 在BaseDexClassLoader中的成員變量private final DexPathList pathList十分重要,ClassLoader中的抽象方法findClass()findResource()、findResources()、findLibrary()均是基于 pathList 來(lái)實(shí)現(xiàn)的(省略了部分源碼):

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
      List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
      Class c = pathList.findClass(name, suppressedExceptions);
      ...
      return c;
    }
    
    @Override
    protected URL findResource(String name) {
      return pathList.findResource(name);
    }
    
    @Override
    protected Enumeration<URL> findResources(String name) {
      return pathList.findResources(name);
    }
    
    @Override
    public String findLibrary(String name) {
      return pathList.findLibrary(name);
    }
    

    那我們來(lái)看看DexPathList中做了什么。

DexPathList

  • 在DexPathList中有個(gè)private Element[] dexElements是它的重點(diǎn),Element是DexPathList的內(nèi)部類,有下面的成員變量:

    static class Element {
            private final File path;
            private final DexFile dexFile;
            private ClassPathURLStreamHandler urlHandler;
            private boolean initialized;
    }
    
  • 讓我們看看Element數(shù)組是如果生成的:

    //在DexPathList構(gòu)造方法中調(diào)用makeDexElements方法生成
    public DexPathList(ClassLoader definingContext, String dexPath,
                String librarySearchPath, File optimizedDirectory) {
        ...
          //splitDexPath()方法是把String切割成多個(gè)地址,再把每個(gè)地址生成File,該方法返回List<File>
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions, definingContext); 
        ...
    }
    
        private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
                List<IOException> suppressedExceptions, ClassLoader loader) {
          Element[] elements = new Element[files.size()];
          int elementsPos = 0;
          //打開(kāi)所有文件并預(yù)先加載(直接或包含)dex文件
          for (File file : files) {
              if (file.isDirectory()) {
                  // 如果是文件夾,則直接添加 Element,這個(gè)一般是用來(lái)處理 native 庫(kù)和資源文件
                  elements[elementsPos++] = new Element(file);
              } else if (file.isFile()) {
                  String name = file.getName();
                  if (name.endsWith(DEX_SUFFIX)) {
                      // 直接是.dex文件,而不是zip/jar文件(apk歸為zip),則直接加載dex文件
                      try {
                          DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                          if (dex != null) {
                              elements[elementsPos++] = new Element(dex, null);
                          }
                      } catch (IOException suppressed) {
                          System.logE("Unable to load dex file: " + file, suppressed);
                          suppressedExceptions.add(suppressed);
                      }
                  } else {
                  //如果是zip/jar文件(apk歸為zip),加載dex文件。
                      DexFile dex = null;
                      try {
                          dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      } catch (IOException suppressed) {
                          suppressedExceptions.add(suppressed);
                      }
                  //如果dex為空則不傳進(jìn)Element,file文件是肯定會(huì)傳進(jìn)的
                      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;
        }
    

    DexPathList.loadDexFile() 方法最終會(huì)調(diào)用 JNI 層的方法來(lái)讀取 dex 文件,這里不再深入探究,有興趣的可以閱讀 從源碼分析 Android dexClassLoader 加載機(jī)制原理 這篇文章深入了解。

  • 獲得了Element數(shù)組就可以通過(guò)DexPathList.findClass()方法來(lái)對(duì)類進(jìn)行加載了,源碼如下:

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        // 遍歷 dexElements  數(shù)組,依次尋找對(duì)應(yīng)的 class,一旦找到就終止遍歷
        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;
    }
    

    ? 這里有關(guān)于熱修復(fù)實(shí)現(xiàn)的一個(gè)點(diǎn),就是將補(bǔ)丁 dex 文件放到 dexElements 數(shù)組前面,這樣在加載 class 時(shí),優(yōu)先找到補(bǔ)丁包中的 dex 文件,加載到 class 之后就不再尋找,從而原來(lái)的 apk 文件中同名的類就不會(huì)再使用,從而達(dá)到修復(fù)的目的。

ClassLoader

  • ClassLoader是所有ClassLoader的最終父類。我們來(lái)瞧瞧ClassLoader的源碼:

    public abstract class ClassLoader {
    
        static private class SystemClassLoader {
            public static ClassLoader loader = ClassLoader.createSystemClassLoader();
        }
    
        //父加載器
        private final ClassLoader parent;
    
        private static ClassLoader createSystemClassLoader() {
            String classPath = System.getProperty("java.class.path", ".");
            String librarySearchPath = System.getProperty("java.library.path", "");
            //可以看出構(gòu)造PathClassLoader傳入了BootClassLoader
            return new PathClassLoader(classPath, librarySearchPath,BootClassLoader.getInstance());
        }
        
         public static ClassLoader getSystemClassLoader() {
            return SystemClassLoader.loader;
        }
    
        private ClassLoader(Void unused, ClassLoader parent) {
            this.parent = parent;
        }
    
        protected ClassLoader(ClassLoader parent) {
            this(checkCreateClassLoader(), parent);
        }
    
        protected ClassLoader() {
            //外界沒(méi)有傳入指定父加載器的情況
            this(checkCreateClassLoader(), getSystemClassLoader());
        }
    
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
        }
    
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException{
                // 檢查是否已經(jīng)加載過(guò)
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    // 沒(méi)有被加載過(guò)
                     // 首先委派給父類加載器加載
                    try {
                        if (parent != null) {
                            //父加載器不為空則調(diào)用父加載器的loadClass
                            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) {
                       // 如果父類加載器無(wú)法加載,才嘗試加載
                        c = findClass(name);
                    }
                }
                return c;
        }
        
        private Class<?> findBootstrapClassOrNull(String name){
            return null;
        }
        ...
    }
    

    ? 從上面可以看出Android中的ClassLoader和Java中的區(qū)別并不大,ClassLoader的構(gòu)造方法也是分為指定parent和不指定parent兩種,不同的是在外界不指定parent的情況下,會(huì)通過(guò)createSystemClassLoader()來(lái)獲取到PathClassLoader作為parent。直白的說(shuō),一個(gè)ClassLoader創(chuàng)建時(shí)如果沒(méi)有指定parent,那么它的parent默認(rèn)就是PathClassLoader,且此PathClassLoader父構(gòu)造器為BootClassLoader。

    ? 可以看到Android中的ClassLoader.loadClass()和Java中的基本是不變的,都是實(shí)現(xiàn)了雙親委托。甚至就連在Java中調(diào)用BootstrapClassLoader的findBootstrapClassOrNull方法也保留著,然而android中并沒(méi)有BootstrapClassLoader,而且并沒(méi)有出現(xiàn)因?yàn)槟硞€(gè)ClassLoader不是Java實(shí)現(xiàn)的而導(dǎo)致無(wú)法持有父加載器的情況。。。所以在這里該方法直接返回nuil。

雙親委派

  • 通過(guò)從ClassLoader.loadClass()方法中我們可以明白Android ClassLoader中的雙親委派流程。

  • Android 雙親委派機(jī)制.jpg
  • 帶上DexClassLoader一起玩雙親委派:

    ? ClassLoader的構(gòu)造方法中有一個(gè)參數(shù)是parent,那么是不是有辦法把PathClassLoader的parent替換成我們想要的DexClassLoader,在把DexClassLoader的parent設(shè)置成BootClassLoader,再加上父委托的機(jī)制,查找類的過(guò)程就變成BootClassLoader->DexClassLoader->PathClassLoader,這樣我們就能夠通過(guò)雙親委派先去加載外部apk的類了。我們可以通過(guò)反射來(lái)實(shí)現(xiàn)我們的設(shè)想。

    public static void loadApk(Context context, String apkPath) {
        File dexFile = context.getDir("dex", Context.MODE_PRIVATE);
        File apkFile = new File(apkPath);
      //獲取到PathClassLoader
        ClassLoader classLoader = context.getClassLoader();
        //創(chuàng)建DexClassLoader并設(shè)置父加載器為BootClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(),
                dexFile.getAbsolutePath(), null, classLoader.getParent());
        try {
            //通過(guò)反射獲取到PathClassLoader的parent成員變量
            Field fieldClassLoader = ClassLoader.class.getDeclaredField("parent");
            if (fieldClassLoader != null) {
                //把parent成員變量賦值為DexClassLoader
                fieldClassLoader.setAccessible(true);
                fieldClassLoader.set(classLoader, dexClassLoader);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    ? 這樣就實(shí)現(xiàn)了DexClassLoader的插入,每次加載app的類之前都會(huì)通過(guò)DexClassLoader指定的位置查找是否有要用來(lái)覆蓋的類。

    ? 新的雙親委派流程圖如下:


    dexclassloader加入雙親委派.png

參考

? Android動(dòng)態(tài)加載之ClassLoader詳解

? 蘋(píng)果核 - Android插件化實(shí)踐(2)--ClassLoader

? 熱修復(fù)入門:Android 中的 ClassLoader

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容