Android插件化-ClassLoader Injection-Java类插件化

269 阅读3分钟

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

Android应用本身基于魔改后的Java虚拟机,而有插件是未安装的Apk,系统不会处理其中的类,所有需要使用ClassLoader加载Apk,然后反射里面的代码。

1.java中的ClassLoader

  • BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等
  • ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包
  • AppClassLoader 负责加载 classpath 里的 jar 包和目录

2.Android中的ClassLoader

在Android系统中的ClassLoader是用来加载dex文件的,也可以加载包含dex的apk文件和jar文件,dex文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并。优化(把不同class文件重复的东西只保留一份),然后生成一个最终的class.dex文件。

PathClassLoader用来加载系统类和应用程序类,可以加载已经安装的apk目录下的dex文件

public class PathClassLoader extends BaseDexClassLoader {
    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!");
    }
}

DexClassLoader用来加载dex文件,可以从存储空间加载dex文件

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

在插件化中,一般使用DexClassLoader。

3.双亲委派机制

每一个ClassLoader中都有个一个parent对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果parent为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。下面是ClassLoader的loadClass方法

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) {
            //父类未找到,使用自己加载
            c = findClass(name);
        }
    }
    return c;
}

4.如何加载插件中的类

要加载插件中的类,我们首先创建一个DexClassLoader,DexClassLoader构造函数如下:

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

构造方法的4个参数:

  • dexPath:dex相关文件(dex、apk、jar)的路径,多个使用冒号分隔
  • optimizedDirectory:解压的dex文件存储路径,路径必须为内部存储路径,一般使用程序私有路径:/data/data/{package_name}/
  • librarySearchPath:包含c/c++库的路径集合,多个路径用分隔符分隔,可为null
  • parent:父加载器

创建DexClassLoader实例后,只要调用其loadClass(className)方法就可以加载插件中的类了。具体如下:

//从assets中将插件apk复制到内部空间
private void extractPlugin() throws IOException {
    InputStream inputStream = mContext.getAssets().open("plugin.apk");
    Path srcPath = Paths.get(mContext.getFilesDir().getAbsolutePath(), "plugin.apk");
    Files.copy(inputStream, srcPath);
}

private void init() throws IOException {
    extractPlugin();

    String pluginPath = getFilePath(mContext.getFilesDir().getAbsolutePath(), "plugin.apk");
    String nativePath = getFilePath(mContext.getFilesDir().getAbsolutePath(), "pluginlib");
    String dexOutPath = getFilePath(mContext.getFilesDir().getAbsolutePath(), "dexout");
	
    //使用DexClassLoader加载插件apk
    DexClassLoader pluginClassLoader = new DexClassLoader(pluginPath, dexOutPath, nativePath, mContext.getClassLoader())
}

加载流程如下:

5.执行插件类的方法

通过反射来执行类的方法

Class loadClass = pluginClassLoader.loadClass("aclass");
loadClass.getMethod("test", null).invoke(loadClass);

这个过程交过ClassLoader注入,完成注入后,所有来自宿主的类都是用宿主的ClassLoader进行加载,所有来着插件Apk的类使用插件ClassLoader加载,而由于ClassLoader的双亲委派机制,实际上系统类不会收ClassLoader的类隔离机制的影响,这样宿主Apk就可以在宿主进程中使用来自插件的组件类了。