在上一章节中我们讲到了Replugin初始化的时候创建了插件管理进程作为服务端。其他工作进程作为客户端,然后分别在各自的进程中初始化自己要做的事情。创建了多个Binder对象用来完成客户端和服务端的信息交互。在完成PmBase 的初始化后。在PMF.init()中还有剩下的部分代码
com.qihoo360.loader2.PMF
public static final void init(Application application) {
setApplicationContext(application);
PluginManager.init(application);
sPluginMgr = new PmBase(application);
sPluginMgr.init();
Factory.sPluginManager = PMF.getLocal();//1
Factory2.sPLProxy = PMF.getInternal();//2
PatchClassLoaderUtils.patch(application);//3
}
"1"中PMF.getLocal()就是在PmBase 构造函数中 创建的PluginCommImpl。这个类负责宿主与插件、插件间的互通,因此这个类中的方法使用的频率会比较高。而Factory类是一个包装类,缓存了PluginCommImpl对象,方便以后的调用。同样的"2"中的PMF.getInternal() 就是在PmBase 中创建的PluginLibraryInternalProxy。同样的这个类中的方法也会经常使用到,比如说startActivity这个方法。
"3"PatchClassLoaderUtils.patch(application)这里的逻辑就涉及到这个框架的核心唯一hook 的点,hook系统的ClassLoader
Replugin是如何hook 系统的ClassLoader。
//com.qihoo360.loader.utils.PatchClassLoaderUtils
public class PatchClassLoaderUtils {
public static boolean patch(Application application) {
try {
// 获取Application的BaseContext (来自ContextWrapper)
Context oBase = application.getBaseContext();
if (oBase == null) {
return false;
}
// 获取mBase.mPackageInfo
// 1. ApplicationContext - Android 2.1
// 2. ContextImpl - Android 2.2 and higher
// 3. AppContextImpl - Android 2.2 and higher
Object oPackageInfo = ReflectUtils.readField(oBase, "mPackageInfo");
if (oPackageInfo == null) {
return false;
}
// mPackageInfo的类型主要有两种:
// 1. android.app.ActivityThread$PackageInfo - Android 2.1 - 2.3
// 2. android.app.LoadedApk - Android 2.3.3 and higher
// 获取mPackageInfo.mClassLoader
ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
if (oClassLoader == null) {
return false;
}
// 外界可自定义ClassLoader的实现,但一定要基于RePluginClassLoader类
ClassLoader cl = RePlugin.getConfig().getCallbacks().createClassLoader(oClassLoader.getParent(), oClassLoader);
// 将新的ClassLoader写入mPackageInfo.mClassLoader
ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);
// 设置线程上下文中的ClassLoader为RePluginClassLoader
// 防止在个别Java库用到了Thread.currentThread().getContextClassLoader()时,“用了原来的PathClassLoader”,或为空指针
Thread.currentThread().setContextClassLoader(cl);
} catch (Throwable e) {
e.printStackTrace();
return false;
}
return true;
}
}
这里利用了反射工具,是代码看上去非常简洁。主要做了这么几件事情
1. application.getBaseContext() 获取APP的BaseContext
2.利用反射获取BaseContext 中的mPackageInfo字段,他的类型是LoadedApk类型
3.利用反射获取mPackageInfo 中的mClassLoader字段。
4.通过宿主的父ClassLoader和宿主ClassLoader利用RePluginCallbacks的createClassLoader方法生成RePluginClassLoader
5.替换mPackageInfo.mClassLoader中的系统ClassLoader为我们新生成的RepluginClassLoader.
6.替换线程中的ClassLoader为我们新生成的RepluginClassLoader.
简简单单几步我们就成功的将系统的ClassLoader替换成了我们自己的RePluginClassLoader。这里我们就完成了hook 的工作。那么为什么hook住了ClassLoader我们就可以利用插件化来实现打开插件中的组件呢。我们来看看ClassLoader是如何工作的
Android 中的ClassLoader
ClassLoader 是一个抽象类。主要功能就是 loadClass 和 findClass
//源码地址:libcore/ojluni/src/main/java/java/lang/Classloader
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先判断这个class是否已经被加载
Class> c = findLoadedClass(name);
if (c == null) {
try {
//判断父加载器是否存在
if (parent != null) {
//用父加载器加载这个类
c = parent.loadClass(name, false);
} else {
//这个方法直接返回null
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;
}
在Java中为了保证一个类的唯一性使用了双亲委派模型,也就是说如果要加载一个类首先会委托给自己的父加载器去完成,父加载器会再向上委托,直到加载器加载过这个类,或者如果没有找到父加载器(到达最顶层),子类才会尝试自己去加载,这样就保证了加载的类都是一个类。一个类的唯一性要由它的类加载器和它本身来确定,也就是说一个Class文件如果使用不同的类加载器来加载,那么加载出来的类也是不相等的。在这里Android 的 ClassLoader 也是使用了双亲委托模式。首先判断这个类是否被加载,如果已经被加载了就返回,没有记载先判断父加载器是都存在,如果存在就用父加载器加载这个类,这样就能够一层层向上委托。如果到最顶层加载器也没加载过,子类才尝试加载。
BootClassLoader是ClassLoader的内部类。
//默认修饰
class BootClassLoader extends ClassLoader {
//单例
private static BootClassLoader instance;
@FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
public static synchronized BootClassLoader getInstance() {
if (instance == null) {
instance = new BootClassLoader();
}
return instance;
}
public BootClassLoader() {
super(null);
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
return Class.classForName(name, false, null);
}
。。。
}
Android 系统启动的时候会使用BootClassLoader来预加载常用类。是ClassLoader的内部类,是一个单例类。类是默认修饰。外部无法访问。所以我们在应用中也无法直接调用。
BaseDexClassLoader是ClassLoader的具体实现类,在其构造方法中创建了DexPathList
public BaseDexClassLoader(String dexPath,
String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
boolean isTrusted) {
super(parent);
...
//创建DexPathList
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
...
List suppressedExceptions = new ArrayList();
//调用了DexPathList 的findClass
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;
}
BaseDexClassLoader加载类的时候,就是通过调用DexPathList.findClass进行加载。在DexPathList中有 Element[] dexElements 数据 。 而Element 元素其实就是Dex文件。(这可能是一个普通的dex文件(在这种情况下是dexZipPath应该是null),或者一个jar(在这种情况下,dexZipPath应该表示zip文件)。
public Element(DexFile dexFile, File dexZipPath) {
if (dexFile == null && dexZipPath == null) {
throw new NullPointerException("Either dexFile or path must be non-null");
}
this.dexFile = dexFile;
this.path = dexZipPath;
this.pathIsDirectory = (path == null) ? null : path.isDirectory();
}
public class DexClassLoader extends BaseDexClassLoader {
//android 8.0 及之后的构造函数
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
//android 8.0 前的构造函数
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
PathClassLoader 和DexClassLoader 都继承自BaseDexClassLoader。而且类中只有构造方法,具体的实现都是在BaseDexClassLoader中完成的,而他两的区别就是 在DexClassLoader的构造函数中 有optimizedDirectory参数。而PathClassLoader中没有,
optmizedDirectory 不为空时,使用用户定义的目录作为 DEX 文件优化后产物 .odex 的存储目录,为空时,会使用默认的 /data/dalvik-cache/ 目录。PathClassLoader 其实并不是只能加载安装后的 APK,也可以加载其他 DEX/JAR/APK 文件,只不过生成的 .odex 文件只能存储在系统默认路径下。不过在Android 8.0 之后optmizedDirectory字段已经弃用,PathClassLoader和DexClassLoader已经没有区别了。
//com.qihoo360.replugin.RePluginClassLoader
public class RePluginClassLoader extends PathClassLoader {
。。。
public RePluginClassLoader(ClassLoader parent, ClassLoader orig) {
// 由于PathClassLoader在初始化时会做一些Dir的处理,所以这里必须要传一些内容进来
// 但我们最终不用它,而是拷贝所有的Fields
super("", "", parent);
mOrig = orig;
// 将原来宿主里的关键字段,拷贝到这个对象上,这样骗系统以为用的还是以前的东西(尤其是DexPathList)
// 注意,这里用的是“浅拷贝”
// Added by Jiongxuan Zhang
copyFromOriginal(orig);
initMethods(orig);
}
private void initMethods(ClassLoader cl) {
Class> c = cl.getClass();
findResourceMethod = ReflectUtils.getMethod(c, "findResource", String.class);
findResourceMethod.setAccessible(true);
findResourcesMethod = ReflectUtils.getMethod(c, "findResources", String.class);
findResourcesMethod.setAccessible(true);
findLibraryMethod = ReflectUtils.getMethod(c, "findLibrary", String.class);
findLibraryMethod.setAccessible(true);
getPackageMethod = ReflectUtils.getMethod(c, "getPackage", String.class);
getPackageMethod.setAccessible(true);
}
private void copyFromOriginal(ClassLoader orig) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
// Android 2.2 - 2.3.7,有一堆字段,需要逐一复制
// 以下方法在较慢的手机上用时:8ms左右
copyFieldValue("libPath", orig);
copyFieldValue("libraryPathElements", orig);
copyFieldValue("mDexs", orig);
copyFieldValue("mFiles", orig);
copyFieldValue("mPaths", orig);
copyFieldValue("mZips", orig);
} else {
// Android 4.0以上只需要复制pathList即可
// 以下方法在较慢的手机上用时:1ms
copyFieldValue("pathList", orig);
}
}
private void copyFieldValue(String field, ClassLoader orig) {
try {
Field f = ReflectUtils.getField(orig.getClass(), field);
if (f == null) {
return;
}
// 删除final修饰符
ReflectUtils.removeFieldFinalModifier(f);
// 复制Field中的值到this里
Object o = ReflectUtils.readField(f, orig);
ReflectUtils.writeField(f, this, o);
} catch (IllegalAccessException e) {
}
}
@Override
protected Class> loadClass(String className, boolean resolve) throws ClassNotFoundException {
//
Class> c = null;
//先通过类名找找看有没有插件中对应的类,找到了直接用插件的ClassLoader加载
c = PMF.loadClass(className, resolve);
if (c != null) {
return c;
}
//
try {
// 插件中没有找到,则用宿主原来的ClassLoader加载
c = mOrig.loadClass(className);
return c;
} catch (Throwable e) {
//
}
//
return super.loadClass(className, resolve);
}
//还有一些重写的方法。可以去源码中查看
。。。。
}
首先RepluginClassLoader为了兼容Android 7.0以上的LoadedApk.updateApplicationInfo中,对addDexPath方法的依赖, 特将继承关系调整到PathClassLoader,以前是ClassLoader。
RePluginClassLoader在构造方法中将宿主原来ClassLoader中的重要字段拷贝到本对象中,用来欺骗系统,接着反射获取原ClassLoader中的重要方法用来重写这些方法,最后重写了loadClass方法,首先会通过要加载的类名来查找是否存在对应的插件信息,如果有取出插件信息中的ClassLoader,使用该插件的ClassLoader来加载类,如果没有找到再使用宿主原来的ClassLoader来加载
//com.qihoo360.replugin.PluginDexClassLoader
public class PluginDexClassLoader extends DexClassLoader {
。。。
//初始化插件的DexClassLoader的构造函数。插件化框架会调用此函数。
public PluginDexClassLoader(PluginInfo pi, String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
mPluginName = pi.getName();
installMultiDexesBeforeLollipop(pi, dexPath, parent);
mHostClassLoader = RePluginInternal.getAppClassLoader();
initMethods(mHostClassLoader);
}
private static void initMethods(ClassLoader cl) {
Class> clz = cl.getClass();
if (sLoadClassMethod == null) {
sLoadClassMethod = ReflectUtils.getMethod(clz, "loadClass", String.class, Boolean.TYPE);
if (sLoadClassMethod == null) {
throw new NoSuchMethodError("loadClass");
}
}
}
@Override
protected Class> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 插件自己的Class。从自己开始一直到BootClassLoader,采用正常的双亲委派模型流程,读到了就直接返回
Class> pc = null;
ClassNotFoundException cnfException = null;
try {
pc = super.loadClass(className, resolve);
if (pc != null) {
return pc;
}
} catch (ClassNotFoundException e) {
// Do not throw "e" now
cnfException = e;
if (PluginDexClassLoaderPatch.need2LoadFromHost(className)) {
try {
return loadClassFromHost(className, resolve);
} catch (ClassNotFoundException e1) {
// Do not throw "e1" now
cnfException = e1;
}
} else {
}
}
// 若插件里没有此类,则会从宿主ClassLoader中找,找到了则直接返回
// 注意:需要读取isUseHostClassIfNotFound开关。默认为关闭的。可参见该开关的说明
if (RePlugin.getConfig().isUseHostClassIfNotFound()) {
try {
return loadClassFromHost(className, resolve);
} catch (ClassNotFoundException e) {
// Do not throw "e" now
cnfException = e;
}
}
// At this point we can throw the previous exception
if (cnfException != null) {
throw cnfException;
}
return null;
}
。。。。
}
RepluginDexClassLoader 继承自DexClassLoader 。用来做一些“更高级”的特性,在RePluginConfig中可直接配置。原本只需要DexClassLoader即可,但为了要支持一些高级特性(如可自由使用宿主的Class),实现了RepluginDexClassLoader这个类。这个类的工作就是去插件中通过类名寻找相应的类。如果没有找到就去宿主中找,如果要在宿主中找,要把isUseHostClassIfNotFound这个开关打开,默认是关闭的。
结合之前Replugin hook 了系统的PathClassLoader 替换成了自己的RepluginClassLoader。重写了loadClass方法来实现拦截类的加载过程,并且每一个插件apk都设置了一个PluginDexClassLoader,在加载类的时候先使用这个PluginDexClassLoader去加载,加载到了直接返回否则再通过持有系统或者说是宿主原有的PathClassLoader去加载,这样就保证了不管是插件类、宿主类、还是系统类都可以被加载到。这样就实现了插件化加载插件中的类的整个过程。
最后在attachBaseContext方法中还剩下PMF.callAttach();
//com.qihoo360.loader2.PmBase
final void callAttach() {
//获取ClassLoader
mClassLoader = PmBase.class.getClassLoader();
// 挂载
for (Plugin p : mPlugins.values()) {
p.attach(mContext, mClassLoader, mLocal);
}
// 加载默认插件
if (PluginManager.isPluginProcess()) {
//默认插件不为空
if (!TextUtils.isEmpty(mDefaultPluginName)) {
//获取插件
Plugin p = mPlugins.get(mDefaultPluginName);
if (p != null) {
boolean rc = p.load(Plugin.LOAD_APP, true);
if (!rc) {
}
if (rc) {
mDefaultPlugin = p;
mClient.init(p);
}
}
}
}
}
首先获取ClassLoader 这里是宿主的PathClassLoader。 然后把Context , PathClassLoader , PluginCommImpl 挂在到插件对象上。最后如果当前进程是插件进程就加载默认插件。