一、Android插件化技术是什么?
1、定义
插件化技术的核心是动态加载,动态加载可理解为,应用在运行的时候通过加载一些本地不存在的可执行文件实现一些特定的功能。
2、分类
根据加载的可执行文件的类型,动态加载可分为动态加载dex/apk/jar和动态加载so库。
二、ClassLoader的工作机制
既然是要实现动态加载,那我们应该先对ClassLoader有大致的认识,并了解如何通过ClassLoader来加载本地不存在的可执行文件。
1、概述
Android中,系统ClassLoader继承关系如下,
这里面,我们通常会用到的是BootClassLoader、DexClassLoader、PathClassLoader。接下来,我们就来看看它们的作用是什么。
(1)BootClassLoader
作用:预加载常用类
(2)DexClassLoader
作用:加载dex文件以及包含dex的压缩文件
构造函数:
public DexClassLoader(String dexPath, // dex文件路径集合
String optimizedDirectory, // 解压的dex文件存储路径(可理解为缓存目录),必须是内部存储路径,
String librarySearchPath, // 包含 C/C++ 库的路径集合,可以是null
ClassLoader parent) { // 父加载器
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
(3)PathClassLoader
作用:加载系统类和应用程序的类
构造函数:
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
(4)BaseDexClassLoader
作用:ClassLoader的通用实现类,DexClassLoader和PathClassLoader都是继承自这个类的,我们将通过这个它来了解类加载器是如何加载dex文件的。
构造函数:
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
DexPathList的构造方法如下:
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
DexPathList#makeDexElements()方法如下:
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
......
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) { // 以.dex结尾的
// Raw dex file (not inside a zip/jar).
try {
/************ 1 ************/
dex = loadDexFile(file, optimizedDirectory);
} catch (loadDex ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) { // 以.apk/.jar/.zip结尾的
try {
zip = new ZipFile(file);
} catch (IOException ex) {
......
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
......
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
loadDexFile()看起来是一个很重要的方法,代码如下:
private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
loadDex最终会调用native的openDexFile,从而完成将Dex文件加载到optimizedDirectory,即内部存储中。
小结:以上的整个流程是创建ClassLoader,并将Dex文件加载到内部存储中。
通过上面的分析,我们明白了DexClassLoader和PathClassLoader的构造函数中几个参数的作用。由于PathClassLoader是不能指定optimizedDirectory的,因此PathClassLoader只能加载内部存储中的可执行文件,即已经安装的apk的dex文件。
2、loadClass方法分析
既然是要实现动态加载,loadClass就是我们的重头戏了。
先看看ClassLoader中的loadClass方法。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先查找已经加载的类
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) {
// findClass由子类覆写
c = findClass(name);
}
}
return c;
}
findClass是由子类覆写的,我们看看BaseDexClassLoader中的实现是怎样的。
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
DexPathList.java
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
DexFile.java
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
至此,BaseDexClassLoader就完成了对指定类的加载。
通过以上的分析,我们基本知道如何通过ClassLoader加载外部的可执行文件了。
三、如何动态加载插件包
1、动态加载插件包中的普通类
理解了ClassLoader的工作机制后,动态加载普通类非常简单了,实例代码如下:
DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,
MyApplication.getApplication().getDir("cache", Context.MODE_PRIVATE),
null, ClassLoader.getSystemClassLoader());
Class clazz = dexClassLoader.loadClass(className);
有了Class对象,我们就可以利用反射调用实例的方法了。
2、动态加载插件包中的res
我们知道,res里的每个资源都会在 R.java中生成一个对应的Integer类型的id,应用启动时会把R.java注册到当前的上下文环境中,因此我们在代码里可以通过R.id来访问资源。而外部的apk的资源并没有注册到当前程序的上下文环境中,因此无法通过R.id来访问资源。那我们应该怎么访问插件包中的资源呢,Android源码分析-资源加载机制这篇文章中给出了解决方案,即模仿系统创建Resources的方式,创建插件包的Resources对象,这样我们就可以通过resources.getDrawable等方法,通过R.id来访问插件包中的资源来。
创建Resources对象需要AssetManager、DisplayMetrics和Configuration。源码中的实现如下:
ResourcesManager#createResourcesImpl()
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
if (DEBUG) {
Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
}
return impl;
}
ResourcesManager#createAssetManager
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (key.mResDir != null) {
if (assets.addAssetPath(key.mResDir) == 0) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
if (key.mSplitResDirs != null) {
for (final String splitResDir : key.mSplitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
Log.e(TAG, "failed to add split asset path " + splitResDir);
return null;
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
assets.addOverlayPath(idmapPath);
}
}
if (key.mLibDirs != null) {
for (final String libDir : key.mLibDirs) {
if (libDir.endsWith(".apk")) {
// Avoid opening files we know do not have resources,
// like code-only .jar files.
if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}
return assets;
}
我们可以模仿源码构造自己的Resources对象,代码如下:
protected void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
3、动态加载Activity
动态加载Activity的主要问题是通过动态加载获得的Activity并没有经过系统初始化,并不具备Activity的性质,如生命周期等。
Android apk动态加载机制的研究(二):资源加载和activity生命周期管理这篇文章给出的方案是在主程序中构建一个空壳Activity,在这个空壳Activity的生命周期方法中通过反射调用插件Activity的生命周期方法,从而实现对插件Activity的生命周期管理。
参考资料: