插件框架-RePlugin源码阅读

2020-04-17  本文已影响0人  河里的枇杷树

写在前面

==如果时间有限可以直接跳到最下面的 核心问题==

* 插件化现状

插件化目前的处境肯定是大不如前,由于android系统逐步完善收紧各种黑科技很难再爆发,各个插件化逐步从爆发大量黑科技到追求稳定性,再加之小程序的产生。让大厂很多合作直接使用小程序,而不再使用插件化.
不过对于中小公司没有小程序能力的,插件化不失为一种比较好的动态化方案。

*为什么阅读RePlugin的源码

对比了VirtualApk和RePlugin,选择阅读RePlugin的源码是因为,它比RePlugin 有更大的概率还在维护中,而且wiki中有已经整理好的原理性的文章,方便阅读

为什么不选择阅读VirtualApp的源码?虽然他是第三代插件化框架,但是目前收费的,阅读源码还是希望使用在项目中。有时间可以阅读以下VirtualApp的 免费版本

插件化无疑就是在解决,如何让宿主app使用到 插件的 类,资源。这样的问题..(核心问题部分中有回答)我们本着这个核心思想去阅读源码应该会有更好的效果

* RePlugin解决了什么问题?

RePlugin解决的是各个功能模块能独立升级,又能需要和宿主、插件之间有一定交互和耦合(所以开发需要按照一定的规则)。有别与类似 VirtualApk 这种双开类型的插件化框架(可以将任一APP作为插件)

感叹

RePlugin应该是我现在阅读的除了Android源码之外最复杂的源码了,不过确实写得挺好的,有很多地方值得学习,尤其是在程序的健壮性和兼容性方面。

版本

v2.3.3

参考


RePlugin原理简介

Replugin的整体框架使用了Binder机制来进行宿主和多插件之间交互通信和数据共享,这里如果了解android四大组件的运行流程的话,看完了Replugin的源码后会感觉非常像简易ServiceManager和AMS的结构。

Replugin默认会使用一个常驻进程作为Server端,其他插件进程和宿主进程全部属于Client端。当然如果修改不使用常驻进程,那么宿主的主进程将作为插件管理进程,而不管是使用宿主进程还是使用默认的常驻进程,Server端其实就是创建了一个运行在该进程中的Provider,通过Provider的query方法返回了Binder对象来实现多进程直接的的沟通和数据共享,或者说是插件之间和宿主之间沟通和数据共享,插件的安装,卸载,更新,状态判断等全部都在这个Server端完成。

其实Replugin还是使用的占坑的方式来实现的插件化,replugin-host-gradle这个gradle插件会在编译的时候自动将坑位信息生成在主工程的AndroidManifest.xml中,Replugin的唯一hook点是hook了系统了ClassLoader,当启动四大组件的时候会通过Clent端发起远程调用去Server做一系列的事情,例如检测插件是否安装,安装插件,提取优化dex文件,分配坑位,启动坑位,这样可以欺骗系统达到不在AndroidManifest.xml注册的效果,最后在Clent端加载要被启动的四大组件,因为已经hook了系统的ClassLoader,所以可以对系统的类加载过程进行拦截,将之前分配的坑位信息替换成真正要启动的组件信息并使用与之对应的ClassLoader来进行类的加载,从而启动未在AndroidManifest.xml中注册的组件。

各个工程模块职责简要解析


各模块解析

replugin-host-gradle

主要职责

  1. 创建 rpShowPlugin... Task 用于将 插件信息写入到 plugins-builtin.json 文件中
  2. 创建 rpGenerateHostConfig Task用于生产 RePluginHostConfig.java 文件,文件内容基本就是 用户配置的信息(坑位信息,进程名称等)
  3. 修改manifast.xml 植入占坑信息

replugin-host-library

各类职责

运行于常驻进程 (常驻进程主要用于插件管理和Service(四大组件)维护)
运行于ui进程

replugin-plugin-gradle

主要职责

  1. 创建调试用的各个task
    • 强制停止宿主程序: rpForceStopHostApp
    • 安装插件到宿主并运行(常用任务): rpInstallAndRunPluginDebug或rpInstallAndRunPluginRelease等
    • 仅仅安装插件到宿主: rpInstallPluginDebug或rpInstallPluginRelease等
    • rpRestartHostApp
      重启宿主程序
    • 仅仅运行插件,如果插件前面没安装,则执行不成功:rpRunPluginDebug或rpRunPluginRelease等
    • 启动宿主程序:rpStartHostApp
    • 仅仅卸载插件,如果完全卸载,还需要执行rpRestartHostApp任务: rpUninstallPluginDebug或rpUninstallPluginRelease
  2. 使用了Transfrom API和Javassist实现了编译期间动态的修改字节码文件,主要是
    • 替换插件工程中的Activity的继承全部替换成Replugin库中定义的XXXActivity(如PluginActivity)。
    • 动态的将插件apk中调用LocalBroadcastManager的地方修改为Replugin中的PluginLocalBroadcastManager调用,(被修改的包含一些系统类 比如LocalBroadcastManager的sendBroadcastSync 就被替换了。)
    • 动态修改ContentResolver和ContentProviderClient的调用修改成Replugin 自定义的调用调用,(被修改的包含一些系统类)
    • 动态的修改插件工程中所有调用Resource.getIdentifier方法的地方,将第三个参数修改为插件工程的包名(被修改的包含一些系统类)

replugin-plugin-library

各类职责


大体工作流程

  1. 宿主APP启动时加载插件(解析插件信息但是不适用),和缓存预埋坑位
  2. 在使用插件时 选择合适坑位

阅读要点

标识解读

内部存储中各个文件夹的含义

阅读时注意的点


调用链

host初始化

宿主启动插件中某Activity流程

==这样就达成了偷梁换柱,系统以为我们加载的是 坑位类,但其实加载的是 插件目标类。而且系统也会乖乖的替我们 管理 插件目标类的 生命周期。太阴了....==

插件中启动activity

1. 插件中直接或者间接(使用activity中使用view.getContext)通过Activity.startActivity打开宿主Activity
因为在编译器 插件中Activity的父类都被改变为继承自 PluginActivity等Replugin 提供的Activity,所以他们的 startActivity 都会以 PluginActivity等的 startActivity为起点
2. 插件中使用Application的context打开Activity (==要走两次PluginContext.startActivity==)

因为插件中的Context是在Plugin.callApp()过程中传递过去的PluginContext,所以,这个流程会以PluginContext.startActivity(Intent intent)为起点

可以看到 插件中启动activity最终都走到了 PluginLibraryInternalProxy.startActivity(5个参数) 这个方法
3. 插件中正常(走的是Activity的startActivity)打开插件中的Activity (==要走两次Activity.startActivity==)
4. 插件中打开宿主中Activity

因为插件的classLoader 如果找不到类就会去 宿主中找,而且 宿主的Activity也已经注册了,所以直接打开就行

插件activity(继承自PluginActivity) onCreate 流程

插件在编译器会将自己的父类替换为RePlugin内容提供的类如PluginActivity


学到的

1. 如何避免资源id冲突

答:不同的插件设置不同的packageId(==范围0x02 - 0x7e,0x01是系统的,0x7f是宿主APP的==),进行区分

2. hook时机完美

感觉hook classLoader的时机非常完美,是在Application的attachBaseContext中进行hook的 ,这个时候是 Appcation刚创建完毕,他的上一步就是创建ContextImpl并保存LoadedApk。感觉非常及时(,不过好像也不用这么早只要下个apk中的类是用自定义classLoader加载的就行?)

3. RePlugin支持插件使用宿主的类

RePlugin 是每一个Plugin都会有一个独立的ClassLoader(PluginDexClassLoader),会优先是用自己的classLoader,如果自己找不到了才回去通过父类查找,这样就支持在不同插件中使用路径和名字完全相同的类

4. gradle plugin 写得确实很优雅,很多之前未见过的写法,及gradle 版本兼容,值得学习

5. 可以通过gradle task 执行adb命令 ,然后进行一些操作

6. handler.postAtFrontOfQueue 这个 api的意思是 发送一个message 而且放到队列的最前面

7. 通过反射删除一个成员的 finel 修饰符 ,真的是厉害啊

  /**
     * 删除final修饰符
     * @param field
     */
    public static void removeFieldFinalModifier(final Field field) {
        // From Apache: FieldUtils.removeFinalModifier()
        Validate.isTrue(field != null, "The field must not be null");

        try {
            if (Modifier.isFinal(field.getModifiers())) {//是否是final类型
                // Do all JREs implement Field with a private ivar called "modifiers"?
                final Field modifiersField = Field.class.getDeclaredField("modifiers");
                final boolean doForceAccess = !modifiersField.isAccessible();
                if (doForceAccess) {
                    modifiersField.setAccessible(true);
                }
                try {
                    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
                } finally {
                    if (doForceAccess) {
                        modifiersField.setAccessible(false);
                    }
                }
            }
        } catch (final NoSuchFieldException ignored) {
            // The field class contains always a modifiers field
        } catch (final IllegalAccessException ignored) {
            // The modifiers field is made accessible
        }
    }

8. Intent的Component 可以用来传递打开Activity的源头参考

9. DexClassLoader 的 optimizedDirectory 和 librarySearchPath 只需要我们指定,不用我们自己去创建,并解析apk


==核心问题==

1. 宿主如何加载插件的类和so文件?

答:首先明确类和so文件都是通过ClassLoader进行加载的,RePlugin中 有两个ClassLoader 一个是宿主的 RePluginClassLoader 他是hook了App的原始 ClassLoader,一个是加载插件Class的 PluginDexClassLoader。当宿主通过RePluginClassLoader加载一个插件里的类时,它先会去使用插件的PluginDexClassLoader去加载,如果找到了就直接返回,如果找不到才会去自己进行加载。具体的可以跟着上面 《宿主启动插件中某Activity流程》走一遍就知道了。

至于为什么 DexClassLoader,其实就是因为 DexClassLoader 在初始化的时候可以传入一个已经优化过的dex文件路径,就可以加载它。 可以动态化可以参考

2. 插件中的资源是如何找到并加载的? 以layout为例

2.1 插件Activity加载自己的layout文件 (比如:demo1插件的 MainActivity 加载 自己的 R.layout.main layout)

首先Activity在创建的时候会创建一个 PhoneWindow ,PhoneWindow在创建的时候回创建一个 LayoutInflater,这个过程中都传递了一个Context,LayoutInflater 会将这个Context记录下来也就是mContext,这个Context其实就是 Activity 的Context。 setContentView( R.layout.main) 最后会调用到 LayoutInflater.infalte()方法,这个时候 就会通过mContext.getResources()获取 Resources 对象,期间会调用到Activity的mBase.getResources方法,最终会调用到ContextImpl的 getResources()方法。

==2.2.1 系统正常启动apk的情况下==

上面提到Resources的获取最终是通过ContextImpl.getResources()方法获取,而ContextImpl中的mResources对象是在构造方法中通过LoadedApk.getResources()方法初始化的如下:

    public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            //通过ApplicationInfo中的 一些文件夹创建 Resources
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
        }
        return mResources;
    }

可以看到创建 Resources 的过程中使用到了 mResDir(apk文件路径,在RePlugin中就是插件的路径) 等这些参数,下面我们先看一下 系统正常启动apk的情况下 mResDir等字段是哪里进程赋值的。

我们都知道在系统启动apk的过程中会通过zygote孵化一个新的进程用于这个APK的运行,当新的进程创建完毕需要将Application和这个进程绑定的时候系统会调用ActivityThread.handleBindApplication,我们就从这里还是看

1.1 ActivityThread.handleBindApplication

private void handleBindApplication(AppBindData data) {
         
         ....
         
          InstrumentationInfo ii = null;
            try {
                //通过 PackageManagerService 解析Apk获取 apk的一些基本信息
                ii = appContext.getPackageManager().
                    getInstrumentationInfo(data.instrumentationName, 0);
            } catch (PackageManager.NameNotFoundException e) {
            }
           

           ....

            //创建 ApplicationInfo 用于记录APP的基本信息 如,包名,apk路径等
            ApplicationInfo instrApp = new ApplicationInfo();
            instrApp.packageName = ii.packageName;
            instrApp.sourceDir = ii.sourceDir;
            instrApp.publicSourceDir = ii.publicSourceDir;
            instrApp.splitSourceDirs = ii.splitSourceDirs;
            instrApp.splitPublicSourceDirs = ii.splitPublicSourceDirs;
            instrApp.dataDir = ii.dataDir;
            instrApp.nativeLibraryDir = ii.nativeLibraryDir;
            //这里创建 LoadedApk 并通过 instrApp记录的一些信息做一些初始化  详见【1.2】
            LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
                    appContext.getClassLoader(), false, true, false);
         
         
         ....
         
          // 此处data.info是指LoadedApk, 通过反射创建目标应用Application对象
           Application app = data.info.makeApplication(data.restrictedBackupMode, null);
         }

1.2 ActivityThread.getPackageInfo

 private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
            ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
            boolean registerPackage) {
                //创建LoadedApk对象 详见【1.3】
                packageInfo =
                    new LoadedApk(this, aInfo, compatInfo, baseLoader,
                            securityViolation, includeCode &&
                            (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
            }

1.3 LoadedApk<init>

    public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
            CompatibilityInfo compatInfo, ClassLoader baseLoader,
            boolean securityViolation, boolean includeCode, boolean registerPackage) {
        final int myUid = Process.myUid();
        aInfo = adjustNativeLibraryPaths(aInfo);

         //ActivityThread对象
        mActivityThread = activityThread;
        mApplicationInfo = aInfo;
        mPackageName = aInfo.packageName;
        mAppDir = aInfo.sourceDir;
        mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
        mSplitAppDirs = aInfo.splitSourceDirs;
        mSplitResDirs = aInfo.uid == myUid ? aInfo.splitSourceDirs : aInfo.splitPublicSourceDirs;
        mOverlayDirs = aInfo.resourceDirs;
        mSharedLibraries = aInfo.sharedLibraryFiles;
        mDataDir = aInfo.dataDir;
        mDataDirFile = mDataDir != null ? new File(mDataDir) : null;
        mLibDir = aInfo.nativeLibraryDir;
        mBaseClassLoader = baseLoader;
        mSecurityViolation = securityViolation;
        mIncludeCode = includeCode;
        mRegisterPackage = registerPackage;
        mDisplayAdjustments.setCompatibilityInfo(compatInfo);
    }

从上面的分析得到,系统正常启动Apk的情况下,系统会在Application创建之前就将 mResDir等信息就赋值给了LoadedApk,后面我们调用getResources就会拿到正确的Resources对象

==2.2.2 看完了正常情况下的,那么RePlugin插件中的Activity是如何正常使用setContentView的呢?==

在上面的描述中我们已经知道在Activity中获取Resources对象会通过mBase.getResources()来获取而且在分replugin-plugin-gradle的时候我们知道插件在编译器会将期继承的Activity替换为PluginActivity等Replugin内部提供的Activity,那么我们来看一下 PluginActivity 中有什么玄机吗?
果真在 PluginActivity中通过如下调用链将Activity的mBase替换成了PluginContext对象,所以在Activity中获取Resources对象最终会走到PluginContext.getResource

PluginContext.getResource方法如下

 public Resources getResources() {
        if (mNewResources != null) {
            return mNewResources;
        }
        return super.getResources();
    }

他只是返回了mNewResources,mNewResources是在PluginContext的构造犯法中赋值的,PluginContext是通过如下调用链创建的(==具体可以看调用链中的内容==)

==2.2.3:总结==

可以看出来系统启动APP和我们动态加载插件完全是不一样的思路。

2.2 插件中使用其他插件的layout文件

通过如下调用链就可以获取到具体的View

3. 插件中的so文件是如何加载的?


3. RePlugin中的核心

  1. ClassLoader : DexClassLoader 和 RePluginClassLoader
  2. Context:PluginContext

问答

1. ==RePlugin是使用DexClassLoader加载自定义路径下的dex吗?==

答:是的,在Android中 DexClassLoader 总是动态话的不二选择,只不过 RePlugin中 有两个ClassLoader 一个是宿主的 RePluginClassLoader 他是hook了App的原始 ClassLoader,一个是加载插件Class的 PluginDexClassLoader。当宿主通过RePluginClassLoader加载一个插件里的类时,它先会去使用插件的PluginDexClassLoader去加载,如果找到了就直接返回,如果找不到才会去自己进行加载。

至于为什么 DexClassLoader,其实就是因为 DexClassLoader 在初始化的时候可以传入一个已经优化过的dex文件路径,就可以加载它。 可以动态化可以参考

2. ProcessPitProviderPersist这个provider对外提供binder然后进行通信,这样做不会有安全问题吗?

3. replugin-host-lib中 manifest中 配置的爆红的 四大组件是干嘛的?

4. RePlugin.attachBaseContext方法中有提到 HostConfigHelper.init();需要在IPC.init只有进行,那是不是说常驻进程名肯定就是独立进程?配置了也没用?

答:有用的,不知道为啥会有那句注释

5. PluginManagerProxy.connectToServer()是在干啥?

答:通过 binder 获取到 PluginManagerServer.Stub 对象也就是 sRemote

6. StubProcessManager.schedulePluginProcessLoop这是在干啥?

答:应该是在回收无用进程

7. Plugin.attach 中的parent参数是已经被 hook的 classLoader了吗?

答:这个是没有被 hook过的 ,因为这个 是在PmBase中初始化的,PmBase这个类是在 hook之前加载的

8. ==replugin-plugin-gradle中为什么要替换 getIdentifier 的三个参数为当前 插件包名?不替换行不行?==

答:难道这个参数在解析.resc文件时会用到,记得在ResGuard中就有解析packageName的时候,应该是这样的,也不对啊,它传的的是调用方的包名...搞不懂

9. com.qihoo360.replugin.Entry 这个类在哪里?里面的 create 干了些啥?在Loader.loadEntryMethod3 方法中有使用到?

答:这个类位于 replugin-plugin-lib中,crate是宿主框架最先调用的类 用于初始化插件框架和环境

10. 动态类是干啥的? RePlugin.registerHookingClass 中会注册?

答:在加载插件类的时候会用到 具体使用位置是 PmBase.loadClass,作用是作为真实类加载之前的 中介类,具体能干啥 还不太清楚,不过看描述很强大的感觉

11. ==PluginLibraryInternalProxy.startActivity 不是只是打开坑位Activity么?插件的Activity怎么显示的?也就是Android 系统怎么被骗==了?

答:整体调用流程如下:

12. 常驻进程什么时候启动的?

答:是在ui进程启动的过程中 通过 PluginProcessMain.connectToHostSvc 这个方法触发 ProcessPitProviderPersist(运行在常驻进程)这个内容提供者初始话而启动的

13. RePlugin是如何避免资源冲突的?

答:Replugin中宿主和插件,插件和插件之间不会存在 资源冲突,因为 他们的资源压根就不会合并。

14. data/data/包名/files 下的文件是什么时候复制过去的?

15. p-n 插件 指的是啥?

16. V5插件是什么鬼?

上一篇下一篇

猜你喜欢

热点阅读