Android插件化(一)-如何加載插件的類

介紹

插件化技術(shù)可以說是Android高級工程師所必須具備的技能之一。學(xué)習(xí)這項技術(shù)是關(guān)心背后技術(shù)實現(xiàn)的原理其监,但是在項目中能不用就不用,因為插件化的做法Google本身是不推薦的介衔。

插件化技術(shù)最初是源于免安裝運行apk的想法壳鹤,這個免安裝的apk我們稱之為插件,而支持插件的APP我們稱為宿主斩启。所以插件化開發(fā)就是將整個APP拆分成很多模塊序调,這些模塊包括一個宿主和多個插件,每個模塊都是一個apk兔簇,最終發(fā)版的時候可以只發(fā)布宿主apk发绢,插件apk在用戶需要相應(yīng)模塊的功能的時候,才去從服務(wù)器上獲取并且加載垄琐。

那么插件化能解決什么問題呢边酒?

  • APP的功能模塊越來越多,體積越來越大狸窘,通過插件化可以減少主包的大小墩朦。
  • 不發(fā)布版本上新功能。
  • 模塊之間耦合度高翻擒,協(xié)同開發(fā)溝通成本越來越大氓涣。
  • 方法數(shù)目超過65535,APP占用內(nèi)存比較大

插件化實現(xiàn)的過程需要思考如下幾個問題:

  • 如何加載插件的類陋气?
  • 如何加載插件的資源劳吠?
  • 如何調(diào)用插件類?

類加載器

Java和Android中的類加載器都是ClassLoader巩趁,Android中的ClassLoader的關(guān)系如下:

ClassLoader類圖.png

我們可以寫一個demo打印一下ClassLoader的關(guān)系:

    private void printClassLoader(){
        ClassLoader classLoader = getClassLoader();
        while (classLoader != null) {
            Log.i("jawe", "printClassLoader: classLoader="+classLoader);
            classLoader = classLoader.getParent();
        }

        Log.d("jawe", "printClassLoader: classLoader="+ Activity.class.getClassLoader());
    }

打印結(jié)果如下:

2019-12-10 13:29:48.498 25138-25138/? I/jawe: printClassLoader: classLoader=dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.studio.busdemo-1_HIoy4YiVYjhXH04u_SeQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.studio.busdemo-1_HIoy4YiVYjhXH04u_SeQ==/lib/arm64, /system/lib64, /vendor/lib64, /product/lib64]]]
2019-12-10 13:29:48.498 25138-25138/? I/jawe: printClassLoader: classLoader=java.lang.BootClassLoader@8a457f1
2019-12-10 13:29:48.498 25138-25138/? D/jawe: printClassLoader: classLoader=java.lang.BootClassLoader@8a457f1

由此可見我們自己對象的ClassLoader是PathClassLoader痒玩,PathClassLoader對象的parent是BootClassLoader,系統(tǒng)類Activity.class對象的ClassLoader也是BootClassLoader议慰。

我們加載一個類的實現(xiàn)如下:

DexClassLoader classLoader = new DexClassLoader(appPath, context.getCacheFile().getAbsolutePath,null,comtext.getClassLoader);

classLoader.loadClass("com.jawe.test.Test");

通過這段代碼我們看一下ClassLoader加載類的原理蠢古,這里使用8.0的源碼查看。

/**
 * A class loader that loads classes from {@code .jar} and {@code .apk} files
 * containing a {@code classes.dex} entry. This can be used to execute code not
 * installed as part of an application.
 *
 * <p>This class loader requires an application-private, writable directory to
 * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
 * such a directory: <pre>   {@code
 *   File dexOutputDir = context.getCodeCacheDir();
 * }</pre>
 *
 * <p><strong>Do not cache optimized classes on external storage.</strong>
 * External storage does not provide access controls necessary to protect your
 * application from code injection attacks.
 */
public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     *
     * <p>The path lists are separated using the character specified by the
     * {@code path.separator} system property, which defaults to {@code :}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     *     resources, delimited by {@code File.pathSeparator}, which
     *     defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     *     should be written; must not be {@code null}
     * @param librarySearchPath the list of directories containing native
     *     libraries, delimited by {@code File.pathSeparator}; may be
     *     {@code null}
     * @param parent the parent class loader
     */
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

這里翻譯一下類的注釋文檔:
一個從含有classes.dex實體的.jar或者.apk包中加載class的類加載器别凹,這個類可以用來執(zhí)行一個沒有安裝的應(yīng)用的代碼即插件中的代碼草讶。


/**
 * Provides a simple {@link 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).
 */
public class PathClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code PathClassLoader} that operates on a given list of files
     * and directories. This method is equivalent to calling
     * {@link #PathClassLoader(String, String, ClassLoader)} with a
     * {@code null} value for the second argument (see description there).
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
     * well as arbitrary resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * The entries of the second list should be directories containing
     * native library files.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param librarySearchPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

PathClassLoader的注釋是:
提供一個簡單的ClassLoader來執(zhí)行系統(tǒng)本地文件的文件列表或者目錄,但是不能從網(wǎng)絡(luò)加載類番川。
Android使用這個類作為系統(tǒng)的類加載器到涂,并且作為應(yīng)用的類加載器脊框。

從上邊兩個類我們可以看出兩者區(qū)別是:
PathClassLoader是作為應(yīng)用或者系統(tǒng)使用的類加載器,而DexClassLoader可以用來加載未安裝apk的classes.dex.
DexClassLoader在構(gòu)造方法內(nèi)創(chuàng)建了一個存儲優(yōu)化dex的目錄践啄,而PathClassLoader沒有浇雹。

我們看一下他們的父類BaseDexClassLoader的構(gòu)造方法:

public class BaseDexClassLoader extends ClassLoader {
  ......
    /**
     * Constructs an instance.
     * Note that all the *.jar and *.apk files from {@code dexPath} might be
     * first extracted in-memory before the code is loaded. This can be avoided
     * by passing raw dex files (*.dex) in the {@code dexPath}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android.
     * @param optimizedDirectory this parameter is deprecated and has no effect
     * @param librarySearchPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    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());
        }
    }
}

這里注意看參數(shù)optimizedDirectory的注釋:這個參數(shù)已經(jīng)廢棄,并且無效了屿讽。
方法體中的new DexPathList的時候第四個參數(shù)直接傳遞null昭灵,這個參數(shù)也是優(yōu)化目錄optimizedDirectory。
所以在Android8.0中PathClassLoader和DexClassLoader無本質(zhì)區(qū)別伐谈,但是使用的時候還是按照官方注釋使用吧烂完。加載插件apk的時候使用DexClassLoader,PathClassLoader是系統(tǒng)使用的诵棵。

通過源碼可以知道加載類流程不在PathClassLoader和DexClassLoader中抠蚣,在BaseDexClassLoader 也沒有找到loadClass方法,根據(jù)類的繼承關(guān)系向上查找父類是CalssLoader履澳,在CalssLoader中查看loadClass的實現(xiàn)如下:

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


 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) {//1沒有找到類
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

第一步檢查這個類是不是已經(jīng)加載過了嘶窄,加載過就直接返回,否則調(diào)用parent的loadClass距贷,前邊的分析我們知道了PathClassLoader的parent是BootClassLoader柄冲,我們看一下BootClassLoader的實現(xiàn):

class BootClassLoader extends ClassLoader {

    ......
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }

    ......
    @Override
    protected Class<?> loadClass(String className, boolean resolve)
           throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            clazz = findClass(className);
        }

        return clazz;
    }

   ......
}

BootClassLoader的findLoadedClass中沒有找到clazz,就調(diào)用findClass忠蝗,findClass中調(diào)用反射Class.classForName加載類现横。
在ClassLoader的loadClass我們看到如果parent也沒有找到類,就調(diào)用子類本身的findClass方法阁最。
以上流程就是我們常說的類加載的雙親委托機制戒祠。整個加載類的流程圖如下:


雙親委托機制.png

這是大概的流程,那么具體的加載類是怎么實現(xiàn)的呢闽撤?

加載類流程

PathClassLoader和DexClassLoader里邊只有構(gòu)造方法得哆,所以真正的findClass是在BaseDexClassLoader中實現(xiàn)的。

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

這里又調(diào)用的是 pathList.findClass的方法哟旗, 前邊的分析得知pathList是在構(gòu)造函數(shù)中創(chuàng)建的。我們繼續(xù)往下看pathList.findClass的實現(xiàn)

final class DexPathList {
  private Element[] dexElements;
...
   public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {

         //安全校驗
        ......      
     
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
        
        ......
        //加載native庫
        ......
      }


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

      private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
        Element[] elements = new Element[files.size()];
        int elementsPos = 0;
        /*
         * Open all files and load the (direct or contained) dex files up front.
         */
        for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  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 {
                  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;
    }

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

}

DexPathList #findClass 主要是從數(shù)組dexElements數(shù)組的Element中findClass查找類栋操,dexElements是在構(gòu)造的時候根據(jù)接收到的path調(diào)用makeDexElements創(chuàng)建的闸餐,makeDexElements根據(jù)傳進來的path掃描目錄下的所有dex文件,由于optimizedDirectory是null矾芙,所以DexFile是通過new創(chuàng)建的舍沙,然后通過new Element(dex, null)創(chuàng)建Element對象。

至此就是ClassLoader加載類的過程剔宪,那么我們實現(xiàn)加載插件類就可以從這里為突破口拂铡。實現(xiàn)的過程大致如下:
1.創(chuàng)建插件的DexClassLoader壹无,通過反射獲取插件的dexElements值。
2.獲取宿主的PathClassLoader感帅,通過反射獲取宿主的dexElements值斗锭。
3.合并插件的dexElements和宿主的dexElements,生成新的Element[]值失球。
4.通過反射將新的Element[]設(shè)置給宿主dexElements岖是。

實現(xiàn)加載插件的類

1.準備
創(chuàng)建一個插件的app,插件類中有一個類Test如下:

public class Test {
    public static void test(){
        Log.i("jawe", "test: 我是插件中的方法");
    }
}

2.宿主app的module中創(chuàng)建一個工具類LoadUtils實現(xiàn)加載插件目錄下的所有dex包实苞,然后實現(xiàn)加載插件類的過程豺撑。

public class LoadUtils {
    public static final String pluginApkPath = "/sdcard/plugin-debug.apk";

    public static void loadPlugin(Context context){

        try {
            //1.宿主的elements
            Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);//允許訪問私有屬性
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Object hostPathList = pathListField.get(pathClassLoader);
            Field dexElementsField = hostPathList.getClass().getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object[] hostElements = (Object[]) dexElementsField.get(hostPathList);

            //2.插件的elements
            DexClassLoader dexClassLoader = new DexClassLoader(pluginApkPath, context.getCacheDir().getAbsolutePath(), 
                    null, pathClassLoader);
            Object pluginPathList = pathListField.get(dexClassLoader);
            Object[] pluginElements = (Object[]) dexElementsField.get(pluginPathList);

            //3.合并elements
            Object[] elements = (Object[]) Array.newInstance(hostElements.getClass().getComponentType(), 
                    hostElements.length+pluginElements.length);
            System.arraycopy(hostElements, 0, elements,0, hostElements.length);
            System.arraycopy(pluginElements, 0, elements, hostElements.length, pluginElements.length);

            //4.將新的elements設(shè)置給宿主的dexElements
            dexElementsField.set(hostPathList, elements);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注釋很詳細這里不在詳述。
3.宿主加載插件的時機是越早越好黔牵,一個app最先調(diào)用的是Application的attachBaseContext方法聪轿。所以我們要在宿主中自定義一個Application,然后在attachBaseContext中調(diào)用LoadUtils.loadPlugin(this);

MainActivity調(diào)用插件中的方法如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.loadTv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Class<?> clazz = Class.forName("com.jawe.plugin.Test");
                    Method testMethod = clazz.getMethod("test");
                    testMethod.invoke(null);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

至此就可以加載插件類和調(diào)用插件中類的方法了猾浦。

總結(jié)

通過這一節(jié)的學(xué)習(xí)我們知道了什么是雙親委托機制陆错?類加載器的工作原理,Java的反射使用等等知識點跃巡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末危号,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子素邪,更是在濱河造成了極大的恐慌外莲,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兔朦,死亡現(xiàn)場離奇詭異偷线,居然都是意外死亡,警方通過查閱死者的電腦和手機沽甥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門声邦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人摆舟,你說我怎么就攤上這事亥曹。” “怎么了恨诱?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵媳瞪,是天一觀的道長。 經(jīng)常有香客問我照宝,道長蛇受,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任厕鹃,我火速辦了婚禮兢仰,結(jié)果婚禮上乍丈,老公的妹妹穿的比我還像新娘。我一直安慰自己把将,他們只是感情好轻专,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秸弛,像睡著了一般铭若。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上递览,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天叼屠,我揣著相機與錄音,去河邊找鬼绞铃。 笑死镜雨,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的儿捧。 我是一名探鬼主播荚坞,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼菲盾!你這毒婦竟也來了颓影?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤懒鉴,失蹤者是張志新(化名)和其女友劉穎诡挂,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體临谱,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡璃俗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了悉默。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片城豁。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖抄课,靈堂內(nèi)的尸體忽然破棺而出唱星,到底是詐尸還是另有隱情,我是刑警寧澤跟磨,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布魏颓,位于F島的核電站,受9級特大地震影響吱晒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沦童,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一仑濒、第九天 我趴在偏房一處隱蔽的房頂上張望叹话。 院中可真熱鬧,春花似錦墩瞳、人聲如沸驼壶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽热凹。三九已至,卻和暖如春泪电,著一層夾襖步出監(jiān)牢的瞬間般妙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工相速, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碟渺,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓突诬,卻偏偏與公主長得像苫拍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子旺隙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容