cover_image

Android插件化解析 -- 插件加载详解

赵鹏 三七互娱技术团队
2024年03月04日 10:02
01

插件化概述

图片
图片

概述

常规的Android APP是由一个主模块(application)和若干个子模块(library,打包为aar)组成,最终打包成一个apk文件,运行在Android系统中。Android插件化,就是把这些子模块打包成独立的apk,主模块apk加载这些子模块apk后,即可调用子模块中的功能,如下图所示。

图片

Android插件化可以让一个应用(宿主)动态地加载和运行其他应用(插件)的代码和资源等,就像这些代码和资源等就是宿主自身的一部分一样。这种方式在一定程度上突破了Android系统的apk包大小限制和动态更新机制限制。

插件化可以实现如下功能:

  • 动态模块化:减少应用初次安装包的大小,并且在需要时才下载安装其他模块;

    热更新:在不需要用户升级整个应用的情况下,修复Bug或更新某个模块;

    模块隔离:将不同功能或模块隔离开,降低模块间的耦合。

实现内容

完成Android APP的插件化开发,通常需要实现以下内容:

    • 插件化配置

    • 插件加载    

      • so库加载

      • apk加载

      • 资源加载

    • Application插件化

    • 四大组件插件化

      • Activity插件化

      • Service插件化

      • BroadcastReceiver插件化

      • ContentProvider插件化

    • ……

插件加载是插件化的一大重点,也是Application和四大组件插件化的基础,本篇文章主要对插件加载进行详细介绍。

插件加载

宿主要想使用插件,首先必须加载插件apk。宿主加载插件apk的关键步骤如下:

  • 解析过滤插件so库;

    加载插件apk,生成DexClassLoader;

    加载插件资源,生成含有插件和宿主资源的MixResources;

    缓存插件信息。

02

解析过滤插件so库

图片
图片

宿主加载插件的so库时,需要解析插件的apk,根据要配置的CPU架构,过滤插件apk中的so库,并把过滤后的so文件复制到一个单独的目录中(这里复制到与插件apk同级的lib目录中)。关键步骤如下所示:

  • 获取插件apk的完整路径;

    获取系统支持的宿主apk的CPU架构;

    根据系统支持的宿主apk的CPU架构,获取插件最终要适配的CPU架构;

    根据要适配的CPU架构,过滤插件apk的so库并保存到lib目录。

整体流程如下图所示:

图片

第一步比较简单,这里详细介绍后三步。

获取宿主CPU架构

根据宿主的Context,获取系统支持的宿主apk的CPU架构,代码如下所示:

public static String getCpuArchByAppInfo(Context context) {    ApplicationInfo applicationInfo = context.getApplicationInfo();    String cpuArch = "";     try {        // 应用在安装过程中,系统就已经确定这个应用的首选CPU架构,并将这个结果保存在应用的ApplicationInfo的primaryCpuAbi属性值中。        // primaryCpuAbi是应用级别的信息,而非系统级别的信息。        Field primaryCpuAbiField = ApplicationInfo.class.getDeclaredField("primaryCpuAbi");        primaryCpuAbiField.setAccessible(true);         Object o1 = primaryCpuAbiField.get(applicationInfo);        if (o1 != null) {            cpuArch = o1.toString();        }    } catch (Exception e) {        e.printStackTrace();    }    return cpuArch;}


获取适配CPU架构

根据系统支持的宿主apk的CPU架构,获取插件最终要适配的CPU架构,关键步骤如下所示:

  • 如果系统支持的宿主apk的CPU架构名称与自定义支持的CPU架构数组(supportCpuABI)中的某个名称一致,就返回宿主的CPU架构名称;否则,下一步:

    如果宿主的CPU架构名称以supportCpuABI中的某个架构名称开头,就返回支持的CPU架构名称;否则,下一步:

    获取系统支持的CPU架构名称数组(cpuABIs),然后遍历cpuABIs和supportCpuABI,如果有相同的名称,就返回;否则,返回默认的CPU架构名称"armeabi-v7a"。

详细代码,如下所示:

private static final String CPU_ARMEABI = "armeabi";private static final String CPU_ARMEABI_V7 = "armeabi-v7a";private static final String CPU_ARMEABI_V8 = "arm64-v8a";private static final String CPU_X86 = "x86";private static final String CPU_X86_64 = "x86_64"; public static String chooseByX86andArm(Context context) {     // 获取宿主的CPU架构    String hostCpu = getCpuArchByAppInfo(context);    // 支持的CPU架构,可自定义    String[] supportCpuABIs = {CPU_ARMEABI, CPU_ARMEABI_V7, CPU_ARMEABI_V8, CPU_X86, CPU_X86_64};     for (String supportCpuABI : supportCpuABIs) {        // 如果宿主的CPU架构与自定义支持的CPU架构一致,则返回宿主的CPU架构        if (hostCpu.equalsIgnoreCase(supportCpuABI)) {            return hostCpu;        }    }     for (String supportCpuABI : supportCpuABIs) {        // 如果宿主的CPU架构名称以支持的某个CPU架构名称开头,就返回支持的CPU架构名称        if (hostCpu.startsWith(supportCpuABI)) {            return supportCpuABI;        }    }     // 获取系统支持的CPU架构名称数组,然后遍历比对supportCpuABI,获取最终的架构名称    return getCpuArch(supportCpuABIs);} public static String getCpuArch(String[] supportCpuABIs) {    String[] cpuABIs = getCpuABIs();    if (cpuABIs != null && supportCpuABIs != null) {        for (String cpuABI : cpuABIs) {            for (String supportCpuABI : supportCpuABIs) {                if (cpuABI.equalsIgnoreCase(supportCpuABI)) {                    return supportCpuABI;                }            }        }    }    return CPU_ARMEABI_V7;} /** * 获取系统支持的CPU架构 * */@SuppressLint("NewApi")public static String[] getCpuABIs() {    if (Build.VERSION.SDK_INT >= 21) {        return Build.SUPPORTED_ABIS;    } else {        return new String[]{Build.CPU_ABI, Build.CPU_ABI2};    }}


过滤插件so库

这一步需要遍历插件apk中的so文件,当so的CPU架构与要适配的CPU架构一致时,就把该so复制到插件apk的同级目录lib中(若lib目录不存在,需先创建)。

代码如下所示:

/** * 解析插件的 so 库 * * @param context 宿主Context * @param apkFile 插件apk的File文件 * @param libDir  lib目录的路径,与插件apk在同级目录中 * */public static boolean releaseSoFile(Context context, File apkFile, String libDir) {    /*      第一步:创建lib的File文件      */    File libDirFile = new File(libDir);    if (!libDirFile.exists()) {        libDirFile.mkdirs();    } else {        for (String s : libDirFile.list()) {            new File(s).delete();        }    }     /*      第二步:获取插件apk要适配的CPU架构      */    String cpu_architect = ApkInfoUtil.chooseByX86andArm(context);     /*      第三步:把插件apk中CPU架构与cpu_architect一致的so复制到lib目录中      */    boolean isSuccess = true;    try {        // apkFile是插件apk的File文件        ZipFile zipFile = new ZipFile(apkFile);        Enumeration<? extends ZipEntry> entries = zipFile.entries();         while (entries.hasMoreElements()) {            ZipEntry zipEntry = entries.nextElement();            if (zipEntry.isDirectory()) {                continue;            }             String zipEntryName = zipEntry.getName();            if (zipEntryName.endsWith(".so")) {                String[] entryNames = zipEntryName.split("/");                if (entryNames.length > 2) {                    String cpu_architect_dir = entryNames[1];                    if (TextUtils.equals(cpu_architect_dir, cpu_architect)) {                        isSuccess = releaseSoFile(libDir, zipFile, zipEntry);                    }                }            }        }    } catch (IOException e) {        e.printStackTrace();        return false;    }     return isSuccess;} private static boolean releaseSoFile(String libDir, ZipFile zipFile, ZipEntry zipEntry) throws IOException {    InputStream inputStream = zipFile.getInputStream(zipEntry);    File newSoFile = new File(libDir, parseSoFileName(zipEntry.getName()));    Logger.d(TAG, String.format("newSoFile:%s", newSoFile.getAbsolutePath()));    return FileUtil.copyToFile(inputStream, newSoFile);} private static String parseSoFileName(String zipEntryName) {    return zipEntryName.substring(zipEntryName.lastIndexOf("/") + 1);}


03

加载插件apk

图片
图片

宿主中要使用插件apk中的类、so库和资源等,就必须先加载插件apk,生成插件的ClassLoader,并添加到宿主APP的ClassLoader体系中。为便于理解,本小节先介绍类加载机制,再介绍插件apk的加载方法。

类加载机制

ClassLoader简介

图片

ClassLoader是一个抽象类,它通过类的全限定名来获取此类的二进制字节流,核心是findClass()方法。Android中的ClassLoader类加载器派生出的有DexClassLoader和PathClassLoader,这两者的区别如下:

  • DexClassLoader: 能够加载未安装的jar/apk/dex

    PathClassLoader: 只能加载系统中已经安装的apk

Android 9(Pie)及其以上版本中,两者已经非常相似,主要是因为PathClassLoader扩充了功能,除了已安装的apk或jar文件之外,也可以从指定路径加载.dex或.jar文件。实际使用中,如果需要加载未安装的.dex或.jar文件,DexClassLoader会更合适。

因为插件的apk没有安装在系统中,因此这里使用DexClassLoader加载插件的apk。

双亲委派模型

图片

Android虚拟机中类加载器的调用流程如下图所示:

图片

双亲委派模型就是,当一个类加载器加载某个类时,首先委托自己的父类加载器去加载,一直向上查找。若顶层类加载器(优先)或父类加载器能加载到该类,则返回这个类所对应的Class对象;若不能加载到,则最后再由发起请求的类加载器去加载该类。这种方式能够保证类的加载按照一定的规则次序进行,越是基础的类,越是被上层的类加载器加载,从而保证程序的安全性。

插件apk加载机制

由于ClassLoader的双亲委派模型,在加载类时,如果已经加载了插件中的类,那么就会直接使用插件中的类,而不会再加载宿主的类,反之对插件也是一样。这就很容易造成宿主和插件的类加载冲突,并且也可能会抛出异常-- java.lang.IllegalAccessError: Class ref in pre-verified class...。因此,在Android插件化时,需要自定义一个ClassLoader,修改类加载逻辑,使得插件和宿主中的类相互隔离,各自加载。

自定义ClassLoader

图片

这里自定义一个ClassLoader -- ApkClassLoader,用来处理插件apk的加载功能,代码如下所示:

public class ApkClassLoader extends DexClassLoader {    /**     * 当前ClassLoader的父类加载器,通过调用宿主的application.getClassLoader()获取     * */    private final ClassLoader parentClassLoader;     /**     * 包名白名单,若类的包名在白名单中,则直接从宿主的apk中加载该类。     * */    private final String[] whitePackageNames;     /**     * 加载so库时的 findLibrary() 方法名     * */    private static final String METHOD_FIND_LIBRARY = "findLibrary";    private Method findLibraryMethod;     /**     * @param dexPath            插件apk的路径     * @param optimizedDirectory 用于存储优化过的 dex 文件(即 ODEX 文件)的目录,这些ODEX文件是为了提高类加载时的效率而生成的     * @param libraryPath        保存插件so库的lib目录的路径,与插件apk在同一级目录     * @param classLoader        当前ClassLoader的父类加载器,通过调用宿主的application.getClassLoader()获取     * @param whitePackageNames  白名单     *     * */    public ApkClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader classLoader, String[] whitePackageNames) {        super(dexPath, optimizedDirectory, libraryPath, classLoader);         this.parentClassLoader = classLoader;        this.whitePackageNames = whitePackageNames;    }     @Override    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {        /*          获取类的包名          */        String packageName;        int dot = className.lastIndexOf('.');        if (dot != -1) {            packageName = className.substring(0, dot);        } else {            packageName = "";        }         /*          判断是不是白名单中的类          */        boolean isWhiteInterface = false;        for (String interfacePackageName : whitePackageNames) {            if (packageName.startsWith(interfacePackageName)) {                isWhiteInterface = true;                break;            }        }         if (isWhiteInterface) {            /*              白名单中的类直接从宿主apk中找              */            return super.loadClass(className, resolve);        } else {            /*              非白名单中的类,先从插件中找,找不到再从宿主中找              */            Class<?> clazz = findLoadedClass(className);            if (clazz == null) {                ClassNotFoundException suppressed = null;                try {                    clazz = findClass(className);                } catch (ClassNotFoundException e) {                    suppressed = e;                }                 if (clazz == null) {                    try {                        clazz = parentClassLoader.loadClass(className);                    } catch (ClassNotFoundException e) {                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {                            e.addSuppressed(suppressed);                        }                        throw e;                    }                }            }             return clazz;        }    }     /**     * 从插件apk中读取接口的具体实现类     *     * @param clazz     接口类     * @param className 实现类的类名     * @param <T>       接口类型     *     * @return          所需接口     * @throws Exception     */    public <T> T getInterface(Class<T> clazz, String className) throws Exception {        try {            Class<?> interfaceImplementClass = loadClass(className);            Object interfaceImplement = interfaceImplementClass.newInstance();            return clazz.cast(interfaceImplement);        } catch (ClassNotFoundException                 | InstantiationException                 | ClassCastException                 | IllegalAccessException e) {            throw new Exception(e);        }    }     /**     * 加载so库     *     * @param name so文件名     * */    @Override    public String findLibrary(String name) {        String path = super.findLibrary(name);        if (path != null) {            return path;        }         if (parentClassLoader instanceof BaseDexClassLoader) {            return ((BaseDexClassLoader) parentClassLoader).findLibrary(name);        }         try {            if (findLibraryMethod == null) {                findLibraryMethod = ClassLoader.class.getDeclaredMethod(METHOD_FIND_LIBRARY, String.class);                findLibraryMethod.setAccessible(true);            }             return (String) findLibraryMethod.invoke(parentClassLoader, name);        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {            e.printStackTrace();            return null;        }    }}


要点说明:

  • parentClassLoader:当前ClassLoader的父类加载器,通过调用宿主的application.getClassLoader()获取,可用于加载宿主中的类。

    whitePackageNames:包名白名单,若类的包名在白名单中,则直接从宿主的apk中加载该类。因为宿主和插件中的类,遵循同一套标准(如:同一套接口)时,宿主在使用时,就需要将插件中的类,转换为宿主中这套标准的类。根据“同一个类加载器加载且全类名相同的类才算同一个类”的机制,需要用父加载器加载的接口才可以进行类型转换。比如:在Activity插件化时,需要把插件Activity实现的接口IActivityInterface加入白名单,宿主加载插件Activity时,直接用宿主的ClassLoader加载IActivityInterface接口,然后把插件中的Activity转换为IActivityInterface类型的对象,这样才能调用插件中的Activity。

    白名单类加载:loadClass()中直接调用了super.loadClass(className, resolve),使用父类加载器加载白名单中的类,因为ApkClassLoader的父类是DexClassLoader,而系统在启动宿主apk时已经创建了DexClassLoader,所以DexClassLoader加载的白名单中的类就是宿主中的类,也就是说,super.loadClass(className, resolve)直接在宿主apk中加载白名单中的类。

    非白名单类加载:loadClass()加载非白名单类时,先从当前已加载的类中找;如果找不到,就使用 findClass()尝试查找;如果仍然找不到,就尝试在父类加载器(宿主的类加载器)中加载;如果父类加载器依然找不到,就抛异常ClassNotFoundException。

    so库加载:首先调用super.findLibrary(name),从父类中查找库文件,因为ApkClassLoader的构造方法中给父类传入了保存插件so库的lib目录的路径,所以这里会到这个路径中查找。如果找不到,且父类加载器(parentClassLoader)是 BaseDexClassLoader的实例,就会调用parentClassLoader的findLibrary(),从宿主中查找。如果parentClassLoader不是 BaseDexClassLoader的实例,就用反射机制调用 parentClassLoader 对象的 findLibrary() 来查找。

ClassLoader加载插件apk

图片

利用ApkClassLoader加载插件apk的代码如下所示:

// pluginApkFile是插件apk的File文件String libDirPath = new File(pluginApkFile.getParent(), "lib").getAbsolutePath();// 解析过滤插件apk中的so库SoLibUtil.releaseSoFile(application, pluginApkFile, libDirPath);// 存储优化过的 dex 文件的目录File optimizeFile = application.getDir("plugin-optimize", Context.MODE_PRIVATE);// 定义白名单String[] whiteInterfaces = new String[]{"com.xxx.yyy.standard"};// pluginPath是插件apk的绝对路径ApkClassLoader pluginClassLoader = new ApkClassLoader(pluginPath, optimizeFile.getAbsolutePath(), libDirPath, application.getClassLoader(), whiteInterfaces);

上述代码执行完毕,就可在pluginClassLoader中加载插件apk中的类。

小结

图片

插件apk的加载机制如下图所示:

图片
04

加载插件资源

图片
图片

加载插件资源的要点如下所示:

  • 自定义宿主Resources,手动创建宿主的资源类;

    自定义插件Resources,手动创建插件的资源类;

    自定义包含宿主和插件资源的MixResources,加载资源时,先从宿主中拿,拿不到再从插件中拿;

    插件通过getLayoutId()、getStringId()等获取资源ID时,先从插件中找,找不到再从宿主中找。

    实例化MixResources对象,供宿主或插件加载资源时使用。

整个流程如下图所示:

图片

自定义宿主Resources

直接使用系统创建的宿主Resources,主要存在以下问题:

  • 有时会因缓存的影响,出现一些难以处理的Bug;

    宿主Resources中没有插件的信息,无法直接用来获取插件的资源。

基于上述原因,这里需要自定义宿主的Resources,代码如下所示:

public class SuperHostResources {    private final Context context;     // 宿主的Context     private final Resources resources; // 宿主的Resources     private Integer pluginApkCookie;   // 插件资源路径优先级     /**     * @param context    宿主的Context     * @param pluginPath 插件apk的绝对路径     * */    public SuperHostResources(Context context, String pluginPath) {        this.context = context;        resources = buildHostResources(pluginPath);    }     private Resources buildHostResources(String pluginPath) {        Resources hostResources;        try {            /*              通过反射把插件路径设置给AssetManager,从而让宿主能调用插件中的资源              */            AssetManager assetManager = context.getResources().getAssets();            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);            addAssetPathMethod.setAccessible(true);            // pluginApkCookie是资源路径的优先级,用于确定在存在多个库具有相同资源ID时,应优先从哪个库中获取资源。            // pluginApkCookie的值越大,说明该路径的资源优先级越高,越先查找。            // 例如,如果添加了两个路径"A"和"B",并且它们分别返回了1和2。当尝试通过资源ID访问一个资源时,系统首先会检查路径"B"看是否存在对应的资源,            // 如果存在,就会使用"路径B"的资源;否则,就会查看"路径A",获取对应的资源。            pluginApkCookie = (Integer) addAssetPathMethod.invoke(assetManager, pluginPath);             /*              手动创建宿主的Resources对象,直接使用系统创建的宿主资源的话,可能会因缓存影响而出现一些难以处理的Bug。              第一个参数:资源管理器,用于访问系统或程序的原始资源文件,如:图像、音频文件等。              第二个参数:屏幕显示度量信息,包含屏幕大小、密度和字体等,通常用于适应不同的屏幕分辨率和尺寸。              第三个参数:描述设备当前的配置,包含设备的语言环境、屏幕方向等,可用于检测一些设备设置的改变,如:屏幕尺寸、用户语言等。              */            hostResources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());        } catch (Exception e) {            e.printStackTrace();            hostResources = context.getResources();        }         return hostResources;    }     public Resources get() {        return resources;    }     /**     * 解析插件 apk 的 AndroidManifest.xml 时会用到     * */    public Integer getPluginApkCookie() {        return pluginApkCookie;    }}


自定义插件Resources

插件Resources的代码如下所示:

public class PluginResources {    private final Context context;     // 宿主的Context    private final Resources resources; // 插件apk的Resources    private final String pluginPath;   // 插件apk的绝对路径    private String pluginPackageName;  // 插件apk的包名     /**     * @param context    宿主Context     * @param pluginPath 插件apk的绝对路径     * */    public PluginResources(Context context, String pluginPath) {        this.context = context;        this.pluginPath = pluginPath;         resources = build();    }     public String getPackageName() {        return pluginPackageName;    }     public Resources get() {        return resources;    }     private Resources build() {        try {            /*              获取宿主的PackageInfo              */            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),                    PackageManager.GET_ACTIVITIES                            | PackageManager.GET_META_DATA                            | PackageManager.GET_SERVICES                            | PackageManager.GET_PROVIDERS                            | PackageManager.GET_SIGNATURES);            String hostPublicSourceDir = packageInfo.applicationInfo.publicSourceDir;            String hostSourceDir = packageInfo.applicationInfo.sourceDir;                         /*              把宿主PackageInfo中的publicSourceDir和sourceDir都赋值为插件apk的绝对路径。              publicSourceDir和sourceDir都是应用主apk的完整路径,在某些情况下,一个应用可能会有私有的apk文件,              此时sourceDir会指向这个私有的apk文件,而publicSourceDir则仍然指向主apk文件。              */            packageInfo.applicationInfo.publicSourceDir = pluginPath;            packageInfo.applicationInfo.sourceDir = pluginPath;             /*              获取插件的PackageInfo              */            PackageInfo pluginInfo = context.getPackageManager().getPackageArchiveInfo(pluginPath, PackageManager.GET_ACTIVITIES);            pluginPackageName = pluginInfo.packageName; // 插件apk的包名             /*              获取插件的Resources              */            Resources pluginResources = context.getPackageManager().getResourcesForApplication(packageInfo.applicationInfo);                         /*              复原宿主PackageInfo中的publicSourceDir和sourceDir              */            packageInfo.applicationInfo.publicSourceDir = hostPublicSourceDir;            packageInfo.applicationInfo.sourceDir = hostSourceDir;             return pluginResources;        } catch (PackageManager.NameNotFoundException e) {            e.printStackTrace();        }         return null;    }}


自定义MixResources

前面已经创建了宿主和插件的Resources,这里需要创建一个混合的Resources(MixResources),同时持有宿主和插件的Resources对象,获取资源时,先从宿主Resources中获取,获取不到再从插件Resources中获取。

创建MixResources的关键步骤如下:

  • 创建ResourcesWrapper,继承系统的Resources,持有宿主的Resources,并且重写获取资源的方法(比如:getText()、getString()、getDimension()等),直接从宿主Resources中获取资源。

    创建MixResources,继承ResourcesWrapper,持有宿主和插件的Resources,并且重写获取资源的方法,优先调用ResourcesWrapper中对应的方法,从宿主中获取资源,获取失败时,再从插件中获取。

ResourcesWrapper

图片

ResourcesWrapper的代码如下所示:

public class ResourcesWrapper extends Resources {     private final Resources hostResources; // 宿主的Resources     public ResourcesWrapper(Resources hostResources) {        super(hostResources.getAssets(), hostResources.getDisplayMetrics(), hostResources.getConfiguration());        this.hostResources = hostResources;    }     @Override    public CharSequence getText(int id) throws NotFoundException {        // 从宿主中获取指定id的text        return hostResources.getText(id);    }     @TargetApi(Build.VERSION_CODES.O)    @Override    public Typeface getFont(int id) throws NotFoundException {        // 从宿主中获取指定id的font        return hostResources.getFont(id);    }     @Override    public CharSequence getQuantityText(int id, int quantity) throws NotFoundException {        return hostResources.getQuantityText(id, quantity);    }     @Override    public String getString(int id) throws NotFoundException {        return hostResources.getString(id);    }     @Override    public String getString(int id, Object... formatArgs) throws NotFoundException {        return hostResources.getString(id, formatArgs);    }     @Override    public String getQuantityString(int id, int quantity, Object... formatArgs) throws NotFoundException {        return hostResources.getQuantityString(id, quantity, formatArgs);    }     @Override    public String getQuantityString(int id, int quantity) throws NotFoundException {        return hostResources.getQuantityString(id, quantity);    }     @Override    public CharSequence getText(int id, CharSequence def) {        return hostResources.getText(id, def);    }     @Override    public CharSequence[] getTextArray(int id) throws NotFoundException {        return hostResources.getTextArray(id);    }     @Override    public String[] getStringArray(int id) throws NotFoundException {        return hostResources.getStringArray(id);    }     @Override    public int[] getIntArray(int id) throws NotFoundException {        return hostResources.getIntArray(id);    }     @Override    public TypedArray obtainTypedArray(int id) throws NotFoundException {        return hostResources.obtainTypedArray(id);    }     @Override    public float getDimension(int id) throws NotFoundException {        return hostResources.getDimension(id);    }     @Override    public int getDimensionPixelOffset(int id) throws NotFoundException {        return hostResources.getDimensionPixelOffset(id);    }     @Override    public int getDimensionPixelSize(int id) throws NotFoundException {        return hostResources.getDimensionPixelSize(id);    }     @Override    public float getFraction(int id, int base, int pbase) {        return this.hostResources.getFraction(id, base, pbase);    }     @Override    public Drawable getDrawable(int id) throws NotFoundException {        return hostResources.getDrawable(id);    }     @TargetApi(Build.VERSION_CODES.LOLLIPOP)    @Override    public Drawable getDrawable(int id, Theme theme) throws NotFoundException {        return hostResources.getDrawable(id, theme);    }     @Override    public Drawable getDrawableForDensity(int id, int density) throws NotFoundException {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {            return hostResources.getDrawableForDensity(id, density);        } else {            return null;        }    }     @TargetApi(Build.VERSION_CODES.LOLLIPOP)    @Override    public Drawable getDrawableForDensity(int id, int density, Theme theme) {        return hostResources.getDrawableForDensity(id, density, theme);    }     @Override    public Movie getMovie(int id) throws NotFoundException {        return hostResources.getMovie(id);    }     @Override    public int getColor(int id) throws NotFoundException {        return hostResources.getColor(id);    }     @TargetApi(Build.VERSION_CODES.M)    @Override    public int getColor(int id, Theme theme) throws NotFoundException {        return hostResources.getColor(id, theme);    }     @Override    public ColorStateList getColorStateList(int id) throws NotFoundException {        return hostResources.getColorStateList(id);    }     @TargetApi(Build.VERSION_CODES.M)    @Override    public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException {        return hostResources.getColorStateList(id, theme);    }     @Override    public boolean getBoolean(int id) throws NotFoundException {        return hostResources.getBoolean(id);    }     @Override    public int getInteger(int id) throws NotFoundException {        return hostResources.getInteger(id);    }     @Override    public XmlResourceParser getLayout(int id) throws NotFoundException {        return hostResources.getLayout(id);    }     @Override    public XmlResourceParser getAnimation(int id) throws NotFoundException {        return hostResources.getAnimation(id);    }     @Override    public XmlResourceParser getXml(int id) throws NotFoundException {        return hostResources.getXml(id);    }     @Override    public InputStream openRawResource(int id) throws NotFoundException {        return hostResources.openRawResource(id);    }     @Override    public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {        return hostResources.openRawResource(id, value);    }     @Override    public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {        return hostResources.openRawResourceFd(id);    }     @Override    public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException {        hostResources.getValue(id, outValue, resolveRefs);    }     @Override    public void getValueForDensity(int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {            hostResources.getValueForDensity(id, density, outValue, resolveRefs);        }    }     @Override    public void getValue(String name, TypedValue outValue, boolean resolveRefs) throws NotFoundException {        hostResources.getValue(name, outValue, resolveRefs);    }     @Override    public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {        return hostResources.obtainAttributes(set, attrs);    }     @Override    public DisplayMetrics getDisplayMetrics() {        return hostResources.getDisplayMetrics();    }     @Override    public Configuration getConfiguration() {        return hostResources.getConfiguration();    }     @Override    public int getIdentifier(String name, String defType, String defPackage) {        return hostResources.getIdentifier(name, defType, defPackage);    }     @Override    public String getResourceName(int resid) throws NotFoundException {        return hostResources.getResourceName(resid);    }     @Override    public String getResourcePackageName(int resid) throws NotFoundException {        return hostResources.getResourcePackageName(resid);    }     @Override    public String getResourceTypeName(int resid) throws NotFoundException {        return hostResources.getResourceTypeName(resid);    }     @Override    public String getResourceEntryName(int resid) throws NotFoundException {        return hostResources.getResourceEntryName(resid);    }     @Override    public void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) throws XmlPullParserException, IOException {        hostResources.parseBundleExtras(parser, outBundle);    }     @Override    public void parseBundleExtra(String tagName, AttributeSet attrs, Bundle outBundle) throws XmlPullParserException {        hostResources.parseBundleExtra(tagName, attrs, outBundle);    }}


MixResources

图片

MixResources的代码如下所示:

public class MixResources extends ResourcesWrapper {     private final Resources pluginResources; // 插件Resources     private final String pluginPackageName;  // 插件apk的包名     /**     * @param hostResources 宿主Resources     * @param context       宿主Context     * @param pluginPath    插件apk的绝对路径     * */    public MixResources(Resources hostResources, Context context, String pluginPath) {         // 把宿主 Resources 传给父类 ResourcesWrapper        super(hostResources);         // 获取插件 Resources        PluginResources pluginResourcesBuilder = new PluginResources(context, pluginPath);        pluginResources = pluginResourcesBuilder.get();        pluginPackageName = pluginResourcesBuilder.getPackageName();    }     @Override    public CharSequence getText(int id) throws Resources.NotFoundException {        try {            // 从宿主中获取text            return super.getText(id);        } catch (Resources.NotFoundException e) {            // 宿主中获取失败时,再从插件中获取            return pluginResources.getText(id);        }    }     @Override    public String getString(int id) throws Resources.NotFoundException {        try {            // 从宿主中获取String            return super.getString(id);        } catch (Resources.NotFoundException e) {            // 宿主中获取失败时,再从插件中获取            return pluginResources.getString(id);        }    }     @Override    public String getString(int id, Object... formatArgs) throws Resources.NotFoundException {        try {            return super.getString(id, formatArgs);        } catch (Resources.NotFoundException e) {            return pluginResources.getString(id, formatArgs);        }    }     @Override    public float getDimension(int id) throws Resources.NotFoundException {        try {            return super.getDimension(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getDimension(id);        }    }     @Override    public int getDimensionPixelOffset(int id) throws Resources.NotFoundException {        try {            return super.getDimensionPixelOffset(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getDimensionPixelOffset(id);        }    }     @Override    public int getDimensionPixelSize(int id) throws Resources.NotFoundException {        try {            return super.getDimensionPixelSize(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getDimensionPixelSize(id);        }    }     @Override    public Drawable getDrawable(int id) throws Resources.NotFoundException {        try {            return super.getDrawable(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getDrawable(id);        }    }     @TargetApi(Build.VERSION_CODES.LOLLIPOP)    @Override    public Drawable getDrawable(int id, Resources.Theme theme) throws Resources.NotFoundException {        try {            return super.getDrawable(id, theme);        } catch (Resources.NotFoundException e) {            return pluginResources.getDrawable(id, theme);        }    }     @Override    public Drawable getDrawableForDensity(int id, int density) throws Resources.NotFoundException {        try {            return super.getDrawableForDensity(id, density);        } catch (Resources.NotFoundException e) {            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {                return pluginResources.getDrawableForDensity(id, density);            } else {                return null;            }        }    }     @TargetApi(Build.VERSION_CODES.LOLLIPOP)    @Override    public Drawable getDrawableForDensity(int id, int density, Resources.Theme theme) {        try {            return super.getDrawableForDensity(id, density, theme);        } catch (Exception e) {            return pluginResources.getDrawableForDensity(id, density, theme);        }    }     @Override    public int getColor(int id) throws Resources.NotFoundException {        try {            return super.getColor(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getColor(id);        }    }     @TargetApi(Build.VERSION_CODES.M)    @Override    public int getColor(int id, Resources.Theme theme) throws Resources.NotFoundException {        try {            return super.getColor(id, theme);        } catch (Resources.NotFoundException e) {            return pluginResources.getColor(id, theme);        }    }     @Override    public ColorStateList getColorStateList(int id) throws Resources.NotFoundException {        try {            return super.getColorStateList(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getColorStateList(id);        }    }     @TargetApi(Build.VERSION_CODES.M)    @Override    public ColorStateList getColorStateList(int id, Resources.Theme theme) throws Resources.NotFoundException {        try {            return super.getColorStateList(id, theme);        } catch (Resources.NotFoundException e) {            return pluginResources.getColorStateList(id, theme);        }    }     @Override    public boolean getBoolean(int id) throws Resources.NotFoundException {        try {            return super.getBoolean(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getBoolean(id);        }    }     @Override    public XmlResourceParser getLayout(int id) throws Resources.NotFoundException {        try {            return super.getLayout(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getLayout(id);        }    }     @Override    public String getResourceName(int resid) throws Resources.NotFoundException {        try {            return super.getResourceName(resid);        } catch (Resources.NotFoundException e) {            return pluginResources.getResourceName(resid);        }    }     @Override    public int getInteger(int id) throws Resources.NotFoundException {        try {            return super.getInteger(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getInteger(id);        }    }     @Override    public CharSequence getText(int id, CharSequence def) {        try {            return super.getText(id, def);        } catch (Resources.NotFoundException e) {            return pluginResources.getText(id, def);        }    }     @Override    public InputStream openRawResource(int id) throws Resources.NotFoundException {        try {            return super.openRawResource(id);        } catch (Resources.NotFoundException e) {            return pluginResources.openRawResource(id);        }    }     @Override    public XmlResourceParser getXml(int id) throws Resources.NotFoundException {        try {            return super.getXml(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getXml(id);        }    }     @TargetApi(Build.VERSION_CODES.O)    @Override    public Typeface getFont(int id) throws Resources.NotFoundException {        try {            return super.getFont(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getFont(id);        }    }     @Override    public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws Resources.NotFoundException {        try {            super.getValue(id, outValue, resolveRefs);        } catch (Resources.NotFoundException e) {            pluginResources.getValue(id, outValue, resolveRefs);        }    }     @Override    public Movie getMovie(int id) throws Resources.NotFoundException {        try {            return super.getMovie(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getMovie(id);        }    }     @Override    public XmlResourceParser getAnimation(int id) throws Resources.NotFoundException {        try {            return super.getAnimation(id);        } catch (Resources.NotFoundException e) {            return pluginResources.getAnimation(id);        }    }     @Override    public InputStream openRawResource(int id, TypedValue value) throws Resources.NotFoundException {        try {            return super.openRawResource(id, value);        } catch (Resources.NotFoundException e) {            return pluginResources.openRawResource(id, value);        }    }     @Override    public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {        try {            return super.openRawResourceFd(id);        } catch (Resources.NotFoundException e) {            return pluginResources.openRawResourceFd(id);        }    }     @Override    public int getIdentifier(String name, String defType, String defPackage) {        int pluginId = super.getIdentifier(name, defType, defPackage);        if (pluginId <= 0) {            return pluginResources.getIdentifier(name, defType, pluginPackageName);        }        return pluginId;    }     /**     * 从插件中获取资源ID,注意:插件中会通过反射调用该方法,所以千万不要删!     * */    public int getIdentifierFromPlugin(String name, String defType) {        return pluginResources.getIdentifier(name, defType, pluginPackageName);    }}


插件获取资源ID

插件apk中,有时会在代码使用getLayoutId()、getStringId()、getDrawableId()等方法动态获取插件中的资源ID。按照MixResources中加载资源的逻辑,这些资源ID会优先到宿主中找,找不到时才到插件中找。如果插件和宿主中存在同名的资源,那么插件在获取资源ID时,就会从宿主中找,最终返回的是宿主中的资源ID,而不是插件中的资源ID,这就会出现资源获取错误的问题。

为了解决这个问题,插件apk中使用上述方法动态获取插件中的资源ID时,需要先从插件中获取,获取失败时,再从宿主中获取。

这里以插件获取layoutId为例,介绍具体实现代码。

  • layoutId()代码如下:

private static final String TYPE_LAYOUT = "layout";private static final int ID_DEFAULT = -1;public static int getLayoutId(Context paramContext, String paramString) {    return getIdentifier(paramContext, paramString, TYPE_LAYOUT);}


  • getIdentifier()代码如下:

private static int getIdentifier(Context context, String name, String type) {    int id = ID_DEFAULT;     try {        // 先从插件中获取资源ID        id = getIdentifierFromPlugin(context, name, type);         if (id == ID_DEFAULT) {            // 从插件中拿不到时,再从宿主中拿            id = context.getResources().getIdentifier(name, type, context.getPackageName());        }    } catch (Exception e) {        e.printStackTrace();    }     return id;}


  • getIdentifierFromPlugin()代码如下:

/** * 从插件中获取资源ID * * @param context 上下文 * @param name    资源名称 * @param type    资源类型 */private static int getIdentifierFromPlugin(Context context, String name, String type) {     try {        /*          因为插件和宿主是两个独立的apk,所以这里使用 ClassLoader 加载宿主中的 MixResources          */        if (mixResource == null) {            mixResource = context.getClassLoader().loadClass("MixResources的全类名");        }         if (mixResource == null) {            return ID_DEFAULT;        }         /*          通过反射调用 MixResources 中的 getIdentifierFromPlugin() 方法,从而从插件中获取资源 ID。          */        Method method = mixResource.getMethod("getIdentifierFromPlugin", String.class, String.class);        return (int) method.invoke(context.getResources(), name, type);    } catch (Exception e) {        e.printStackTrace();        return ID_DEFAULT;    }}


实例化MixResources对象

前面介绍了宿主和插件资源的自定义方法,在加载插件apk时,还需要实例化这些资源的对象,代码如下:

SuperHostResources superHostResources = new SuperHostResources(application, pluginPath);Resources resources = new MixResources(superHostResources.get(), application, pluginPath);


05

缓存插件信息

图片
图片

宿主APP启动后,要先加载插件的so库、ClassLoader和资源等,并缓存在内存中,需要时直接从内存中调用。前面介绍了加载方法,这里主要介绍一下如何缓存。

缓存插件信息的主要步骤如下:

  • 自定义保存插件信息的类Plugin;

    创建单例类PluginManager,持有Plugin对象,并设置插件信息。

自定义Plugin类

Plugin的代码如下所示:

public class Plugin {     private ApkClassLoader classLoader;  // 插件的 ClassLoader    private Resources resource;          // 实际持有的是 MixResources 对象     public ClassLoader getClassLoader() {        return classLoader;    }     public void setClassLoader(ApkClassLoader classLoader) {        this.classLoader = classLoader;    }     public Resources getResource() {        return resource;    }     public void setResources(Resources resources) {        resource = resources;    }}


初始化Plugin对象并缓存

创建一个单例类PluginManager,实例化Plugin对象,并设置插件的信息,需要时直接使用即可。代码如下所示:

public class PluginManager {         private static PluginManager pluginManager;    private Plugin plugin;         public static PluginManager getInstance() {        if (pluginManager == null) {            synchronized (PluginManager.class) {                if (pluginManager == null) {                    pluginManager = new PluginManager();                }            }        }         return pluginManager;    }         private PluginManager() {     }         public Plugin getPlugin() {        return plugin;    }     /**     * 加载插件 APK,初始化 Plugin     *     * @param application 宿主 Application     * @param pluginPath  插件 APK 的绝对路径     */    public void loadPlugin(Application application, String pluginPath) {        /*          实例化 Plugin          */        plugin = new Plugin();         /*          配置插件 ClassLoader          */        File pluginApkFile = new File(pluginPath);        String libDirPath = new File(pluginApkFile.getParent(), "lib").getAbsolutePath();        // 解析过滤插件apk中的so库        SoLibUtil.releaseSoFile(application, pluginApkFile, libDirPath);        // 存储优化过的 dex 文件的目录        File optimizeFile = application.getDir("plugin-optimize", Context.MODE_PRIVATE);        // 创建白名单        String[] whiteInterfaces = new String[]{"com.xxx.yyy.standard"};        // 根据插件 APK,创建插件 ClassLoader        ApkClassLoader pluginClassLoader = new ApkClassLoader(pluginPath, optimizeFile.getAbsolutePath(), libDirPath, application.getClassLoader(), whiteInterfaces);        plugin.setClassLoader(pluginClassLoader);         /*          配置 Resources          */        SuperHostResources superHostResources = new SuperHostResources(application, pluginPath);        Resources resources = new MixResources(superHostResources.get(), application, pluginPath);        plugin.setResources(resources);    }    // ......}

注意:因为宿主Application或启动页Activity中都可能使用插件的资源,并且宿主的Context在初始化完毕后会有缓存,这可能会影响插件资源的使用,所以插件apk的加载时机要尽可能早,建议在宿主Application的attachBaseContext()中就加载插件apk,即:调用PluginManager的loadPlugin()方法。

06

总结

图片
图片

宿主和插件是两个独立的apk,要在宿主中使用插件的功能,就必须先加载插件apk,建议在宿主Application的attachBaseContext()方法中就加载插件apk。

加载插件apk的主要流程如下:


  • 解析过滤插件so库,关键步骤如下:

    • 获取插件apk的完整路径;

    • 获取系统支持的宿主apk的CPU架构;

    • 根据宿主apk的CPU架构,获取插件最终要适配的CPU架构;

    • 根据要适配的CPU架构,过滤插件apk的so库并保存到lib目录。


    详见下图:

图片

  • 加载插件apk,生成DexClassLoader,关键步骤如下:

    • 自定义ApkClassLoader,继承DexClassLoader,并重写loadClass():白名单中的类直接从宿主中找,非白名单中的类,先从插件中找,找不到再从宿主中找,

    • 传入插件apk的路径、保存so库的lib路径、宿主的ClassLoader等,创建ApkClassLoader对象。


    详见下图:

    图片


  • 加载插件资源,生成含有宿主和插件资源的MixResources,关键步骤如下:

    图片


    • 自定义宿主Resources,手动创建宿主的资源类;

    • 自定义插件Resources,手动创建插件的资源类;

    • 自定义包含宿主和插件资源的MixResources,加载资源时,先从宿主中找,找不到再从插件中找;

    • 插件通过getLayoutId()、getStringId()等获取资源ID时,先从插件中找,找不到再从宿主中找。

    • 实例化MixResources对象,供宿主或插件加载资源时使用。



  • 缓存插件信息,需要时直接调用,关键步骤如下:


    • 自定义保存插件信息的Plugin类;

    • 创建单例类PluginManager,持有Plugin对象,并设置插件信息。


三七互娱技术团队

扫码关注 了解更多

图片


修改于2024年03月04日
继续滑动看下一个
三七互娱技术团队
向上滑动看下一个