在多年的迭代和升级工作中,组件化项目越来越庞大(几十个模块,近10个第三方播放SDK),直接导致发版困难、方法数超标、工作效率大大降低,质量问题频发等等。项目迫切需要一套方案来解决这些问题。由于我们是自行研发的系统和主板,如果直接使用第三方框架,可能会引起相关的适配问题而不好解决,所以需要实现一套自己的插件化框架,也便于后期进行更多的定制。于是进行了下面粗浅的研究。
项目是影视类项目,引进了很多第三方播放SDK,实际上用户在单次启动时并不会用到这么多的SDK,所以直接加载全部的SDK是很浪费性能的。同时,各个公司的SDK是相互不干扰的,按照以往统一升级的做法,在遇到一家有任何问题的时候只能整体应用升级,非常不方便。且在日常版本迭代中任何一个模块出问题都会直接导致项目延期。。。综合以上情况我们需要做到以下几点:
1、插件的动态加载,使用到再加载,且插件不能安装,不能修改第三方SDK的内容;
2、无差别加载插件,实现在不修改主工程的情况下直接接入新的SDK;
3、插件版本控制和独立升级
4、资源的动态加载
一:插件的动态加载
为优化应用启动速度、运行流畅性、内存占用等,可以在使用到相关公司的资源时,再加载对应的插件(实际上用户一般只会使用到一到两家的资源,这时候全量加载上来完全没有必要)。
对于插件的加载主要分为三个部分
1、dex加载
由于插件apk是没有安装的,我们不能直接使用,需要将插件的dex加载到宿主中来运行。对DexClassLoader相关源码分析后,可以发现类加载的主要逻辑处理都在DexPathList类中进行,其中部分代码如下,这部分也是我们加载插件的关键:
class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";
private final ClassLoader definingContext;
private final DexPathList.Element[] dexElements;//dex数组,我们要将插件的dex注入到该数组中
private final DexPathList.Element[] nativeLibraryPathElements;//jni数组
private final List<File> nativeLibraryDirectories;//jni加载路径
private final List<File> systemNativeLibraryDirectories;
private final IOException[] dexElementsSuppressedExceptions;
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
if(definingContext == null) {
throw new NullPointerException("definingContext == null");
} else if(dexPath == null) {
throw new NullPointerException("dexPath == null");
} else {
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;
ArrayList<IOException> suppressedExceptions = new ArrayList();
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
this.nativeLibraryDirectories = splitPaths(libraryPath, false);
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList(this.nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(this.systemNativeLibraryDirectories);
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, (File)null, suppressedExceptions);
if(suppressedExceptions.size() > 0) {
this.dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
} else {
this.dexElementsSuppressedExceptions = null;
}
}
}
public Class findClass(String name, List<Throwable> suppressed) {
DexPathList.Element[] arr$ = this.dexElements;
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; ++i$) {
DexPathList.Element element = arr$[i$];
DexFile dex = element.dexFile;
if(dex != null) {
Class clazz = dex.loadClassBinaryName(name, this.definingContext, suppressed);
if(clazz != null) {
return clazz;
}
}
}
if(this.dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(this.dexElementsSuppressedExceptions));
}
return null;
}
在本项目中不能修改第三方SDK内部的调用,我们必须保证让没有安装的插件跟正常安装的apk一样使用,从以上源码可以看到应用中的类资源在构造时会被加载到dexElements数组中,在使用时则会遍历dexElements,如果找不到则会报noClassFoundError,所以我们的思路是当前通用的方案:将插件的dex注入到应用本身的dexElements数组中,以流行的hook方式完成注入,具体实现如下:
//先构造插件的DexClassLoader
DexClassLoader pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), soPath, context.getClassLoader());
//判断是否可以拿到BaseDexClassLoader,防止注入失败
boolean hasBaseDexClassLoader = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseDexClassLoader = false;
Log.i(TAG, "no find class dalvik.system.BaseDexClassLoader inject failed!");
}
if (hasBaseDexClassLoader) {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
try {
Log.i(TAG, "get dex dexElements");
//获取应用本身的dex数组
Object baseDexElements = getDexElements(getPathList(pathClassLoader));
//获取插件的dex数组
Object addDexElements = getDexElements(getPathList(dexClassLoader));
//合并宿主和插件的dex数组
Object dexElements = combineAllArray(baseDexElements, addDexElements);
Log.i(TAG, "get Baseclassloader PathList");
Object pathList = getPathList(pathClassLoader);
Log.i(TAG, "do inject dex");
//输入
setField(pathList, pathList.getClass(), "dexElements", dexElements);
Log.i(TAG, "inject dex SUCCESS");
} catch (Throwable e) {
e.printStackTrace();
}
}
private static Object getPathList(BaseDexClassLoader classLoader) {
Class<? extends BaseDexClassLoader> aClass = classLoader.getClass();
Class<?> superclass = aClass.getSuperclass();
try {
Field pathListField = superclass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object object = pathListField.get(classLoader);
return object;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
private static Object getDexElements(Object object) {
if (object == null)
return null;
Class<?> aClass = object.getClass();
try {
Field dexElements = aClass.getDeclaredField("dexElements");
dexElements.setAccessible(true);
return dexElements.get(object);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
private static Object combineAllArray(Object object, Object object2) {
int oldSize = Array.getLength(object);
int addSize = Array.getLength(object2);
Class<?> aClass = Array.get(object, 0).getClass();
Object obj = Array.newInstance(aClass, oldSize + addSize);
for(int i = 0;i < oldSize; i++) {
Array.set(obj, i, Array.get(object, i));
}
for(int i = 0; i < addSize; i++) {
Array.set(obj, oldSize + i, Array.get(object2, i));
}
return obj;
}
private static void setField(Object pathList, Class aClass, String fieldName, Object fieldValue) {
try {
Field declaredField = aClass.getDeclaredField(fieldName);
declaredField.setAccessible(true);
declaredField.set(pathList, fieldValue);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
以上完成了插件apk的dex注入,由此可以实现对插件类的调用。由于此类SDK中四大组件只涉及到service的调用,所以我们要保证SDK内service的正常调用,按以上做法实现注入后实际并不能启动service,因为Android在使用到service的时候需要校验是否在manifest里注册,因我们的dex已经完成注入,这里我们只需要将插件中的service在宿主中注册即可。且测试正常使用。关于插件aar中是否有service,有哪些service,可以通过反编译的方式查看,具体可见://TODO
2、jni加载
第三方SDK通常都会包含一些so文件,以上的注入方法对so是无效的,所以我们要单独实现so的注入,同样的原理,Android在使用so的时候也会通过遍历DexPathList中的nativeLibraryPathElements数组。如果找不到则会报错UnsatisfiledLinkError。
class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";
private final ClassLoader definingContext;
private final DexPathList.Element[] dexElements;//dex数组,我们要将插件的dex注入到该数组中
private final DexPathList.Element[] nativeLibraryPathElements;
private final List<File> nativeLibraryDirectories;//jni加载路径
具体实现方式如下:
Object pathList = getPathList((PathClassLoader) context.getClassLoader());
//获取应用当前的so加载路径数组
Field nativeLibraryDirectories = pathList.getClass().getDeclaredField("nativeLibraryDirectories");
nativeLibraryDirectories.setAccessible(true);
//获取 DEXPATHList中的属性值
File[] files1 = (File[]) nativeLibraryDirectories.get(pathList);
for(int i=0;i<files1.length;i++) {
Log.i(TAG, "get base JniLib success "+files1[i].getAbsolutePath());
}
//创建新的数组
Object filesss = Array.newInstance(File.class, files1.length + 1);
//添加自定义.so路径
File pluginFile = new File(soPath);
//添加插件的so路径
Array.set(filesss, 0, pluginFile);
Log.i(TAG, "add plugin JniLib : "+pluginFile.getAbsolutePath());
//将系统自己的追加上
for(int i = 1;i<files1.length+1;i++){
Array.set(filesss,i,files1[i-1]);
}
//注入so
nativeLibraryDirectories.set(pathList, filesss);
Log.i(TAG, "inject JniLib success ");
以上实现了so的注入,但是在测试中发现,Android7.0以上注入不生效,这里暂时改成手动加载的方式,但是在SDK内部如果有System.loadlibrary的动作,则需要catch UnsatisfiledLinkError,否则应用崩溃。所以,最好还是插件的so路径注入到宿主中最为稳妥,这里只是暂时的解决方法。
if(Build.VERSION.SDK_INT >= 24) {
Log.i(TAG, "injectJniLibPath in Android >= 7.0");
File pluginFile = new File(soPath);
if(pluginFile.exists() && pluginFile.isDirectory()) {
File[] libs = pluginFile.listFiles();
for(int i=0;i<libs.length;i++) {
Log.i(TAG, "System.load "+libs[i].getAbsolutePath());
System.load(libs[i].getAbsolutePath());
Log.i(TAG, "System.load success "+libs[i].getAbsolutePath());
}
}
}
3、resource加载
Android对于资源的使用是通过R.java中生成的资源id来调用的,宿主和插件既然都是独立的apk,那么最终生成的资源id很有可能会冲突,插件中使用的资源在注入dex到宿主后,资源id对应的资源是宿主中的资源而非插件自身的资源。直接使用是会错位的。本项目中SDK内没有资源,所以没有进行太过深入的研究,这里介绍下当前流行的方案:
1、滴滴的VirtualAPK框架,其原理是修改插件apk打包的资源id,使插件apk的资源id避开宿主的0x7F开头的资源id,再将插件的资源注入到宿主中,以完成宿主和插件的资源合并,达到正常使用资源的效果。
2、携程DynamicAPK插件化框架,原理差不多,都是修改资源id的方式,只不过一个是修改打包工具aapt,一个是修改打包完成后的产物。
这里提一个比较简单的方法:在不修改aapt以及aapt产物的情况下,更快更简单的实现资源的正常使用。通过研究资源调用的源码发现,资源的调用是通过Context内的getResources()来实现的。所以我们只要重写传递给插件的context的getResources()方法,将getResources()方法的返回值修改为插件自己的Resource就好了。
首先,我们要创建插件apk的Resource:
private Resources createResourcesFromPaths(Context context, String... apkPaths)
{
Resources resources = null;
if(apkPaths != null)
{
AssetManager assetManager = createAssetManager(apkPaths);
if(assetManager != null)
{
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
Log.d(TAG, "createResourcesFromPaths success !");
}
}
return resources;
}
/**
* 根据指定资源文件路径创建AssetManager
* @param apkPaths
* @return
*/
private AssetManager createAssetManager(String... apkPaths)
{
AssetManager assetManager = null;
try
{
assetManager = AssetManager.class.newInstance();
} catch (InstantiationException e1)
{
e1.printStackTrace();
} catch (IllegalAccessException e1)
{
e1.printStackTrace();
}
try
{
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
for(String path : apkPaths)
{
int ret = (Integer) addAssetPath.invoke(assetManager, path);
Log.d(TAG, "addAssetManager, path = "+ path + ", ret=" + ret);
}
} catch (IllegalAccessException e)
{
e.printStackTrace();
} catch (NoSuchMethodException e)
{
e.printStackTrace();
} catch (IllegalArgumentException e)
{
e.printStackTrace();
} catch (InvocationTargetException e)
{
e.printStackTrace();
}
return assetManager;
}
创建完resource之后,修改插件使用的context的getresource()方法的返回值为我们创建的这个Resource,这里我们实现一个pluginContext来实现:
public class PluginContext extends ContextWrapper {
@Override
public Resources getResources()
{
return myPluginResources;
}
}
这种方式的虽然简单,但是使用很有限制,一般传递的context都是当前activity,这种方式也就不适合了,实际比较适合自己开发插件apk的情况。
到这里,已经基本上完成了插件的注入,可以正常使用插件的内容了。