介绍
插件化技术可以说是Android高级工程师所必须具备的技能之一。学习这项技术是关心背后技术实现的原理,但是在项目中能不用就不用,因为插件化的做法Google本身是不推荐的。
插件化技术最初是源于免安装运行apk的想法,这个免安装的apk我们称之为插件,而支持插件的APP我们称为宿主。所以插件化开发就是将整个APP拆分成很多模块,这些模块包括一个宿主和多个插件,每个模块都是一个apk,最终发版的时候可以只发布宿主apk,插件apk在用户需要相应模块的功能的时候,才去从服务器上获取并且加载。
那么插件化能解决什么问题呢?
- APP的功能模块越来越多,体积越来越大,通过插件化可以减少主包的大小。
- 不发布版本上新功能。
- 模块之间耦合度高,协同开发沟通成本越来越大。
- 方法数目超过65535,APP占用内存比较大
插件化实现的过程需要思考如下几个问题:
- 如何加载插件的类?
- 如何加载插件的资源?
- 如何调用插件类?
类加载器
Java和Android中的类加载器都是ClassLoader,Android中的ClassLoader的关系如下:
我们可以写一个demo打印一下ClassLoader的关系:
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());
}
打印结果如下:
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,系统类Activity.class对象的ClassLoader也是BootClassLoader。
我们加载一个类的实现如下:
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.
*
* This class loader requires an application-private, writable directory to
* cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
* such a directory:
{@code
* File dexOutputDir = context.getCodeCacheDir();
* }
*
* Do not cache optimized classes on external storage.
* 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.
*
*
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的类加载器,这个类可以用来执行一个没有安装的应用的代码即插件中的代码。
/**
* 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:
*
*
* - JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
*
- Raw ".dex" files (not inside a zip file).
*
*
* 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来执行系统本地文件的文件列表或者目录,但是不能从网络加载类。
Android使用这个类作为系统的类加载器,并且作为应用的类加载器。
从上边两个类我们可以看出两者区别是:
PathClassLoader是作为应用或者系统使用的类加载器,而DexClassLoader可以用来加载未安装apk的classes.dex.
DexClassLoader在构造方法内创建了一个存储优化dex的目录,而PathClassLoader没有。
我们看一下他们的父类BaseDexClassLoader的构造方法:
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());
}
}
}
这里注意看参数optimizedDirectory的注释:这个参数已经废弃,并且无效了。
方法体中的new DexPathList的时候第四个参数直接传递null,这个参数也是优化目录optimizedDirectory。
所以在Android8.0中PathClassLoader和DexClassLoader无本质区别,但是使用的时候还是按照官方注释使用吧。加载插件apk的时候使用DexClassLoader,PathClassLoader是系统使用的。
通过源码可以知道加载类流程不在PathClassLoader和DexClassLoader中,在BaseDexClassLoader 也没有找到loadClass方法,根据类的继承关系向上查找父类是CalssLoader,在CalssLoader中查看loadClass的实现如下:
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;
}
第一步检查这个类是不是已经加载过了,加载过就直接返回,否则调用parent的loadClass,前边的分析我们知道了PathClassLoader的parent是BootClassLoader,我们看一下BootClassLoader的实现:
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,就调用findClass,findClass中调用反射Class.classForName加载类。
在ClassLoader的loadClass我们看到如果parent也没有找到类,就调用子类本身的findClass方法。
以上流程就是我们常说的类加载的双亲委托机制。整个加载类的流程图如下:
这是大概的流程,那么具体的加载类是怎么实现的呢?
加载类流程
PathClassLoader和DexClassLoader里边只有构造方法,所以真正的findClass是在BaseDexClassLoader中实现的。
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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;
}
这里又调用的是 pathList.findClass的方法, 前边的分析得知pathList是在构造函数中创建的。我们继续往下看pathList.findClass的实现
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 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 files, File optimizedDirectory,
List 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 主要是从数组dexElements数组的Element中findClass查找类,dexElements是在构造的时候根据接收到的path调用makeDexElements创建的,makeDexElements根据传进来的path扫描目录下的所有dex文件,由于optimizedDirectory是null,所以DexFile是通过new创建的,然后通过new Element(dex, null)创建Element对象。
至此就是ClassLoader加载类的过程,那么我们实现加载插件类就可以从这里为突破口。实现的过程大致如下:
1.创建插件的DexClassLoader,通过反射获取插件的dexElements值。
2.获取宿主的PathClassLoader,通过反射获取宿主的dexElements值。
3.合并插件的dexElements和宿主的dexElements,生成新的Element[]值。
4.通过反射将新的Element[]设置给宿主dexElements。
实现加载插件的类
1.准备
创建一个插件的app,插件类中有一个类Test如下:
public class Test {
public static void test(){
Log.i("jawe", "test: 我是插件中的方法");
}
}
2.宿主app的module中创建一个工具类LoadUtils实现加载插件目录下的所有dex包,然后实现加载插件类的过程。
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设置给宿主的dexElements
dexElementsField.set(hostPathList, elements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注释很详细这里不在详述。
3.宿主加载插件的时机是越早越好,一个app最先调用的是Application的attachBaseContext方法。所以我们要在宿主中自定义一个Application,然后在attachBaseContext中调用LoadUtils.loadPlugin(this);
MainActivity调用插件中的方法如下:
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();
}
}
});
}
}
至此就可以加载插件类和调用插件中类的方法了。
总结
通过这一节的学习我们知道了什么是双亲委托机制?类加载器的工作原理,Java的反射使用等等知识点。