Android插件化开发实战

4,947 阅读18分钟

1.什么是插件化

插件化的概念

插件化技术初源于免安装运行 apk 的想法,这个免安装的 apk 就可以理解为插件,而支持插件的 app 我们一般叫宿主。宿主可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,一方面减小 了安装包的大小,另一方面可以实现 app 功能的动态扩展。

插件化解决的问题

  1. APP的功能模块越来越多,体积越来越大
  2. 模块之间的耦合度高,协同开发沟通成本越来越大
  3. 方法数目可能超过65535,APP占用的内存过大
  4. 应用之间的互相调用

插件化与组件化的区别

组件化开发就是将一个app分成多个模块,每个模块都是一个组件,开发的 过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。

插件化开发和组件化略有不同,插件化开发是将整个app拆分成多个模块, 这些模块包括一个宿主和多个插件,每个模块都是一个apk,最终打包的时 候宿主apk和插件apk分开打包

插件化的开源框架

我们在选择开源框架的时候,需要根据自身的需求来,如果加载的插件不需要和宿主有任何耦合,也无须和宿主进 行通信,比如加载第三方 App,那么推荐使用 RePlugin,其他的情况推荐使用 VirtualApk。

插件化实现思路

我们如何去实现一个插件化呢?

首先我们要知道,插件apk是没有安装的,那我们怎么加载它呢?不知道。。。 没关系,这儿我们还可以细分下,一个 apk 主要就是由代码和资源组成,所以上面的问题我们可以变为:如何加载 插件的类?如何加载插件的资源?这样的话是不是就有眉目了。

然后我们还需要解决类的调用的问题,这个地方主要是四大组件的调用问题。我们都知道,四大组件是需要注册 的,而插件的四大组件显然没有注册,那我们怎么去调用呢?

所以我们接下来就是解决这三个问题,从而实现插件化

  1. 如何加载插件的类?
  2. 如何启动插件的四大组件?
  3. 如何加载插件的资源?

2.如何调用插件中的类

类加载(ClassLoader)

我们在学 java 的时候知道,java 源码文件编译后会生成一个 class 文件,而在 Android 中,将代码编译后会生成 一个 apk 文件,将 apk 文件解压后就可以看到其中有一个或多个 classes.dex 文件,它就是安卓把所有 class 文件 进行合并,优化后生成的。

java 中 JVM 加载的是 class 文件,而安卓中 DVM 和 ART 加载的是 dex 文件,虽然二者都是用的 ClassLoader 加 载的,但因为加载的文件类型不同,还是有些区别的,所以接下来我们主要介绍安卓的 ClassLoader 是如何加载 dex 文件的。

在加载阶段,虚拟机主要完成三件事:   1.通过一个类的全限定名来获取定义此类的二进制字节流。   2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。   3.在Java堆中生成一个代表这个类的Class对象, 作为方法区域数据的访问入口

反射

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法和属性。

ClassLoader的实现类

ClassLoader是一个抽象类,实现类主要分为两种类型:系统类加载器和自定义加载器。 其中系统类加载器主要包括三种:

  • BootClassLoader :用于加载Android Framework层class文件。
  • PathClassLoader :用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
  • DexClassLoader :用于加载指定的dex,以及jar、zip、apk中的classes.dex 类继承关系如下图:

PathClassLoader 和 DexClassLoader

    // libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java 
    public class PathClassLoader extends BaseDexClassLoader {
        // optimizedDirectory 直接为 null   
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }


        // optimizedDirectory 直接为 null    
        public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
            super(dexPath, null, librarySearchPath, parent);
        }
    }

    // API 小于等于 26/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java 
    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) {
            // 26开始,super里面改变了,看下面两个构造方法        
            super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
        }
    }

    // API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java 
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(parent);
        // DexPathList 的第四个参数是 optimizedDirectory,可以看到这儿为 null    
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
    }

    // API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java 
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }

根据源码了解到,PathClassLoader 和 DexClassLoader 都是继承自 BaseDexClassLoader,且类中只有构造方 法,它们的类加载逻辑完全写在 BaseDexClassLoader 中。

其中我们值的注意的是,在8.0之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是 生成的 odex(优化的dex)存放的路径,PathClassLoader 直接为null,而 DexClassLoader 是使用用户传进来的 路径,而在8.0之后,二者就完全一样了。

BootClassLoader 和 PathClassLoader 之间的关系

ClassLoader classLoader = getClassLoader(); 
while (classLoader != null) {    
    Log.e("leo", "classLoader:" + classLoader);    
    classLoader = classLoader.getParent();
} 
Log.e("leo", "classLoader:" + Activity.class.getClassLoader());

上面的代码的打印结果

classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file 
"/data/user/0/com.enjoy.pluginactivity/cache/plugin-debug.apk", zip file 
"/data/app/com.enjoy.pluginactivity-T4YwTh8gHWWDDS19IkHRg==/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.pluginactivityT4YwTh-8gHWWDDS19IkHRg==/lib/x86_64, /system/lib64, /vendor/lib64]]] classLoader:java.lang.BootClassLoader@a26e88d 
classLoader:java.lang.BootClassLoader@a26e88d

通过打印结果可知,应用程序类是由 PathClassLoader 加载的,Activity 类是 BootClassLoader 加载的,并且 BootClassLoader 是 PathClassLoader 的 parent,这里要注意 parent 与父类的区别。这个打印结果我们下面还会提到。

加载原理

dex文件生成命令

dx --dex --output=output.dex input.clas

那我们如何使用类加载器去加载一个类呢? 非常的简单,例如:我们有一个apk文件,路径是 apkPath,然后里面有个类 com.enjoy.plugin.Test,那么我们可以通过如下方式去加载 Test 类:

DexClassLoader dexClassLoader = new DexClassLoader(dexPath,context.getCacheDir().getAbsolutePath(),null, context.getClassLoader()); 
Class<?> clazz = dexClassLoader.loadClass("com.enjoy.plugin.Test");

因为我们需要将插件的 dex 文件加载到宿主里面,所以我们接下来分析源码,看 DexClassLoader 类加载器到底是怎么加载一个 apk 的 dex 文件的。

通过查找发现,DexClassLoader 类中没有 loadClass 方法,一路向上查找,后在 ClassLoader 类中找到了该方 法,源码如下:(后续源码如无标明,都是 API 26 Android 8.0)

// /libcore/ojluni/src/main/java/java/lang/ClassLoader.java
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 检测这个类是否已经被加载  --> 1
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // 如果parent不为null,则调用parent的loadClass进行加载
                    c = parent.loadClass(name, false);
                } else {                // 正常情况下不会走这儿,因为 BootClassLoader 重写了 loadClass 方法,结束了递归
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {

            }

            if (c == null) {            // 如果仍然找不到,就调用 findClass 去查找 --> 2
                c = findClass(name);
            }
        }
        return c;
    }

    // -->1 检测这个类是否已经被加载
    protected final Class<?> findLoadedClass(String name) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;    // 后通过 native 方法实现查找
        return VMClassLoader.findLoadedClass(loader, name);
    }

    // -->2 加载器一般都会重写这个方法,定义自己的加载规则 
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

    // /libcore/libart/src/main/java/java/lang/VMClassLoader.java 
    native static Class findLoadedClass(ClassLoader cl, String name);

首先检测这个类是否已经被加载了,如果已经加载了,直接获取并返回。如果没有被加载,parent 不为 null,则调用parent的loadClass进行加载,依次递归,如果找到了或者加载了就返回,如果即没找到也加载不了,才自己去加载。这个过程就是我们常说的双亲委托机制

根据前面的打印结果可以知道,BootClassLoader是后一个加载器,所以我们来看下它是如何结束向上递归查找的。

  class BootClassLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            return Class.classForName(name, false, null);
        }

        @Override
        protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
            Class<?> clazz = findLoadedClass(className);

            if (clazz == null) {
                clazz = findClass(className);
            }

            return clazz;
        }
    }

我们发现 BootClassLoader 重写了 findClass 和 loadClass 方法,并且在 loadClass 方法中,不再获取 parent, 从而结束了递归。

接着我们再来看下,在所有 parent 都没加载成功的情况下,DexClassLoader 是如何加载的。通过查找我们发现 在它的父类 BaseDexClassLoader 中,重写了 findClass 方法。

 // /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {    // 在 pathList 中查找指定的 Class
        Class c = pathList.findClass(name, suppressedExceptions);
        return c;
    }

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(parent);    // 初始化 pathList
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
    }

接着再来看 DexPathList 类中的 findClass 方法。

 private Element[] dexElements;

    public Class<?> findClass(String name, List<Throwable> suppressed) {    //通过 Element 获取 Class 对象    
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
        return null;
    }

每一个Element就对应一个dex文件,因为dex文件有多个,所以使用的Element[]表示

完整流程图

实现步骤

我们发现 Class 对象就是从 Element 中获得的,而每一个 Element 就对应一个 dex 文件,因为我们的 dex 文件可 能有多个,所以这儿使用数组 Element[]。到这儿我们的思路就出来了,分为以下几步:

  1. 创建插件的 DexClassLoader 类加载器,然后通过反射获取插件的 dexElements 值。
  2. 获取宿主的 PathClassLoader 类加载器,然后通过反射获取宿主的 dexElements 值。
  3. 合并宿主的 dexElements 与 插件的 dexElements,生成新的 Element[]。
  4. 后通过反射将新的 Element[] 赋值给宿主的 dexElements 。

具体代码如下

public static void loadClass(Context context) {
        try {        
            // 获取 pathList 的字段
            Class baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            
            // 获取 dexElements 字段
            Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            
            /**
             *  获取插件的 dexElements[]
             */
            // 获取 DexClassLoader 类中的属性 pathList 的值
            DexClassLoader dexClassLoader = new DexClassLoader(apkPath, context.getCacheDir().getAbsolutePath(), null, context.getClassLoader());
            Object pluginPathList = pathListField.get(dexClassLoader);

            // 获取 pathList 中的属性 dexElements[] 的值--- 插件的 dexElements[]        
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

            /**
             *获取宿主的 dexElements[]
             */
            // 获取 PathClassLoader 类中的属性 pathList 的值        
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Object hostPathList = pathListField.get(pathClassLoader);

            // 获取 pathList 中的属性 dexElements[] 的值--- 宿主的 dexElements[]        
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

            /**
             * 将插件的 dexElements[] 和宿主的 dexElements[] 合并为一个新的 dexElements[]
             */
            // 创建一个新的空数组,第一个参数是数组的类型,第二个参数是数组的长度        
            Object[] dexElements = (Object[]) Array.newInstance(
                    hostDexElements.getClass().getComponentType(),
                    pluginDexElements.length + hostDexElements.length);

            // 将插件和宿主的 dexElements[] 的值放入新的数组中        
            System.arraycopy(hostDexElements, 0, dexElements, 0, hostDexElements.length);
            System.arraycopy(pluginDexElements, 0, dexElements, hostDexElements.length, pluginDexElements.length);
            
            /**
            * 将生成的新值赋给 "dexElements" 属性
            */
            hostDexElementsField.set(hostPathList, dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3.如何启动插件中的四大组件

实现思路

通过 动态代理反射 实现 Hook

Hook

Activity是需要在清单文件中注册的,显然,插件的 Activity 没有在宿主的清单文件中注册,那我们如何来启动它 呢? 这里我们就需要使用 Hook 技术,来绕开系统的检测。可能有些同学不知道 Hook 是什么,所以我们先简单的介绍 下 Hook 技术

Hook 是什么?

Hook 中文意思就是 钩子。简单说,它的作用就是改变代码的正常执行流程。

正常情况下对象A调用对象B,对象B处理后将数据返回给对象A,如下图:

而加入Hook后的流程就变成了下图形式:

Hook可以是一个方法或者一个对象,它就像一个钩子一样挂在对象B上面,当对象A调用对象B之前,Hook可以做 一些处理,起到“欺上瞒下”的作用。而对象B就是我们常说的Hook点。为了保证Hook的稳定性,Hook点一般选择 容易找到并且不易变化的对象,如静态变量和单例。

那么思路就来了,首先我们在宿主里面创建一个 ProxyActivity 继承自 Activity,并且在清单中注册。当启动插件 Activity 的时候,在系统检测前,找到一个Hook点,然后通过 Hook 将插件 Activity 替换成 ProxyActivity,等到检 测完了后,再找一个Hook点,使用 Hook 将它们换回来,这样就实现了插件 Activity 的启动。思路是不是非常的 简单。

如何查找 Hook 点呢?这就需要我们了解 Activity 的启动流程了。

Activity启动流程

首先我们来看一张 Activity 启动流程的简单示意图,如下:

通过这张图我们可以确定 Hook 点的大致位置。

  1. 在进入 AMS 之前,找到一个 Hook 点,用来将插件 Activity 替换为 ProxyActivity。
  2. 从 AMS 出来后,再找一个 Hook 点,用来将 ProxyActivity 替换为插件 Activity。

在看源码之前,我们再想一个问题,看源码是要找什么东西作为 Hook 点呢?

将插件Activity 替换为 ProxyActivity

我们在项目中一般通过 startActivity(new Intent(this,PluginActivity.class)); 启动 PluginActivity,如果我 想换成启动 ProxyActivity,调用方法 startActivity(new Intent(this,ProxyActivity.class)); 这样就可以了。 是不是已经知道答案了!!!没错,我们只要找到能够修改 Intent 的地方,就可以作为 Hook 点,从这儿也可以 看出 Hook 点并不是唯一的。

通过动态代理实现HookAMS

好下面我们进入源码

// android/app/Activity.java @Override 
    public void startActivity(Intent intent) {
        this.startActivity(intent, null);
    }

    @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        startActivityForResult(intent, -1, options);
    }

    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
        Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);
    }
// android/app/Instrumentation.java
public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        // 这儿就是我们的 Hook 点,替换传入 startActivity 方法中的 intent 参数
        int result = ActivityManager.getService().startActivity(whoThread, who.getBasePackageName(),
                intent, intent.resolveTypeIfNeeded(who.getContentResolver()),
                token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
}

既然 Hook 点找到了,那我们怎么修改呢? 答案是动态代理,所以我们要生成一个代理对象,显然,我们要代理的是 ActivityManager.getService() 返回的 对象。

那下面我们就来看下它返回的是什么?

首先我们看下 ActivityManager.getService() 返回的是一个什么类的对象

// android/app/ActivityManager.java 
public static IActivityManager getService() { 
       return IActivityManagerSingleton.get(); 
}

可以看到,它返回的是 IActivityManager 类的对象。下面我们就生成代理对象,并且当执行的方法是 startActivity 的时候,替换它的参数 intent。代码如下:

Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
            new Class[]{iActivityManagerClass}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    // 当执行的方法是 startActivity 时作处理
                    if ("startActivity".equals(method.getName())) {
                        int index = 0;
                        // 获取 Intent 参数在 args 数组中的index值
                        for (int i = 0; i < args.length; i++) {
                            if (args[i] instanceof Intent) {
                                index = i;
                                break;
                            }
                        }
                        // 得到原始的 Intent 对象 -- 唐僧(插件)的Intent
                        Intent intent = (Intent) args[index];

                        // 生成代理proxyIntent -- 孙悟空(代理)的Intent
                        Intent proxyIntent = new Intent();
                        proxyIntent.setClassName("com.enjoy.pluginactivity",
                                ProxyActivity.class.getName());
                        // 保存原始的Intent对象
                        proxyIntent.putExtra(TARGET_INTENT, intent);                    
                        // 使用proxyIntent替换数组中的Intent
                        args[index] = proxyIntent;
                    }
                    return method.invoke(mInstance, args);
                }
            });

接着我们再使用反射将系统中的 IActivityManager 对象替换为我们的代理对象 mInstanceProxy。那如何替换了? 我们接着看源码。

// android/app/ActivityManager.java 
public static IActivityManager getService() {
       return IActivityManagerSingleton.get();
}

private static final Singleton<IActivityManager> IActivityManagerSingleton = new Singleton<IActivityManager>() {
        @Override
        protected IActivityManager create() {
            final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
            final IActivityManager am = IActivityManager.Stub.asInterface(b);
            return am;
        }
    };

通过上面的代码,我们知道 IActivityManager 是调用的 Singleton 里面的 get 方法,所以下面我们再看下 Singleton 是怎么样的。

// android/util/Singleton 
    public abstract class Singleton<T> {
        private T mInstance;

        protected abstract T create();

        public final T get() {
            synchronized (this) {
                if (mInstance == null) {
                    mInstance = create();
                }
                return mInstance;
            }
        }
    }

可以看出,IActivityManagerSingleton.get() 返回的实际上就是 mInstance 对象。所以接下来我们要替换的就是这 个对象。代码如下:

// 获取 Singleton<T> 类的对象 
Class<?> clazz = Class.forName("android.app.ActivityManager");
Field singletonField = clazz.getDeclaredField("IActivityManagerSingleton"); 
singletonField.setAccessible(true);
Object singleton = singletonField.get(null);
// 获取 mInstance 对象 
Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance"); 
mInstanceField.setAccessible(true);
final Object mInstance = mInstanceField.get(singleton);
// 使用代理对象替换原有的 mInstance 对象
mInstanceField.set(singleton,mInstanceProxy);

到这儿我们的第一步就实现了,接着我们来实现第二步,在出来的时候,将它们换回去。

ProxyActivity 替换为插件 Activity

通过 反射 实现 HookHandler

在出来的时候,会调用 Handler 的 handleMessage,所以下面我们看下 Handler 的源码。

public void handleMessage(Message msg) {
}

public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
}

当 mCallback != null 时,首先会执行 mCallback.handleMessage(msg),再执行 handleMessage(msg),所以我 们可以将 mCallback 作为 Hook 点,创建它。ok,现在问题就只剩一个了,就是找到含有 intent 的对象,没办 法,只能接着看源码。

// android/app/ActivityThread.java 
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case LAUNCH_ACTIVITY: {
                final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
            }
            break;
        }
    }

    static final class ActivityClientRecord {
        Intent intent;
    }

可以看到,在 ActivityClientRecord 类中,刚好就有个 intent,而且这个类的对象,我们也可以获取到,就是 msg.obj。接下来就简单了,实现代码如下,如果有兴趣的同学也可从 handleLaunchActivity 方法一路跟下去,看看 ActivityClientRecord 的 intent 到底在哪使用的,这儿我们就不赘叙了。

// 获取 ActivityThread 类的 对象
    Class<?> clazz = Class.forName("android.app.ActivityThread");
    Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
    activityThreadField.setAccessible(true);
    Object activityThread = activityThreadField.get(null);

    // 获取 Handler 对象
    Field mHField = clazz.getDeclaredField("mH");
    mHField.setAccessible(true);
    final Handler mH = (Handler) mHField.get(activityThread);

    // 设置 Callback 的值
    Field mCallbackField = Handler.class.getDeclaredField("mCallback");
    mCallbackField.setAccessible(true);
    mCallbackField.set(mH,new Handler.Callback(){
        @Override public boolean handleMessage (Message msg){
        switch (msg.what) {
            case 100:
                try {                    
                    // 获取 proxyIntent
                    Field intentField = msg.obj.getClass().getDeclaredField("intent");
                    intentField.setAccessible(true);
                    Intent proxyIntent = (Intent) intentField.get(msg.obj);

                    // 目标 intent 替换 proxyIntent
                    Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                    proxyIntent.setComponent(intent.getComponent());
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
        }
        return false;
    }
    });

版本适配问题

4.如何加载插件的资源以及冲突的解决

资源加载

在项目中,我们一般通过 Resources 去访问 res 中的资源,使用 AssetManager 访问 assets 里面的资源。

String appName = getResources().getString(R.string.app_name); 
InputStream is = getAssets().open("icon_1.png");

实际上,Resources 类也是通过 AssetManager 类来访问那些被编译过的应用程序资源文件的,不过在访问之前, 它会先根据资源 ID 查找得到对应的资源文件名。 而 AssetManager 对象既可以通过文件名访问那些被编译过的, 也可以访问没有被编译过的应用程序资源文件。

raw文件夹和assets文件夹有什么区别

  • raw : Android会自动的为这目录中的所有资源文件生成一个ID,这意味着很容易就可以访问到这个资源,甚至在xml 中都是可以访问的,使用ID访问速度是最快的。
  • assets : 不会生成ID,只能通过AssetManager访问,xml中不能访问,访问速度会慢些,不过操作更加方便。

我们来看下 Resources 调用 getString 的代码实现过程:

// android/content/res/Resources.java 
public String getString(@StringRes int id) throws NotFoundException {    
    return getText(id).toString(); 
} 

public CharSequence getText(@StringRes int id) throws NotFoundException {    
    CharSequence res = mResourcesImpl.getAssets().getResourceText(id);    
    if (res != null) {        
        return res;    
    }
}

通过上面的代码知道 Resources 的实现类是 ResourceImpl 类,getAssets() 返回的是 AssetManager,所以也就 证实了资源的加载实际是通过 AssetManager 来加载的。 接下来我们看下 AssetManager 是如何创建和初始化的,又是如何加载 apk 资源的,只有掌握了原理,我们才知 道如何去加载另一个 apk 的资源。

// android/app/LoadedApk 
public Resources getResources() {    
    if (mResources == null) {        
    // 获取 ResourcesManager 对象的单例,然后调用 getResources 方法去获取 Resources 对象 --> 1       
    mResources = ResourcesManager.getInstance().getResources(null, mResDir,
        splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,           
        Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),getClassLoader());    
    }    
    return mResources; 
}
// android/app/ResourcesManager // --> 1
    public @Nullable
    Resources getResources(@Nullable IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs,
                           @Nullable String[] overlayDirs, @Nullable String[] libDirs, int displayId,
                           @Nullable Configuration overrideConfig,
                           @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader) {
        try {
            final ResourcesKey key = new ResourcesKey(
                    resDir,// 这个就是 apk 文件路径
                    splitResDirs, overlayDirs, libDirs, displayId, 
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);

            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            // 获取或者创建 Resources 对象 --> 2
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }


    // --> 2
    private @Nullable
    Resources getOrCreateResources(@Nullable IBinder activityToken,
                                   @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        // 创建 ResourcesImpl 对象 --> 3
        ResourcesImpl resourcesImpl = createResourcesImpl(key);
        // resources 是 ResourcesImpl 的装饰类
        return resources;
    }

    // --> 3
    private @Nullable
    ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {    
        // 创建 AssetManager 对象 --> 4
        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }
        // 将 assets 对象传入到 ResourcesImpl 类中
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        return impl;
    }

    // --> 4
    protected @Nullable
    AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();
        if (key.mResDir != null) {        
            // 通过 addAssetPath 方法添加 apk 文件的路径
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }
        return assets;
    }

通过上面代码的分析,我们知道了 apk 文件的路径是通过 assets.addAssetPath 方法设置的,所以如果我们想将 插件的 apk 文件添加到宿主中,就可以通过反射修改这个地方。

实现步骤

  1. 创建一个 AssetManager 对象,并调用 addAssetPath 方法,将插件 apk 的路径作为参数传入。
  2. 将第一步创建的 AssetManager 对象作为参数,创建一个新的 Resources 对象,并返回给插件使用。

具体代码如下:

 public static Resources loadResource(Context context) {
        try {
            Class<?> assetManagerClass = AssetManager.class;
            AssetManager assetManager = (AssetManager) assetManagerClass.newInstance();
            Method addAssetPathMethod = assetManagerClass.getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.setAccessible(true);
            addAssetPathMethod.invoke(assetManager, apkPath);
            Resources resources = context.getResources();       
            //用来加载插件包中的资源
            return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

然后在宿主的自定义 Application 类中添加如下代码:

// 宿主代码 
    private Resources resources;

    @Override
    public void onCreate() {
        super.onCreate();

        // 获取新建的 resources 资源
        resources = LoadUtil.loadResource(this);
    }

    // 重写该方法,当 resources 为空时,相当于没有重写,不为空时,返回新建的 resources 对象
    @Override
    public Resources getResources() {
        return resources == null ? super.getResources() : resources;
    }

接着在插件中,创建BaseActivity,如下:

 // 插件中代码
    public abstract class BaseActivity extends Activity {

        @Override
        public Resources getResources() {
            if (getApplication() != null && getApplication().getResources() != null) {
                // 因为宿主重写了该方法,所以获取的将是新创建的 resources 对象
                return getApplication().getResources();
            }
            return super.getResources();
        }
    }

然后让插件的 Activity 都继承自 BaseActivity,这样,插件在获取资源时,使用的就是在宿主中新创建的 resources 对象,也就可以拿到资源了。

总结

插件化涉及的技术其实是非常多的,比如应用程序启动流程、四大组件启动流程、AMS原理、PMS原理、 ClassLoader原理、Binder机制,动态代理等等。后送给大家一句话,路漫漫其修远兮,吾将上下而求索。