介紹
插件化技術(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)系如下:
我們可以寫一個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方法阁最。
以上流程就是我們常說的類加載的雙親委托機制戒祠。整個加載類的流程圖如下:
這是大概的流程,那么具體的加載類是怎么實現(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的反射使用等等知識點跃巡。