Replugin 全面解析(1)

作者: 蒋扬海 | 来源:发表于2017-08-05 21:51 被阅读4614次

前言

Replugin 已经开源一个月了,最近几天终于抽出时间来研究研究,这里将我的一些心得体会写下来,分享给大家,希望能帮助后来者少走弯路。关于 Replugin 的基本介绍及起优缺点网上已经有一些不错的文章,大家可以搜索一下,很容易就能找到。这篇文章的主要目标是介绍 Replugin 的一些核心概念以及一些核心流程,让大家了解 Replugin 的运作原理。这其中包括 Host 的启动流程,插件的加载和启动流程,坑位的原理等。开发团队利用了一些非常巧妙的方法使得整个框架在只有一个 Hook 点的情况下支持 android 原生的大部分特性,不得不说这一点很厉害,无论系统如何升级,国内厂商如何定制系统,都不会影响这个框架的运行,除非他们连 ClassLoader 都能干掉。当然在阅读源码的过程中,也发现整个代码质量还有提高和优化的空间,另外有一些小设计上有点复杂,如果开发团队有时间能重构优化一下就好了。当然,瑕不掩瑜,这个框架值得大家学习和借鉴!!

阅读提示

  • 这个系列一共有5篇文章,对核心原理和四大组件分别进行讲解
  • 文章中的代码都是从 Replugin 源码中搬过来的,但省略了一些部分以便于讲解,代码中的注释大部分是作者本人所加,便于理解代码,也能缩减讲解的篇幅,在阅读时请不要忽略注释。
  • 由于代码分支较多,为了方便讲解,我在一些注释中标注了A,B,C等用于标记分支代码
  • 要完全了解Replugin的一些源码,你需要能够理解Binder通信机制的原理,android中ClassLoader的原理,以及对四大组件的启动流程有所了解。

目录

  • 核心概念
    • Hook点
    • UI进程,Persistent进程
    • 坑位
  • Host启动流程
    • UI进程启动流程
    • Persistent进程启动

核心概念

  • 唯一Hook点:RepluginClassLoader

    在应用启动的时候,Replugin使用RepluginClassLoader将系统的PathClassLoader替换掉,并且只篡改了loadClass方法的行为,用于加载插件的类,后面我们会详细讲解。每一个插件都会有一个PluginDexClassLoader,RepluginClassLoader会调用插件的PluginDexClassLoader来加载插件中的类与资源。

    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
      Class<?> c = null;
      c = PMF.loadClass(className, resolve);   //主力这里就是Hook点,先使用插件的
      if (c != null) {                         //PluginDexClassLoader加载
          return c;
      }
      //只有在插件没有找到相应的类,才使用系统原来的PathClassLoader加载宿主中的类
      try {
      c = mOrig.loadClass(className);
          return c;
      } catch (Throwable e) {
      }
          
      return super.loadClass(className, resolve);
    }
    
  • UI进程,Persistent进程

    Replugin启动时会默认启动两个进程,一个是UI进程,一个是Persistent进程(常驻进程),在IPluginManager接口中定义了两个常量PROCESS_UIPROCESS_PERSIST来表示这两个进程。

    public interface IPluginManager {
        int PROCESS_UI = -1;        //UI进程
        int PROCESS_PERSIST = -2;   //Persistent进程
    }
    

    UI进程很好理解,就是程序的主进程。

    Persistent进程是一个服务器进程,默认用:GuardService来标示,它是Replugin的核心之一。所有其他的进程在启动组件的时候都会通过PmHostSvc 与这个进程通信,以下是Persistent进程中运行的两个重要服务:

    • PluginManagerServer 用于插件的管理,比如加载插件,更新插件信息,签名验证,版本检查,插件卸载等
    • PluginServiceServer 用于Service的启动调度等工作
  • 坑位

    坑位是Replugin中设计非常巧妙的一个概念,它的功能是与RepluginClassLoader配合才能实现的。所谓坑位就是预先在Host的Manifest中注册的一些组件(Activity, Service, Content Provider,唯独没有Broadcast Receiver),叫做坑位。这些坑位组件的代码都是由gradle插件在编译时生成的,他们实际上并不会被用到。在启动插件的组件时,会用这些坑位去替代要启动的组件,并且会建立一个坑位与真实组件之间的对应关系(用ActivityState表示),然后在加载类的时候RepluginClassLoader 会根据前文提到的被篡改过的行为偷偷使用插件的PluginDexClassLoader加载要启动的真实组件类,骗过了系统,这就是唯一hook点的作用。​

Host启动流程

Host在启动的时候会先进行UI进程的初始化工作,但在进行到中途的时候会巧妙的将Persistent进程启动起来,以提供服务,不然UI进程将无法正常启动起来,因为有很多东西时运行在Persistent进程的。

  • UI进程启动流程

    • 入口位置RePluginApplication.attachBaseContext,紧接着调用Replugin.App.attachBaseContext

      请注意,下面的代码中有一个注释中标注来“分支A",这个分支会在后面讲到!!!

      public static void attachBaseContext(Application app, RePluginConfig config) {
          ......
          RePluginInternal.init(app);
          sConfig = config;
          sConfig.initDefaults(app);
                  
          IPC.init(app);   //初始化进程信息,判断当前进程是UI进程还是Persistent进程
          ......
          PMF.init(app);    //初始化当前进程
          PMF.callAttach(); //分支A: 将插件与当前进程关联,如果是在单独的进程中运行插件,则会加载并运行插件
      
          sAttached = true;
      }
      
    • 来看 PMF.init(app),这个函数会做两件事情,初始化PmBase以及Hook系统的PathClassLoader。

      public static final void init(Application application) {
          setApplicationContext(application);
          PluginManager.init(application);
      
          sPluginMgr = new PmBase(application);
          sPluginMgr.init();  
          ......
          PatchClassLoaderUtils.patch(application);   //Hook系统Loader,这里是系统唯一Hook点
      }
      
    • PmBase.int()函数在UI进程和Persistent进程中会运行不同的分支,我们这里来看UI进程相关的部分。

      请注意注释中分支B的存在,后面会见讲到!

      void init() {
          ......
          PluginProcessMain.installHost(); // 连接到Persistent进程
          initForClient();     //分支B: 初始化UI进程,主要是更新一些插件相关信息
          ......
      }
      
    • PluginProcessMain.installHost首先获取与Persistent进程通信的IBinder对象,然后连接到Persistent进程中的IPluginManagerServer服务对象(其实就是获取到Binder通信机制中作为客户端的代理对象),到这里运行Replugin的基础设施就已经准备好了。

      static final void installHost() {
          Context context = PMF.getApplicationContext()
          //获取与Persistent进程通信的IBinder对象
          IBinder binder = PluginProviderStub.proxyFetchHostBinder(context);
          ......
          sPluginHostRemote = IPluginHost.Stub.asInterface(binder);
          
           //连接到插件化管理器的服务端
          PluginManagerProxy.connectToServer(sPluginHostRemote);
          ......
      }
      
    • 在上一步中,有一个重点没有讲到,那就是获取IBinder对象这一步PluginProviderStub.proxyFetchHostBinder

      private static final IBinder proxyFetchHostBinder(Context context, String selection) {
          Cursor cursor = null;
          try {
              Uri uri = ProcessPitProviderPersist.URI;
              cursor = context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null); // 访问ProcessPitProviderPersist
              IBinder binder = BinderCursor.getBinder(cursor);
              return binder;
          } finally {
              CloseableUtils.closeQuietly(cursor);
          }
      }
      

      当前进程尝试通过ContentResolver去访问ProcessPitProviderPersist以获取一个与Persistent进程通信的IBinder对象,但是ProcessPitProviderPersist在第一次被访问时并没有运行起来,于是Android系统会自动启动它。但是请看ProcessPitProviderPersist在Manifest中的注册代码:

      <provider                   android:name="com.qihoo360.replugin.component.process.ProcessPitProviderPersist"
      android:authorities="${applicationId}.loader.p.main"
      android:exported="false"
      android:process=":GuardService" />
      

      注意,android:process=":GuardService"表示ProcessPitProviderPersist会被运行在另外一个叫做GuardService的进程中,于是Android系统立即通过ActivityManagerService向Zygote进程请求folk一个新的进程,ProcessPitProviderPersist就运行在这个进程中,这个进程就是Persistent进程了。

      有三点你需要知道:

      • 第一,默认情况下,GuardService会被当作Persistent进程的名字,在IPC.init()函数中会用这个名字来判断当前进程是不是Persistent进程。

      • 第二,有很多坑位组件使用android:process=":GuardService"属性,因此如果Persistent进程不小心被杀掉了,在任何需要启动这些坑位组件的地方都会将Persistent进程重新启动起来。

      • 第三,系统在启动新进程的时候,会在新进程中执行RepluginApplication的初始化,所以以上提到的流程都会在这个进程中执行一遍,但是因为在PmBase.init()函数中有一个条件判断IPC.isPersistentProcess(),Persistent进程会执行和UI进程不同的代码路径。

上面我们顺着一条线走通了,接着我们来看看在前面的代码中标记过的代码分支A和B

  • 分支B,PmBase.initForClient() 会通过远程调用向服务端PmHostSvc获取所有插件的信息,这些信息是在Persistent进程的启动流程(后面会讲到)中被加载的,接着会判断是否有更新,如果有插件已经更新了,会通过远程调用让PluginManagerServer重新加载插件。

    private final void initForClient() {
        List<PluginInfo> plugins = null;
        try {
            plugins = PluginProcessMain.getPluginHost().listPlugins(); // 获取插件
        } catch (Throwable e) {
        }
    
        List<PluginInfo> updatedPlugins = null;
        if (isNeedToUpdate(plugins)) {
            try {
                updatedPlugins = PluginManagerProxy.updateAllPlugins(); // 更新插件
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 分支A,PMF.callAttach()其实就是调用PmBase.callAttach(),首先将插件与当前进程关联起来,主要是将RepluginClassLoaderPluginCommImpl赋值给插件,它们会在插件真正加载运行时被用到。 如果插件启动了自己的进程来运行,那么在插件的进程中会真正的去运行插件,插件运行过程在本文的后面部分会详细讲解。

    final void callAttach() {
        mClassLoader = PmBase.class.getClassLoader();  // 获取RepluginClassLoader
        for (Plugin p : mPlugins.values()) {
            p.attach(mContext, mClassLoader, mLocal);  // 将分支B中获取的插件与当前进程关联
        }
    
        if (PluginManager.isPluginProcess()) {   //如果插件启动了自己单独的进程,就会启动插件
            if (!TextUtils.isEmpty(mDefaultPluginName)) {
                Plugin p = mPlugins.get(mDefaultPluginName);
                if (p != null) {
                    boolean rc = p.load(Plugin.LOAD_APP, true);
                    if (rc) {
                        mDefaultPlugin = p;
                        mClient.init(p);
                    }
                }
            }
        }
    }
    

以上是UI进程启动中的一些重要流程,接着我们来看看Persistent进程启动流程中的一些要点。

  • Persistent进程启动

    • Persitent进程的启动流程前面几个步骤跟UI进程是一样的,这里就不重复,我们开始从不同的地方讲起。还记得上面提高过的PmBase.init()函数里面的IPC.isPersistentProcess()判断吗?在Persistent进程里这个判断返回true,于是Pmbase.init()将执行以下的分支代码:

      void init() {
          mHostSvc = new PmHostSvc(mContext, this);  //前面提高过的PmHostSvc终于出现啦!!!
          PluginProcessMain.installHost(mHostSvc);   
          initForPersistent();
      }
      
    • 在Persistent进程中也会通过PluginProcessMain.installHost(mHostSvc)连接到IPluginManagerServer,但因为IPluginManagerServer就运行在当前进程,因此这里不会进行Binder通信,而是直接调用PmHostSvc端fetchManagerServer方法。

    • initForPersistent会加载加载插件并保存起来,这样所有作为客户端的进程才能获取到插件信息。

      private final void initForPersistent() {
          //这三行识为了兼容儿存在,以后会被废弃掉,所以不用太关注
          mAll = new Builder.PxAll();
          Builder.builder(mContext, mAll);
          refreshPluginMap(mAll.getPlugins());
      
          try {
              List<PluginInfo> l = PluginManagerProxy.load(); // 加载插件
              if (l != null) {
                  refreshPluginMap(l);    // 将获取到的插件信息保存在 PmBase.mPlugins中
              }
          } catch (RemoteException e) {
          }
      }
      
    • 顺着PluginManagerProxy.load()跟踪下去,最后真正做加载工作的是PluginInfoList.load()函数。Constant.LOCAL_PLUGIN_APK_SUB_DIR就是插件安装以后的存放目录。

      public boolean load(Context context) {
           try {    
              File d = context.getDir(Constant.LOCAL_PLUGIN_APK_SUB_DIR, 0);
              File f = new File(d, "p.l");   
              ......
              // 从配置文件p.l中读取插件信息,插件信息以JSON格式保存在这个文件中
              String result = FileUtils.readFileToString(f, Charsets.UTF_8); //读出字符串
              ......
              mJson = new JSONArray(result);  //解析出JSON
          } catch (IOException e) {
              return false;
          } catch (JSONException e) {
              return false;
          }
      
          for (int i = 0; i < mJson.length(); i++) {
              JSONObject jo = mJson.optJSONObject(i);
              if (jo != null) {
                  PluginInfo pi = PluginInfo.createByJO(jo); //创建PluginInfo对象
                  if (pi == null) {
                      continue;
                  }
                  addToMap(pi);      //保存插件信息
              }
          }
          return true;
      }
      

小结

RepluginClassLoader 和坑位机制是 Replugin 最重要的两个基本概念,对四大组件的支持基本都是在此基础上架构起来的!

另外Replugin中的进程关系也有一些复杂,在后面的文章中会详细讲解。

下一篇Replugin 全面解析(2) 会讲解插件Activity加载和启动流程!

相关文章

  • Replugin 全面解析(1)

    前言 Replugin 已经开源一个月了,最近几天终于抽出时间来研究研究,这里将我的一些心得体会写下来,分享给大家...

  • Replugin 全面解析(3)

    上一篇分析中我们分析了Replugin框架Host端的一些核心概念,还梳理了Activity启动的流程,但是有两个...

  • Replugin 全面解析 (2)

    Activity作为四大组件中最重要的组件,在Replugin中对它的支持的架构设计也是最复杂的,所以本篇分析我们...

  • Replugin 全面解析 (4)

    在前两篇分析的基础上,这篇我们来看看Replugin是如何支持Service组件的。 本篇会包含以下内容: Ser...

  • Replugin 全面解析(5)

    本篇我们来看看四大组件中的BroadcaseReceiver和ContentProvider。总体来说,这两个组件...

  • RePlugin 插件化框架介绍与使用说明

    RePlugin GitHub 主页RePlugin Wiki 主页RePlugin 原理剖析全面插件化:RePl...

  • RePlugin使用总结

    Replugin是什么?由360推出的(完整的?稳定的?适合全面使用的?)插件优化方案RePlugin项目地址 1...

  • Replugin源码解析之replugin-plugin-gra

    概述 该部分基础知识在Gradle学习-----Gradle自定义插件及Replugin源码解析之replugin...

  • 全面插件化时代RePlugin来临

    一、RePlugin简介 RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案。我们“逐>词”拆...

  • 360 RePlugin插件化-项目接入

    RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Tea...

网友评论

  • 豆奶不好喝:大神。可以修改坑位的配置吗?因为坑位默认是竖屏的,想修改横屏怎么办啊?
  • 过期的薯条:不知道楼主用过360的 DroidPlugin没 ? 今天项目中需要 刚使用了下,发现有些apk插件依旧无法打开,而且打开以后 一些操作搞不定,不知道是不是因为那个操作调用so的原因。。
    需求是 打开插件apk进行车牌解析。不知道Replugin如何
  • hugobada:你好,拜读你的文章收获很大,感谢
    我想请教一个问题,插件加载默认是在UI进程还是GuardService进程,比如replugin官方示例代码中的demo1,demo2是在UI进程加载的吗
    蒋扬海:@hugobada 不客气哈
    hugobada:@神罗天征_39a0 感谢回复,确实,我跟代码发现是在UI进程,一直以为加载插件会在guardService进程,说明我没看错代码(因为实在不太相信我看到的结论),再次感谢博主的答复
    蒋扬海:@hugobada 我记不大清楚了,不过很跟一下代码应该很容易能找到。不过我记得到无论是在哪个进程加载的,都会把插件信息同步到guardservice里面去,所以我推断应该是在UI进程
  • cmeiyuan:纠正个笔误:
    分支B,PMF.callAttach()其实就是调用PmBase.callAttach()
    应该是分支A
    蒋扬海: @cmeiyuan 已更新
    蒋扬海: @cmeiyuan 多谢
  • 大庸自在人:大神,听说你出书了:+1:
    大庸自在人:@神罗天征_39a0 OK,好的
    蒋扬海: @湘西来的土匪 请关注,《kotlin 实战》,多谢!

本文标题:Replugin 全面解析(1)

本文链接:https://www.haomeiwen.com/subject/zybwlxtx.html