天天看点

Android插件化的一种简单实现-插件的动态加载

在多年的迭代和升级工作中,组件化项目越来越庞大(几十个模块,近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的情况。

到这里,已经基本上完成了插件的注入,可以正常使用插件的内容了。