我们在上一篇文章《Android插件化原理和实践 (一)之 插件化简介和基本原理简述》中介绍了插件化一些基本知识和历史,最后还列出了三个根本问题。接下来我们打算围绕着这三个根本问题展开对插件化的学习。首先本章将介绍第一个根本问题:宿主和插件中如何相互调用代码。要实现它们相互调用,就得要宿主先将插件加载起来。Android中要想从加载外部插件就在于ClassLoader。
1 初识PathClassLoader和DexClassLoader
ClassLoader下有子类BaseDexClassLoader,BaseDexClassLoader又有两个重要的子类:PathClassLoader和DexClassLoader。先来看看PathClassLoader和DexClassLoader的源码:
PathClassLoader.java
/**
* 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 libraryPath 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 libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
DexClassLoader.java
/**
* 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.getDir(String, int)} to create
* such a directory: <pre> {@code
* File dexOutputDir = context.getDir("dex", 0);
* }</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 libraryPath 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 libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
PathClassLoader和DexClassLoader这两个类很简单,区别仅在于构造函数的第2个参数optimizedDirectory,其中PathClassLoader把这个参数设置为null。我们来翻译一下PathClassLoader的头部注释,大概意思是:PathClassLoader用于加载本地文件系统上的文件和目录,它不能从网络上加载,Android使用这个类来加载系统类及应用程序。再来翻译一下DexClassLoader的头部注释,大概意思是:DexClassLoader用于加载来自外部jar、apk包含的dex目录。
说到加载dex,是不是想起了为了解决65535问题,而使用了multidex来将一个apk文件内的dex进行拆分。没错,其实在使用这个过程中,主的classes.dex是由App使用PathClassLoader进行加载的,而其余的classes2.dex这些子dex都会以资源的形式在App启动后使用DexClassLoader进行加载到ClassLoader中的。
接着再来翻译一下DexClassLoader的构造函数中参数的意思:
dexPath | 接收一个jar或apk文件列表,多个的话,由File.pathSeparator分隔,在Android中默认使用”:”分隔 |
optimizedDirectory | dex文件被加载后会被编译器优化,优化之后的dex存放路径,不能为空。要注意的是,它需要一个应用私有的可写的一个路径,以防止应用被注入攻击,例子如: File dexOutputDir = context.getDir(“dex”, 0); |
libraryPath | 包含libraries的目录列表,同样用File.pathSeparator分割,可为空,传入null |
parent | 父类的class loader |
2 加载插件中的类
好了,我们已经清楚了DexClassLoader是用来加载外部apk的dex,而且也了解了它的构造函数的调用了,那么现在就来通过一个Demo来看看它是否真的可以加载外部apk的dex吧。Demo工程目录如下所示,存在两个applicaton模块,一个是宿主Host,一个是插件Plugin。
Plugin插件中仅有一个TestBean.java类:
package com.zyx.plugin;
public class TestBean {
private String mName = "子云心";
public void setName(String name) {
mName = name;
}
public String getName() {
return mName;
}
}
Host的MainActivity中编写所有的逻辑代码:
package com.zyx.plugindemo;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends Activity {
private final static String sApkName = "Plugin-debug.apk";
private DexClassLoader mClassLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
simulationDownload(this, sApkName);
mClassLoader = loadPlugin(this, sApkName);
onDo();
}
/**
* 加载插件
* @param context
* @param apkName
* @return
*/
private DexClassLoader loadPlugin(Context context, String apkName) {
File extractFile = context.getFileStreamPath(apkName);
String dexpath = extractFile.getPath();
File fileRelease = context.getDir("dex", 0);
String absolutePath = fileRelease.getAbsolutePath();
DexClassLoader classLoader = new DexClassLoader(dexpath, absolutePath, null, context.getClassLoader());
return classLoader;
}
/**
* 执行插件代码
*/
private void onDo() {
try {
Class mLoadClassBean = mClassLoader.loadClass("com.zyx.plugin.TestBean");
Object testBeanObject = mLoadClassBean.newInstance();
Method getNameMethod = mLoadClassBean.getMethod("getName");
getNameMethod.setAccessible(true);
String name = (String) getNameMethod.invoke(testBeanObject);
Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 模拟下载,实际上是将Assets中的插件apk文件复制到/data/data/files 目录下
* @param context
* @param sourceName
*/
private void simulationDownload(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上述代码中,simulationDownload是一个测试方法代码,为了方便开发验证,我们将Plugin通过编译后生成的Plugin-debug.apk文件放在了Host的assets目录下。然后再通过simulationDownload方法将Plugin-debug.apk文件转存到/data/data/files 目录下来模拟真实开发时的下载逻辑。
我们将重点放回在loadPlugin和onDo两个方法上,loadPlugin方法中通过获取Plugin-debug.apk文件的目录信息来构造了一个DexClassLoader对象,然后在onDo方法中,通过这个对象使用了反射来读到Plugin插件中的TestBean类。App运行后,会弹出一个Toast,内容就是TestBean中的mName值。
你以为插件化中加载插件代码就是这么简单吗?你错了,上面的Demo只能使宿主调用插件中的普通类,而实际上并不能使宿主调起插件中的四大组件。即使你在宿主中的AndroidMainifest.xml中声明了组件信息也是不行。可以尝试在插件中创建一个TestService服务,并在宿主中的AndroidMainifest.xml中声明它,然后在宿主中通过startService来启动服务,最后再次运行App后就会报出以下的异常:
至于为什么?和解决方法是怎样?我们留在下一篇文章来介绍。