手动实现一次插件化

96
MxsQ
1.2 2019.07.07 11:39* 字数 3252

为什么学习插件化

在项目迭代过程中,业务愈加复杂,在单工程开发模型下,业务模块耦合度极高,模块边界模糊,对工程所做的任何修改都必须编译整个工程,团队协同暴露出诸多冲突与不便,不得不向多工程开发模型发展。代表为组件化、插件化。

多工程开发模型的核心点在于,能像搭积木一样灵活地将各个模块组装形成一个可独自编译运行的小工程,而最终的APP,则是由这些小工程聚合而成。这样,团队成员可以专注于各自的小工程里,互不干涉,边界清晰。

组件化与插件化如何做选择

如果就日常开发的角度看,两者并没有什么不同,比如均可实现业务组件的热插拔,并且遇到的问题也是一致的,比如需要解决分层问题、跳转问题、跨模块通信问题等等。但从赋予能力与技术实现上的角度来看,可将组件化看成插件化的子集。插件化包含了组件化所有的优点,并实现了动态化。

插件化解决的核心问题如下:

  • 动态化发布
  • 可以看到dex分包和资源分包方案

因此,如果面对的业务不存在快速迭代频繁发布的需求,插件化的威力将减少大半。并且,动态化往往伴随兼容性与稳定性问题,因此还需衡量团队面对此等问题的付出与产出是否值得。

文章目的

虽然已有各种插件化框架,并学习知悉插件化原理,但,纸上得来终觉浅,绝知此事要躬行。通过手动实现一次插件化,加深对插件化原理的理解。并且在平日学习到的各种Framework层的知识,也可在此过程中得到印证。

note: 文章源码基于8.0

如何实现插件化

插件化的动态化实际要解决三个问题:

  • 类加载
  • 资源加载
  • 四大组件的管理

类加载

当使用到插件中的类时,需要先将类进行加载才可使用。因此,需要知道Android下的类加载原理。


类加载器.png

图片来源

类加载原理可参考: 好怕怕的类加载器

简单来说,使用类加载器通过双亲委任模型对类进行加载,特点如下:

  • 每个类加载器实例化时需要传入另一个类加载器作为父加载器
  • 一个类是否被加载过根据 ClassLoader + PackageName + ClassName 来进行判断
  • 加载类时会让父加载器先进行加载,如果父加载器不加载,则自己再加载,这样能保证上层类如Framework层的类能直接使用,避免重复加载,还可以隔离核心类库被修改
  • PathClassLoader 负责加载系统的类和主dex中的类
  • DexClassLoader 可从包含 Classes.dex 的jar包或APK中加载类

资源加载

类的使用往往涉及对资源的使用,图片、布局文件等等。插件里的资源未进行装载,而当访问到时,必然crash,因此需要将插件的资源进行加载。


资源加载原理.png

资源加载原理可参考:Android 资源加载机制剖析

资源加载原理可以描述为:

  • APK打包时,通过aapt将所有资源规整出arsc文件,arsc文件包含了所有资源ID、资源类型、文件路径信息和所有字符串信息
  • 通过AssetManager.addAssetPath()传入APK路径,最终触发Nativie层AssetMananger.cpp.appendPathToResTable()创建ResTable
  • Java层访问资源时,通过资源ID和ReTable,可以得知资源的描述信息TypeValue,即可从中拿到资源的关键信息并进行访问

四大组件的管理

对于四大组件来说,装载了相应类并没有达到可用状态。之后均以Activity来做说明。

以new方式创建Activity是不行的,Activity的运行需要具备相应的上下文,并需要在AndroidManifest文件里进行注册。APK在安装时,PMS从AndroidManifest里收集所有Activity的信息(当然还有其它信息,这里省略),等在Activity启动时,AMS通过PMS获取Activity信息,如启动模式、所在进程等,再进行启动。

在插件化的场景下,使用Activity需要解决三个问题:
1、Activity的启动需要上下文
2、在AndroidManifest里进行注册
3、通过AMS的校验

Activity的启动原理可参考: Activity启动时发生了什么

手动实现

知道了插件化所需解决的问题,也粗略了解了相应问题涉及的原理,也就可以见招拆招,开始简单地手动实现插件化。

第一步,合并DEX

在类加载原理的基础上,Android中的ClassLoader实例化时接收APK文件路径,从中解析出Dex,并存于BaseDexClassLoader.dexPathList.dexElements,以便需要类加载时从中获取类信息。
源码可见
DexClassLoader()
-> BaseDexClassLoader()
-> DexPathList()
-> DexPathList.makeDexElements()

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
    ......
    // 解析出dex
    DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
    if (dex != null) {
        // 将dex存储elments,之后此elements的所有元素将存于DexPathList.dexElements
        elements[elementsPos++] = new Element(dex, null);
    }
    ......
}

了解原理思路就明了了。通过新建类加载器,为插件APK解析出Element,并将其插入宿主的Element,即可提供插件的类信息,如图


插件化-合并dex.png

代码实现如下

    public static void loadPluginDex(Application context, ClassLoader classLoader) throws Exception{

        // 获取插件apk
        String apkPath =  getPatchApkPath(context);
        File apkFile = new File(apkPath);

        // 创建安装插件的Classloader
        DexClassLoader dexClassLoader = new DexClassLoader(apkFile.getAbsolutePath(), null, null,classLoader);

        // 获取BaseDexClassLoader.dexPathList
        Object pluginDexPatchList = ReflectUtil.getField(dexClassLoader, "pathList");
        // 获取DexFile.dexElements
        Object pluginDexElements =  ReflectUtil.getField(pluginDexPatchList, "dexElements");

        // 通过反射获取宿主 dexPathList
        Object hostDexPatchList = ReflectUtil.getField(classLoader, "pathList");
        // 通过反射获取宿主 dexElements
        Object hostDexElements =  ReflectUtil.getField( hostDexPatchList, "dexElements");

        // 合并dexElements
        Object array = combineArray(hostDexElements, pluginDexElements);
        ReflectUtil.setField( hostDexPatchList, "dexElements", array);
        
        // 载入资源文件
        mergePluginResources(context);
    }

第二步,加载插件资源

日常开发时,访问资源往往通过 context.getResources().xxx(R.xxx.xxx)进行访问,而getResources()获得的Resources对象存于ContextImpl.mResources,此对象还存于LoadedApk.packageInfo。

源码可见
ActivityThread.handleLaunchActivity()
-> ActivityThread.performLaunchActivity()
-> LoadedApk.makeApplication()

    // LoadedAPK
    public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
            ......
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
            ......
    }
    
    // ContextImpl
    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
                null);
        // 设置mResource
        context.setResources(packageInfo.getResources());
        return context;
    }

因此实现方式是,通过AssetManaget从APK中将资源载入生成新的Resources,再替换ContextImpl.mResources以及ContextImpl.mPackageInfo。
实现代码为:

    public static void loadPluginResources(Application application) throws Exception{
        AssetManager assetManager = AssetManager.class.newInstance();
        // 获取 AssetManager.addAssetPath() 方法
        Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
        // 载入插件的资源
        addAssetPath.invoke(assetManager, getPatchApkPath(application));

        // 创建新的Resource对象
        Resources merResource = new Resources(assetManager,
                application.getBaseContext().getResources().getDisplayMetrics(),
                application.getBaseContext().getResources().getConfiguration());

        // 替换 ContextImpl.mResources
        ReflectUtil.setField(
                application.getBaseContext(),
                "mResources",
                merResource);

        // 获取 ContextImpl 中的 LoadedApk 类型的 mPackageInfo
        Field mPackageInfoField = application.getBaseContext().getClass().getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object packageInfoO = mPackageInfoField.get(application.getBaseContext());
        // 替换 mPackageInfo.mResources
        ReflectUtil.setField(packageInfoO, "mResources", merResource);

        // 替换 ContextImpl 中的 Resources.Theme
        Field themeField = application.getBaseContext().getClass().getDeclaredField("mTheme");
        themeField.setAccessible(true);
        themeField.set(application.getBaseContext(), null);
    }

第三步,管理插件Actvity

Activity启动过程,调用栈可以简要描述成下图


Activity启动调用栈.png

1、注册问题
Activity需要在AndroidManifest进行注册才可使用,且无法进行动态注册,那么想要使用插件中未注册的Activity,此步骤也无法省略。常规的做法是,使用一个替身StubActivity在AndroidManifest里进行注册,以达到占位效果,所有插件的Activity均通过StubActivity共同欺骗AMS。
2、通过AMS校验
既然替身StubActivity已经进行过正常注册,必然能经过AMS的校验。问题是,使用StubActivity代替实际Activity通过AMS校验,就需要在合适的时机将实际Activity装扮成StubActivity,同样在合适时机将其还原。
3、上下文
提升StubActivity跟AMS打交道时,能拿到相应的Context。Activity的启动过程中,在Activity.attach()时机将Context进行绑定。因此保证ActivityThread构建出的Activity为实际需要的Activity,自然可以拿到Context。

综上所诉,问题可以简化成,需要一个替身来通过AMS检测并在合适时机还原。因此存在动态替换方案与静态替换方案。后续做了三种实现方式。

方式一,Hook Instrumentation

Instumentation.execStartActivity() 可视为启动Activity的起点,可作为装扮实际Activity的节点。当ActivityThread加载Activity类时,则通过Instumentation.newActivity()进行加载,所以此处可作为恢复真正Activity的节点。

在AndroidManifest里注册StubActivity,不需要创建类文件

<activity android:name=".StubActivity" />

创建自己的Instrumentation类,进行实现,关键代码如下

public class InstrumentationHook extends Instrumentation {
        public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        if (!StubHelper.checkActivity(who, intent)){
             // 保存要启动的插件Activity的信息,使用StubActivity做伪装
            intent.putExtra(REAL_ACTIVITY_BANE, intent.getComponent().getClassName());
            intent.setClassName(who, StubHelper.STUB_ACTIVITY);
        }

        try {
            // 通过实际的mInstrumentation进行启动
            return (ActivityResult) startActivityMethod.invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {

        }
        return null;
    }
    
        @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        String startActivityName = intent.getStringExtra(REAL_ACTIVITY_BANE);
        if (!TextUtils.isEmpty(startActivityName)) {
            // 还原Activity
            return super.newActivity(cl, startActivityName, intent);
        }

        return super.newActivity(cl, className, intent);
    }
    
    ......
}

再将Instrumentation进行替换,这部分代码省略。

方式二,代理AMS

启动Activity过程,可是视起点为AMS.startActivity(),且AMS最后通过向ActivityThread发送信息,在ActivityThread.H接收到LAUNCH_ACTIVITY信号后,将创建Activity,也代表Activity通过检验。选取这两处作为替换、还原节点。

在APP内,AMS以IActivityManager形式存在,通过代理IActivityManager则能达到代理AMS的目的。在8.0的源码里,代理IActivityManager,则需按照以下步骤:

  • 获取ActivityManager.getService(),通过此方法获得IActivityManagerSingleton
  • IActivityManagerSingleton类型为Singleton,Singleton为系统提供单例实现辅助类,实例存于Singleton.mInstance,因此IActivityManager存于mInstance
  • 代理IActivityManager

代码实现如下

    private static void replaceActivity(final Context context) throws Exception{

        // 通过ActivityManager获取AMS实例, , 26以上有效
        Class amClass = Class.forName("android.app.ActivityManager");
        Method getServiceMethod = amClass.getDeclaredMethod("getService");
        final Object iActivityManagerObje = getServiceMethod.invoke(null);
        Field iActivityManagerSingletonField = amClass.getDeclaredField("IActivityManagerSingleton");
        Object iActivityManagerSingletonObj = ReflectUtil.getStaticField(amClass, "IActivityManagerSingleton");

        // 获取 mInstance
        Class singleTonClass = Class.forName("android.util.Singleton");
        Field mInstanceField = singleTonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true);

        iActivityManagerSingletonField.setAccessible(true);

        // ams 实例
        final Object amsObj = ReflectUtil.getField(iActivityManagerSingletonObj, "mInstance");

        // 获取IActivityManager类
        Class<?> iamClass = Class.forName("android.app.IActivityManager");
        // 创建IActivityManager的动态代理
        Object proxy = Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{iamClass},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        // hook startActivity
                        if (!"startActivity".equals(method.getName())){
                            return method.invoke(amsObj, args);
                        }

                        // 找到intent对象
                        int intentIndex= 0;
                        for (int i=0; i<args.length; i++){
                            if (args[i] instanceof Intent){
                                intentIndex = i;
                                break;
                            }
                        }

                        Intent realIntent = (Intent) args[intentIndex];

                        // 检查启动的Activity是否在宿主Manifest声明
                        if (StubHelper.checkActivity(context, realIntent)){
                            return method.invoke(amsObj, args);
                        }

                        // 使用占坑的Activity绕过AMS,替换Intent
                        Intent stubIntent = new Intent();
                        // SELF_PAK为插件APK包名
                        stubIntent.setComponent(new ComponentName(StubHelper.SELF_PAK, StubHelper.STUB_ACTIVITY));
                        stubIntent.putExtra(StubHelper.REAL_INTENT, realIntent);
                        args[intentIndex] = stubIntent;

                        return method.invoke(amsObj, args);
                    }
                }
        );

        // 代理ams
        mInstanceField.setAccessible(true);
        mInstanceField.set(iActivityManagerSingletonObj, proxy);
    }

以上完成了伪装成StubActivity的功能。在触发创建Activity的节点,通过ActivityThread.H来接收触发,而H为Handler,因此,可以为Handler设置Callback,在接收到LAUNCH_ACTIVITY信号时,恢复成真正的Activity。代码实现如下:

    private static void restoreActivity() throws Exception{
        // 获取 ActivityThread
        Class atClass = Class.forName("android.app.ActivityThread");
        Object curAtObj = ReflectUtil.getStaticField(atClass, "sCurrentActivityThread");

        // 获取 ActivityThread 中的 handle , 即 mH
        final Handler mHObj = (Handler) ReflectUtil.getField(curAtObj, "mH");

        // 设置 Handler 的 mCallBack
        Class handlerClass = Handler.class;
        ReflectUtil.setField(mHObj, "mCallback", new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                try{
                    int LAUNCH_ACTIVITY = 0;
                    Class hClass = Class.forName("android.app.ActivityThread$H");
                    LAUNCH_ACTIVITY = (int) ReflectUtil.getStaticField(hClass, "LAUNCH_ACTIVITY");

                    if (msg.what == LAUNCH_ACTIVITY){
                        // 恢复原来的intent
                        Intent intent = (Intent) ReflectUtil.getField(msg.obj, "intent");
                        Intent realIntent = intent.getParcelableExtra(StubHelper.REAL_INTENT);
                        if (realIntent != null){
                            intent.setComponent(realIntent.getComponent());
                        }
                    }
                } catch (Exception e){

                }
                mHObj.handleMessage(msg);
                return true;
            }
        });
    }

动态代理的插件APK

自行管理Instrumentation或代理AMS的方式属于动态替换方案,为其准备插件APK。插件APK需注意,继承类为Activity。且替换了Resources,因此,在插件APK的Activity,暂需重写getResource方法如下

    @Override
    public Resources getResources() {
        return (getApplication() != null && getApplication().getResources()!= null)
                    ? getApplication().getResources()
                    : super.getResources();
    }

插件APK可来源于网络,但为了简单起见,直接从本地读取。只需将APK存放于宿主有权限读取的文件路径即可。我的情况存于context.getExternalCacheDir().getAbsolutePath()路径下,路径为

/storage/emulated/0/Android/data/你的宿主包名/cache

通过adb命令

adb push 本地文件路径 手机存储路径

即可将插件APK推到手机

方式三

此方式属于静态代理,较好理解,不需要Hook任何Framework层代码。分三步执行:

  1. 在宿主里创建ProxyActivity,以此Activity来代理生命周期
  2. 插件里以BasePluginActivity为基类创建Activity,当然,这些Activity并不是真正的Activity,只是看起来像Activity
  3. 启动Activity实际上也是启动ProxyActivity,然后将ProxyActivity与插件Activity进行双向绑定,搜集插件Activity的回调方法。在生命周期回调时,通过ProxyActivity传达回调信息给插件Activity,也就是通过调用插件Activity的具体方法
插件化静态代理实现.png

BasePluginActivity如下:

public abstract class BasePluginActivity {
    // 宿主
    protected Activity mHost;

    // 建立与代理Activity的连接
    public void proxy(Activity host){
        mHost = host;
    }

    public void setContentView(int layoutId){
        mHost.setContentView(layoutId);
    }
    
    protected abstract void onCreate(Bundle savedInstanceState);
    protected void onStart(){};
    protected void onRestart(){};
    protected void onResume(){};
    protected void onPause(){};
    protected void onStop(){};
    protected void onDestroy(){};
}

ProxyAcitivty关键代码如下:

public class ProxyActivity extends Activity {

    // 插件 Activity实例
    private Object mPluginActivity;
    // 插件 Activity类名
    private String mPluginClassName;
    // 生命周期方法回调
    private Map<String, Method> mLifecycleMethods = new HashMap<>();

    public static final String PLUGIN_STUB = "plugin_stub";

    // 插件Activity类名
    public static final String PLUGIN_CLASS_NAME = "com.bf.qinx.cosplayplugin.PluginActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 获取插件类名
        mPluginClassName = getIntent().getStringExtra(PLUGIN_STUB);

        // 代理插件
        proxyPluginActivity();
        // 执行插件Activity.onCreate()
        invokeLifecycleMethod("onCreate", new Object[]{savedInstanceState});
    }
    
        /**
     * 代理插件Activity
     */
    private void proxyPluginActivity() {
        try{
            // 获取插件Activity
            Class<?> clazz = Class.forName(mPluginClassName);
            Constructor<?> con = clazz.getConstructor(new Class[]{});
            mPluginActivity = con.newInstance(new Object[]{});

            // 触发插件的hook点,建立链接,即调用BasaPlauginActivity.proxy()
            Method proxyMethod = clazz.getMethod("proxy", new Class[]{Activity.class});
            proxyMethod.setAccessible(true);
            proxyMethod.invoke(mPluginActivity, new Object[]{ this });

            // 收集插件Activity其它生命周期方法,就不展开了
            proxyLifecycle(clazz);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
    
    ......
}

建立好了ProxyActivity与插件Activity的连接并收集到相应方法存于mLifecycleMethods后,在ProxyActivity生命周期出发时,从mLifecycleMethods触发代理Activity相应方法即可,举个例如onResume()

    @Override
    protected void onResume() {
        super.onResume();
        invokeLifecycleMethod("onResume", null);
    }

    private void invokeLifecycleMethod(String methodName, Object[] args){
        try{
            Object[] methodArgs = args;
            if (methodArgs == null){
                methodArgs = new Object[]{};
            }
            Method method = mLifecycleMethods.get(methodName);
            if (method != null){
                method.invoke(mPluginActivity, methodArgs);
            }
        } catch (Exception e){
            Log.d("xx", "invokeLifcycleMethod: " + e.getMessage());
        }
    }

启动插件Activity

以下依次为hook ams、hook instrumentation、静态替换的启动Activity方式

    private void startPatchActivityFormAMS(){
        Intent intent = new Intent();
        ComponentName componentName = new ComponentName("这里是插件包名" , PATCH_ACTIVITY);
        intent.setComponent(componentName);
        startActivity(intent);
    }

    private void statPatchActivityFromInstrumentation(){
        Intent intent = new Intent();
        ComponentName componentName = new ComponentName(MainActivity.this , PATCH_ACTIVITY);
        intent.setComponent(componentName);
        startActivity(intent);
    }

    private void startCosplayActivity(){
        Intent intent = new Intent();
        intent.setClass(MainActivity.this, ProxyActivity.class);
        intent.putExtra(ProxyActivity.PLUGIN_STUB, ProxyActivity.PLUGIN_CLASS_NAME);
        startActivity(intent);
    }

总结

以上通过动态方案和静态方案为方向,提供了三种实现方式。其中动态方案的优点在于,真正地去管理Activity(以Activity为例)的生命周期,并通过hook关键Framework代码让插件Activity达到可用状态。缺点在兼容性和稳定性存在一定风险,需要出处理各大源码之间的差异,比如上述代理AMS时,实际源码场景为26以上。反观静态代理方法,实际上没有任何稳定性与兼容性问题,问题在于有一定的局限性,比如,当你想更逼真地模拟一个Activity,得下一定的功夫。

Demo 源码地址: Demo源码地址

项目结构如下


插件化实现-项目结构.jpg

实现插件化,实际要解决类加载、资源加载、管理四大组件生命周期三大问题:

  1. 通过类加载机制,与Android里类加载特点,将插件dex插入BaseDexClassLoader.dexPathList.dexElements即可解决类加载问题
  2. 通过AssertManager将插件dex中arsc文件解析成访问资源所需的ResTable
  3. 通过Hook AMS 或 Hook Instrumentation,以StubActivity达到通过AMS检测的目的。也可通过静态方式,通过ProxyActivity来模拟Activity达到目的

当然,以上的仅仅是最简单的实现,避开了很多的问题,在实际的插件化方案实现要复杂的多。

通过手动实现一次插件化,不仅仅是对实现原理有更深刻的了解,也佐证了涉及到的Framework知识,因此对类加载原理、资源加载原理、Activity启动原理也有更真切的认识。

参考

Android插件化原理解析
Android插件化原理和实践 (四) 之 合并插件中的资源
手把手讲解 Android Hook无清单启动Activity的应用
Android插件化之从入门到放弃
Android 开发:由模块化到组件化(一)

安卓