插件化(一)

前言
插件化技术最初源于免安装运行 apk 的想法,这个免安装的 apk 就可以理解为插件,而支持插件的 app 我们一般
叫宿主。宿主可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,一方面减小
了安装包的大小,另一方面可以实现 app 功能的动态扩展。

插件化的实现

我们如何去实现一个插件化呢?
首先我们要知道,插件apk是没有安装的,那我们怎么加载它呢?不知道。。。
没关系,这儿我们还可以细分下,一个 apk 主要就是由代码和资源组成,所以上面的问题我们可以变为:如何加载
插件的类?如何加载插件的资源?这样的话是不是就有眉目了。
然后我们还需要解决类的调用的问题,这个地方主要是四大组件的调用问题。我们都知道,四大组件是需要注册
的,而插件的四大组件显然没有注册,那我们怎么去调用呢?
所以我们接下来就是解决这三个问题,从而实现插件化
1. 如何加载插件的类?
2. 如何加载插件的资源?

 

3. 如何调用插件类?
类加载(ClassLoader)
我们在学 java 的时候知道,java 源码文件编译后会生成一个 class 文件,而在 Android 中,将代码编译后会生成
一个 apk 文件,将 apk 文件解压后就可以看到其中有一个或多个 classes.dex 文件,它就是安卓把所有 class 文件
进行合并,优化后生成的。
java 中 JVM 加载的是 class 文件,而安卓中 DVM 和 ART 加载的是 dex 文件,虽然二者都是用的 ClassLoader 加
载的,但因为加载的文件类型不同,还是有些区别的,所以接下来我们主要介绍安卓的 ClassLoader 是如何加载
dex 文件的。
ClassLoader的实现类
ClassLoader是一个抽象类,实现类主要分为两种类型:系统类加载器和自定义加载器。
其中系统类加载器主要包括三种:
BootClassLoader
用于加载Android Framework层class文件。
PathClassLoader
用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
DexClassLoader
用于加载指定的dex,以及jar、zip、apk中的classes.dex
类继承关系如下图:

我们先来看下 PathClassLoader 和 DexClassLoader。

/ /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
// optimizedDirectory 直接为 null
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
// optimizedDirectory 直接为 null
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
// API 小于等于 26/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
// 26开始,super里面改变了,看下面两个构造方法
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
// API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// DexPathList 的第四个参数是 optimizedDirectory,可以看到这儿为 null
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}
// API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

根据源码了解到,PathClassLoader 和 DexClassLoader 都是继承自 BaseDexClassLoader,且类中只有构造方
法,它们的类加载逻辑完全写在 BaseDexClassLoader 中。
其中我们值的注意的是,在8.0之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是
生成的 odex(优化的dex)存放的路径,PathClassLoader 直接为null,而 DexClassLoader 是使用用户传进来的
路径,而在8.0之后,二者就完全一样了。
下面我们再来了解下 BootClassLoader 和 PathClassLoader 之间的关系。

// 在 onCreate 中执行下面代码
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
Log.e("leo", "classLoader:" + classLoader);
classLoader = classLoader.getParent();
}
Log.e("leo", "classLoader:" + Activity.class.getClassLoader());

打印结果:

classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file
"/data/user/0/com.enjoy.pluginactivity/cache/plugin-debug.apk", zip file
"/data/app/com.enjoy.pluginactivity-T4YwTh-
8gHWWDDS19IkHRg==/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.pluginactivity-
T4YwTh-8gHWWDDS19IkHRg==/lib/x86_64, /system/lib64, /vendor/lib64]]]
classLoader:java.lang.BootClassLoader@a26e88d
classLoader:java.lang.BootClassLoader@a26e88d

通过打印结果可知,应用程序类是由 PathClassLoader 加载的,Activity 类是 BootClassLoader 加载的,并且
BootClassLoader 是 PathClassLoader 的 parent,这里要注意 parent 与父类的区别。这个打印结果我们下面还
会提到。
加载原理
那我们如何使用类加载器去加载一个类呢?
非常的简单,例如:我们有一个apk文件,路径是 apkPath,然后里面有个类 com.enjoy.plugin.Test,那么我们可
以通过如下方式去加载 Test 类:

DexClassLoader dexClassLoader = new
DexClassLoader(dexPath,context.getCacheDir().getAbsolutePath(), null,
context.getClassLoader());
Class<?> clazz = dexClassLoader.loadClass("com.enjoy.plugin.Test");

因为我们需要将插件的 dex 文件加载到宿主里面,所以我们接下来分析源码,看 DexClassLoader 类加载器到底是
怎么加载一个 apk 的 dex 文件的。
通过查找发现,DexClassLoader 类中没有 loadClass 方法,一路向上查找,最后在 ClassLoader 类中找到了改方
法,源码如下:(后续源码如无标明,都是 API 26 Android 8.0)

// /libcore/ojluni/src/main/java/java/lang/ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
// 检测这个类是否已经被加载 --> 1
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
} else {
// 正常情况下不会走这儿,因为 BootClassLoader 重写了 loadClass 方法,结束了递归
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果仍然找不到,就调用 findClass 去查找 --> 2
c = findClass(name);
}
}
return c;
}
// -->1 检测这个类是否已经被加载
protected final Class<?> findLoadedClass(String name) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
// 最后通过 native 方法实现查找
return VMClassLoader.findLoadedClass(loader, name);
}
// -->2 加载器一般都会重写这个方法,定义自己的加载规则
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
// /libcore/libart/src/main/java/java/lang/VMClassLoader.java
native static Class findLoadedClass(ClassLoader cl, String name);

首先检测这个类是否已经被加载了,如果已经加载了,直接获取并返回。如果没有被加载,parent 不为 null,则
调用parent的loadClass进行加载,依次递归,如果找到了或者加载了就返回,如果即没找到也加载不了,才自己
去加载。这个过程就是我们常说的 双亲委托机制。
根据前面的打印结果可以知道,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 重写了 findClass 和 loadClass 方法,并且在 loadClass 方法中,不再获取 parent,
从而结束了递归。

接着我们再来看下,在所有 parent 都没加载成功的情况下,DexClassLoader 是如何加载的。通过查找我们发现
在它的父类 BaseDexClassLoader 中,重写了 findClass 方法

// /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在 pathList 中查找指定的 Class
Class c = pathList.findClass(name, suppressedExceptions);
return c;
}
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// 初始化 pathList
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}

接着再来看 DexPathList 类中的 findClass 方法。

private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
//通过 Element 获取 Class 对象
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
return null;
}

我们发现 Class 对象就是从 Element 中获得的,而每一个 Element 就对应一个 dex 文件,因为我们的 dex 文件可
能有多个,所以这儿使用数组 Element[]。到这儿我们的思路就出来了,分为以下几步:
1. 创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值。
2. 获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。
3. 合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。
4. 最后通过反射将新的 Element[] 赋值给宿主的 dexElements 。
具体代码如下:

public static void loadClass(Context context) {
try {
// 获取 pathList 的字段
Class baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
// 获取 dexElements 字段

资源加载
在项目中,我们一般通过 Resources 去访问 res 中的资源,使用 AssetManager 访问 assets 里面的资源。

 

String appName = getResources().getString(R.string.app_name);
InputStream is = getAssets().open("icon_1.png");

实际上,Resources 类也是通过 AssetManager 类来访问那些被编译过的应用程序资源文件的,不过在访问之前,
它会先根据资源 ID 查找得到对应的资源文件名。 而 AssetManager 对象既可以通过文件名访问那些被编译过的,
也可以访问没有被编译过的应用程序资源文件。
我们来看下 Resources 调用 getString 的代码实现过程

// android/content/res/Resources.java
public String getString(@StringRes int id) throws NotFoundException {
return getText(id).toString();
}
public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
if (res != null) {
return res;
}
}

通过上面的代码知道 Resources 的实现类是 ResourceImpl 类,getAssets() 返回的是 AssetManager,所以也就
证实了资源的加载实际是通过 AssetManager 来加载的。
接下来我们看下 AssetManager 是如何创建和初始化的,又是如何加载 apk 资源的,只有掌握了原理,我们才知
道如何去加载另一个 apk 的资源。

// android/app/LoadedApk
public Resources getResources() {
if (mResources == null) {
// 获取 ResourcesManager 对象的单例,然后调用 getResources 方法去获取 Resources 对象 --> 1
mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),getClassLoader());
}
return mResources;
}
// android/app/ResourcesManager
// --> 1
public @Nullable Resources getResources(@Nullable IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
try {
final ResourcesKey key = new ResourcesKey(
resDir, // 这个就是 apk 文件路径
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
// 获取或者创建 Resources 对象 --> 2
return getOrCreateResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
// --> 2
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
// 创建 ResourcesImpl 对象 --> 3
ResourcesImpl resourcesImpl = createResourcesImpl(key);
// resources 是 ResourcesImpl 的装饰类
return resources;
}
// --> 3
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
// 创建 AssetManager 对象 --> 4
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
// 将 assets 对象传入到 ResourcesImpl 类中
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
// --> 4
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
if (key.mResDir != null) {
// 通过 addAssetPath 方法添加 apk 文件的路径
if (assets.addAssetPath(key.mResDir) == 0) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
return assets;
}

通过上面代码的分析,我们知道了 apk 文件的路径是通过 assets.addAssetPath 方法设置的,所以如果我们想将
插件的 apk 文件添加到宿主中,就可以通过反射修改这个地方。

实现步骤:
1. 创建一个 AssetManager 对象,并调用 addAssetPath 方法,将插件 apk 的路径作为参数传入。
2. 将第一步创建的 AssetManager 对象作为参数,创建一个新的 Resources 对象,并返回给插件使用。
具体代码如下:

public static Resources loadResource(Context context) {
try {
Class<?> assetManagerClass = AssetManager.class;
AssetManager assetManager = (AssetManager) assetManagerClass.newInstance();
Method addAssetPathMethod = assetManagerClass.getDeclaredMethod("addAssetPath",
String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, apkPath);
Resources resources = context.getResources();
//用来加载插件包中的资源
return new Resources(assetManager, resources.getDisplayMetrics(),
resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
然后在宿主的自定义 Application 类中添加如下代码:
// 宿主代码
private Resources resources;
@Override
public void onCreate() {
super.onCreate();
// 获取新建的 resources 资源
resources = LoadUtil.loadResource(this);
}
// 重写该方法,当 resources 为空时,相当于没有重写,不为空时,返回新建的 resources 对象
@Override
public Resources getResources() {
return resources == null ? super.getResources() : resources

接着在插件中,创建BaseActivity,如下

public abstract class BaseActivity extends Activity {
@Override
public Resources getResources() {
if (getApplication() != null && getApplication().getResources() != null) {
// 因为宿主重写了该方法,所以获取的将是新创建的 resources 对象
return getApplication().getResources();
}
return super.getResources();
}

然后让插件的 Activity 都继承自 BaseActivity,这样,插件在获取资源时,使用的就是在宿主中新创建的
resources 对象,也就可以拿到资源了。
宿主启动插件的Activity
Activity 是需要在清单文件中注册的,显然,插件的 Activity 没有在宿主的清单文件中注册,那我们如何来启动它
呢?
 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值