插件化实现原理(学习笔记六)

介绍:

插件化本质上来说是运行没有安装的apk,支持插件的app一般称为宿主。宿主提供上下文环境通常作为主APP在运行时加载和运行插件,这样便可将app中一些不常用的功能模块做成插件,一方面可以减少安装包的大小,另一方面可以实现APP功能的动态扩展

优势:

让用户不用重新安装 APK 就能升级应用功能,减少发版本频率,增加用户体验。提供一种快速修复线上 BUG 和更新的能力。按需加载不同的模块,实现灵活的功能配置,减少服务器对旧版本接口兼容压力。模块化、解耦合、并行开发、 65535 问题。

实现原理:

在Android中的插件化技术,可以理解为动态加载的过程,分为下面几步:

  • 把可执行文件( .so/dex/jar/apk 等)拷贝到应用 APP 内部。
  • 加载可执行文件,更换静态资源调
  • 用具体的方法执行业务逻辑

Android 项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种:

  1. 动态加载 .so 库
  2. 动态加载 dex/jar/apk文件

第一点,Android在NDK中就使用了动态,动态加载.so库并通过JNI调用封装好的方法,后者一般是由C/C++编译而成,运行在Native层,效率比运行在虚拟机层上要高,通常用来完成一些对性能要求比较高的工作。

第二点,基于 ClassLoader 的动态加载 dex/jar/apk 文件,也是现在普遍使用的动态加载,使用这种方式实现插件化,需要解决一些问题:

  1. 如何加载插件的类
  2. 如何加载插件的资源
  3. 如何调用插件类
1.加载插件的类

java文件编译后生产的是class文件,而apk文件包含一个或多个classes.dex,它是把所有的class文件合并优化后生产的,jvm加载class文件,而android的DVM和ART加载的dex文件,二者都是用Classloader加载的,二者加载的文件类型不同,还是会有一些区别,主要需要了解android的ClassLoader如何加载dex文件。

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

  • BootClassLoader 用于加载 Android Framwork层class文件
  • PathClassLoader 用于加载 Android应用程序的类,可加载指定的dex,zip,apk中的classes.dex
  • DexClassLoader 用于加载指定的dex,zip,apk中的classes.dex
PathClassLoader 与 DexClassLoader

PathClassLoader 和 DexClassLoader 都是继承自 BaseDexClassLoader,且类中只有构造方法,它们的类加载逻辑完全写在 BaseDexClassLoader 中,并且在8.0之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是生成的 odex(优化的dex)存放的路径,PathClassLoader 直接为null,而 DexClassLoader 是使用用户传进来的路径,而在8.0之后,二者就完全一样了。

加载原理

假设apk的文件是dexPath,需要加载里面的Test类,通过以下代码可以实现:

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

DexClassLoader 类中没有 loadClass 方法,该方法是在父类ClassLoader中实现

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 重写了 fifindClass 和 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 类中的 fifindClass 方法

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;
}

我们发现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.lengh);
    /**
    * 将生成的新值赋给 "dexElements" 属性
    */
        hostDexElementsField.set(hostPathList, dexElements);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
2.加载插件的资源

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

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

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

AssetManager 加载资源的过程:

// 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 对象,也就可以拿到资源了。

3.宿主启动插件的Activity

Activity是需要在清单文件中注册的,显然,插件的 Activity 没有在宿主的清单文件中注册,这里我们就需要使用 Hook 技术,来绕开系统的检测。
Hook
首先我们在宿主里面创建一个 ProxyActivity 继承自 Activity,并且在清单中注册。当启动插件Activity 的时候,在系统检测前,找到一个Hook点,然后通过 Hook 将插件 Activity 替换成 ProxyActivity,等到检测完了后,再找一个Hook点,使用 Hook 将它们换回来,这样就实现了插件 Activity 的启动。
要找到Hook点,首先需要了解Activity的启动流程,这里不做详细描述。
总体上来说,Activity的启动的通信流程大致是APP–AMS–APP:

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

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

源码:

// 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);
}

然后使用动态代理技术,生成一个动态代理对象,代理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);

到这儿我们的第一步就实现了,接着我们来实现第二步,在出来的时候,将它们换回去。从AMS到APP通信会调用 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;
    }
});

总结

插件化涉及的技术其实是非常多的,比如应用程序启动流程、四大组件启动流程、AMS原理、PMS原理、ClassLoader原理、Binder机制,动态代理等等。越学习越能感受到每一门技术都不简单,需要积累的东西还有很多,加油。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值