插件框架-RePlugin源码阅读

写在前面

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

* 插件化现状

插件化目前的处境肯定是大不如前,由于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 :
    主程序使用的Gradle插件,主要职责是在我们的主程序打包的过程中(编译的过程中)动态的修改AndroidManifest.xml的信息,动态的生成占位各种Activity、provider和service的声明。
  • replugin-host-library :
    这个库是要由主程序依赖的,也是Replugin的核心,它的主要职责是初始化Replugin的整体框架,整体框架使用了Binder机制来实现多进程直接的的沟通和数据共享,或者说是插件之间和宿主之间沟通和数据共享,hook住ClassLoader,加载插件、启动插件、多插件的管理全部都与这个库辅助
  • replugin-plugin-gradle :
    这个是插件工程使用的Gradle的插件,这个库使用了Transfrom API和Javassist实现了编译期间动态的修改字节码文件,主要是替换插件工程中的Activity的继承全部替换成Replugin库中定义的XXXActivity。动态的将插件apk中调用LocalBroadcastManager的地方修改为Replugin中的PluginLocalBroadcastManager调用,动态修改ContentResolver和ContentProviderClient的调用修改成Replugin调用,动态的修改插件工程中所有调用Resource.getIdentifier方法的地方,将第三参数修改为插件工程的包名
  • replugin-plugin-library :
    这个库是由插件工程依赖的,这个库的主要目的是通过反射的方式来使用主程序中接口和功能,这个库在出程序加载加载插件apk后会进行初始化。

各模块解析

replugin-host-gradle

主要职责

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

replugin-host-library

各类职责

运行于常驻进程 (常驻进程主要用于插件管理和Service(四大组件)维护)
  • PmHostSvc(binder对象):这个类可以理解成是我们的Server端,它直接或间接参与了Server端要做的所有事情
  • PluginServiceServer(binder对象):主要负责了对Service的提供和调度工作,例如startService、stopService、bindService、unbindService全部都由这个类管理
  • PluginManagerServer(binder对象):掌管了所有对插件的的操作,例如插件的安装、加载、卸载、更新等等
  • Builder.PxAll : 缓存所有(各种类型)插件
运行于ui进程
  • RePlugin:RePlugin的对外入口类 ,宿主App可直接调用此类中的方法,来使用插件化的几乎全部的逻辑。
  • IPC:用于“进程间通信”的类。插件和宿主可使用此类来做一些跨进程发送广播、判断进程等工作。
  • PMF:框架和主程序接口代码
  • PmBase:具有很多重要的功能,例如:分配坑位、初始化插件信息、Clent端连接Server端、加载插件、更新插件、删除插件、等等
  • PluginProcessPer:它是一个Binder对象,它代表了“当前Clent端”,使用它来和Server端进行通信
  • LaunchModeStates:存储 LaunchMode + Theme -> 此种组合下的 ActivityState 状态集合
  • ActivityState:坑位与真实组件之间的对应关系
  • PluginContainers:用来管理Activity坑位信息的容器,初始化了多种不同启动模式和样式Activity的坑位信息。
  • PluginCommImpl:负责宿主与插件、插件间的互通,很多对提供方法都经过这里中转或者最终调到这里
  • PluginLibraryInternalProxy:Replugin框架中内部逻辑使用的很多方法都在这里,包括插件中通过“反射”调用的内部逻辑如PluginActivity类的调用、Factory2等
  • RePluginClassLoader:用于替代宿主原有PathClassLoader的工作
  • PluginDexClassLoader:个用来加载插件apk的类
  • PluginProcessMain:进程管理类
  • IPluginManagerServer(aidl文件):插件管理器。用来控制插件的安装、卸载、获取等。运行在常驻进程中
  • IPluginHost(aidl文件):涉及到插件交互、运行机制有关的管理器
  • StubProcessManager:坑位进程管理
  • PluginManagerProxy:用于各进程(包括常驻自己)缓存 PluginManagerServer 的Binder实现
  • PluginContext:插件要用的 Context
  • PluginApplicationClient: 一种能处理【插件】的Application的类
  • RePluginInternal:主要功能是缓存了 Context(宿主Application) 对象,并对外提供
  • PluginInfo:用来描述插件,通过解析json生成
  • PluginProviderStub:用于客户端进程通过 ContentProvider 获取常驻进程 binder对象等操作

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

各类职责

  • Entry:宿主框架最先调用的类 用于初始化框架和环境
  • RePluginServiceManager:插件内部向外提供服务的管理实现类

大体工作流程

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

阅读要点

标识解读

  • N1 : UI 进程标识
  • P{n} : 自定义进程标识
  • NR : launchMode为 Standard
  • STP: launchMode为 LAUNCH_SINGLE_TOP
  • ST: launchMode为 LAUNCH_SINGLE_TASK
  • SI:launchMode为 LAUNCH_SINGLE_INSTANCE
  • NTS :表示坑的 theme 为不透明
  • TS:表示坑的 theme 为透明
  • p_n插件:
  • 纯APP插件:

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

  • app_plugins_v3_libs:内置插件等的 so文件存放目录
  • app_p_c:存放可以覆盖更新的插件 so文件
  • app_p_n:纯"APK"插件的 so文件存放目录?

阅读时注意的点

  • 每一个插件(Plugin)都由一个Loader,每个 Loader都由一个 ComponentList

调用链

host初始化

  • RePluginApplication.attachBaseContext
    • RePlugin.App.attachBaseContext
      • IPC.init() : 确认常驻进程名 和 当前进程是那种进程(主进程or常驻进程or其他)
      • PMF.init:
        • PluginManager.init
          • 初始化主线程handler
          • 通过当前进程的名字 获取 进程对应的 int值
        • PmBase.<init> : (所有进程都会调用)
          • PluginProcessPer.<init> :
            • PluginServiceServer.<init>
            • PluginContainers.init() : 初始化坑位
          • PluginCommImpl.<init>
          • PluginLibraryInternalProxy.<init>
        • PmBase.init() : 判断是否使用常驻进程作为服务进程,并对服务进程和客户进程分别初始化
          • PmBase.initForServer(常驻进程中的操作) : 初始化服务进程,最主要的操作就是 将所有插件信息缓存到 PmBase.mPlugins 数组中
            • Builder.builder : 整理插件并缓存到 PxAll中
            • PmBase.refreshPluginMap : 将插件信息全部缓存到 mPlugins 中
            • PluginManagerProxy.load : 加载纯 APP插件?
            • PmBase.refreshPluginMap : 更新 mPlugins 中信息
          • PmBase.initForClient(客户端进程中的操作):1. 链接常驻进程,2. 获取插件信息
            • PluginProcessMain.connectToHostSvc(): 连接常驻进程(初始化用于通信的binder代理对象)
              • PluginProviderStub.proxyFetchHostBinder : 获取常驻进程b PmHostSvc inder 对象
              • IPluginHost.Stub.asInterface(binder) : 获取PmHostSvc inder 对象的代理对象
              • PluginManagerProxy.connectToServer : 初始化 PluginManagerProxy.sRemote 对象用于和常驻进程通信
              • PluginManagerProxy.syncRunningPlugins() : 和常驻进程同步插件运行列表
              • PmBase.attach : 注册该进程信息到“插件管理进程”中?
            • PmBase.refreshPluginsFromHostSvc : 从常驻进程获取插件列表,将插件信息全部缓存到 mPlugins 中
          • PluginTable.initPlugins : 创建一份 最新快照到 PluginTable.PLUGINS
        • PatchClassLoaderUtils.patch : hook App的classLoader 为 RePluginClassLoader
      • PMF.callAttach()
        • PmBase.callAttach()
          • Plugin.load(); 加载并启动插件
            • Plugin.loadLocked() 加载插件
              • Plugin.doLoad(): 加载插件信息、资源、Dex,并运行Entry类
                • Loader.loadDex():
                  • PackageManager.getPackageArchiveInfo : 获取插件的 PackageInfo
                  • mPackageInfo.applicationInfo.sourceDir : 设置插件的路径,这个地址后面再获取插件的Resources对象时会用到(设置之前是空的)
                  • mPackageInfo.applicationInfo.publicSourceDir : 同上
                  • mPackageInfo.applicationInfo.nativeLibraryDir :设置 so文件存放 路径 ,插件加载so时会使用
                  • pm.getResourcesForApplication : 创建插件使用的Resources对象
                  • RePlugin.getConfig().getCallbacks().createPluginClassLoader : 创建插件的ClassLoader
                  • new PluginContext : 创建插件使用的 Context (PluginContext),
              • Plugin.loadEntryLocked(): 会反射加载插件中的Entry类以初始化插件框架和环境
            • Plugin.callApp(): 启动并初始化插件Application,
              • Plugin.callAppLocked :
                • PluginApplicationClient.getOrCreate : 创建插件的Application对象
                  • PluginApplicationClient.<init> :
                  • PluginApplicationClient.initCustom : 创建插件的Application对象
                • PluginApplicationClient.callAttachBaseContext : 将插件使用的Context 通过Application传给插件 (PluginContext)
  • RePluginApplication.onCreate
    • RePlugin.App.onCreate()
      • PMF.callAppCreate
        • PmBase.callAppCreate
          • 常驻进程:获取cookie
          • 其他进程注册 安装插件和卸载插件的广播
        • PluginInfoUpdater.register():非常驻进程注册监听PluginInfo变化的广播以接受来自常驻进程的更新

宿主启动插件中某Activity流程

  • RePlugin.startActivity
    • Factory.startActivityWithNoInjectCN
      • PluginCommImpl.startActivity
        • PluginLibraryInternalProxy.startActivity(5个参数)
          • PluginCommImpl.loadPluginActivity : 找到坑位activity 并封装为 ComponentName 再返回,这个过程中还会给Intent塞一下参数,比如 插件名称
            • MP.startPluginProcess : 启动目标进程 并获取PluginProcessPer的binder 代理对象 用于通信
            • client.allocActivityContainer : 远程分配坑位并返回
          • context.startActivity : 打开坑位Activity,这个时候App就要调用classLoader加载坑位Activity了,但是App的ClassLoader被我们hook成为了 RePluginClassLoader ,所以也就是使用 RePluginClassLoader 来加载 坑位activity,下面我么那就继续看这个流程
          • RePluginClassLoader. loadClass : 记载类
            • PMF.loadClass :
              • PmBase.loadClass :
                • PluginProcessPer.resolveActivityClass :
                  • PluginDexClassLoader.loadClass : 使用插件classLoader 加载类

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

插件中启动activity

1. 插件中直接或者间接(使用activity中使用view.getContext)通过Activity.startActivity打开宿主Activity
因为在编译器 插件中Activity的父类都被改变为继承自 PluginActivity等Replugin 提供的Activity,所以他们的 startActivity 都会以 PluginActivity等的 startActivity为起点
  • PluginActivity.PluginActivity
    • RePluginInternal.startActivity
      • ProxyRePluginInternalVar.startActivity.call ;反射调用 宿主工程中的 com.qihoo360.i.Factory2.startActivity 方法
        • PluginLibraryInternalProxy.startActivity(2个参数) :
          • PluginLibraryInternalProxy.fetchPluginByPitActivity : 获取插件名
          • Factory.startActivityWithNoInjectCN :
            • PluginCommImpl.startActivity:
              • PluginLibraryInternalProxy.startActivity(5个参数)
                • PluginCommImpl.loadPluginActivity:执行这个方法时 因为没有在插件中肯定找不到宿主的Activity 所以会直接返回false,然后一层层退出到 RePluginInternal 中
    • super.startActivity : 正常启动activity
2. 插件中使用Application的context打开Activity (==要走两次PluginContext.startActivity==)

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

  • PluginContext.startActivity(Intent intent): 第一次 是替换intent中要打开的Activity为 坑位activity
    • Factory2.startActivity(Context context, Intent intent) : 这次返回true
      • PluginLibraryInternalProxy.startActivity(2个参数) :
      • PluginLibraryInternalProxy.fetchPluginByPitActivity : 获取插件名
        • Factory.startActivityWithNoInjectCN :
          • PluginCommImpl.startActivity:
            • PluginLibraryInternalProxy.startActivity(5个参数) : 替换目标为坑位Activity
              • context.startActivity : 这里又调用了一次Context.startActivity,所以又回到了 PluginContext.startActivity中
  • PluginContext.startActivity(Intent intent): 第二次是打开坑位activity,欺骗系统打开 目标activity
    • Factory2.startActivity : 这次返回false
    • super.startActivity(intent) : 真正的打开activity
可以看到 插件中启动activity最终都走到了 PluginLibraryInternalProxy.startActivity(5个参数) 这个方法
3. 插件中正常(走的是Activity的startActivity)打开插件中的Activity (==要走两次Activity.startActivity==)
  • PluginActivity.startActivity : 第一次
    • Factory2.startActivity(Activity activity, Intent intent) : 反射调用 这次返回true
      • PluginLibraryInternalProxy.startActivity(2个参数) :
      • PluginLibraryInternalProxy.fetchPluginByPitActivity : 获取插件名
        • Factory.startActivityWithNoInjectCN :
          • PluginCommImpl.startActivity:
            • PluginLibraryInternalProxy.startActivity(5个参数): 替换目标为坑位Activity
              • context.startActivity : 这里的 context其实是 Activity
  • PluginActivity.startActivity : 第二次
    • Factory2.startActivity(Activity activity, Intent intent) : 反射调用 这次返回false
      • super.startActivity(intent) : 真正的打开activity
4. 插件中打开宿主中Activity

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

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

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

  • RePluginInternal.handleActivityCreateBefore : 对FragmentActivity做特殊处理
  • super.onCreate() : PluginActivity 父类的 onCreate
  • RePluginInternal.handleActivityCreate : 填充一下必要的东西,比如 lable等?

学到的

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

  • PluginActivity.attachBaseContext
    • RePluginInternal.createActivityContext
      • ProxyRePluginInternalVar.createActivityContext.call

PluginContext.getResource方法如下

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

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

  • Plugin.load(); 加载并启动插件
    • Plugin.loadLocked() 加载插件
      • Plugin.doLoad(): 加载插件信息、资源、Dex,并运行Entry类
        • Loader.loadDex():
          • PackageManager.getPackageArchiveInfo : 获取插件的 PackageInfo
          • mPackageInfo.applicationInfo.sourceDir : 设置插件的路径,这个地址后面再获取插件的Resources对象时会用到(设置之前是空的)
          • pm.getResourcesForApplication : 创建插件使用的Resources对象
          • RePlugin.getConfig().getCallbacks().createPluginClassLoader : 创建插件的ClassLoader
          • new PluginContext : 创建插件使用的 Context (PluginContext),
==2.2.3:总结==

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

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

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

  • RePlugin.fetchViewByLayoutName
    • RePluginCompat.fetchViewByLayoutName
      • RePlugin.fetchContext : 加载插件,并获取插件自身的Context对象,以获取资源等信息
      • RePluginCompat.fetchResourceIdByName : 要注意一下的是 在插件编译期 这个方法最后调用的 Resources.getIdentifier 的第三个参数被替换成了 插件的包名,至于为什么现在还不知道(==关注问题中的第八个,会后会在哪里解答==)
        • RePlugin.fetchPackageInfo : 获取 插件对应的 PackageInfo
        • RePlugin.fetchResources : 加载插件,并获取插件的资源信息
        • Resources.getIdentifier : 获取资源id,
      • LayoutInflater.from(context).inflate : 填充为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 系统怎么被骗==了?

答:整体调用流程如下:

  • Pmbase根据Intent找到对应的插件
  • 分配坑位Activity,与插件中的Activity建立一对一的关系并保存在PluginContainer中
  • 让系统启动坑位Activity,因为它是在Manifest中注册过的
  • Android系统会尝试使用RepluginClassLoader加载坑位Activity的Class对象
  • RepluginClassLoader 通过建立的对应关系找到插件Activity,并使用PluginDexClassLoader 加载插件Activity 的Class对象并返回
  • Android系统就使用这个插件中的Activity的Class对象来运行生命周期函数
  • 让系统以为是自己的classLoader加载的类但是其实是使用插件ClassLoader加载的然后给到系统,这一招偷梁换柱 真的是高啊 。到此 Android系统就被 骗啦
    ==这样狸猫换太子也太6了==
  • 参考:Replugin 全面解析 (2)

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

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

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

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

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

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

16. V5插件是什么鬼?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容