Replugin 全面解析(1)

前言

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中的注册代码:

      
      

      注意,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 plugins = null;
        try {
            plugins = PluginProcessMain.getPluginHost().listPlugins(); // 获取插件
        } catch (Throwable e) {
        }
    
        List 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 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))