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

      <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加载和启动流程!

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,834评论 25 707
  • 在前两篇分析的基础上,这篇我们来看看Replugin是如何支持Service组件的。 本篇会包含以下内容: Ser...
    蒋扬海阅读 9,279评论 6 10
  • Activity作为四大组件中最重要的组件,在Replugin中对它的支持的架构设计也是最复杂的,所以本篇分析我们...
    蒋扬海阅读 10,494评论 2 17
  • 请教个问题:刚才在朋友圈看到一条很好的分享,给了购买链接,分享的朋友说“并没有收取任何好处”。以前我看到这样的分享...
    一滴水_5977阅读 319评论 0 0
  • 文|静水 1 大学毕业前夕,我在省会一家会计师事务所实习。当时带我的师傅姓杜,我对师傅毕恭毕敬,每天第一个到办公室...
    静水的人生阅读 390评论 0 1