Replugin 全面解析 (2)

Activity作為四大組件中最重要的組件,在Replugin中對它的支持的架構(gòu)設(shè)計(jì)也是最復(fù)雜的,所以本篇分析我們就來看看Activity的啟動流程。

以下這張圖簡要的畫出類Activity啟動的過程,當(dāng)然簡化了一些流程:

  • Pmbase根據(jù)Intent找到對應(yīng)的插件
  • 分配坑位Activity,與插件中的Activity建立一對一的關(guān)系并保存在PluginContainer
  • 讓系統(tǒng)啟動坑位Activity,因?yàn)樗窃贛anifest中注冊過的
  • Android系統(tǒng)會嘗試使用RepluginClassLoader加載坑位ActivityClass對象
  • RepluginClassLoader 通過建立的對應(yīng)關(guān)系找到插件Activity,并使用PluginDexClassLoader 加載插件Activity 的Class對象并返回
  • Android系統(tǒng)就使用這個(gè)插件中的Activity的Class對象來運(yùn)行生命周期函數(shù)

Android系統(tǒng)就是這樣被欺騙了!

activity.jpg

啟動一個(gè)Activity的入口函數(shù)是Replugin.startActivity(),然后調(diào)用Factory.startActivityWithNoInjectCN,再經(jīng)過PluginCommImpl.startActivivty(),最終來到PluginLibraryInternalProxy.startActivity(),這里將是真正開始工作的地方,會分為以下幾個(gè)步驟:

  • 如果有必要,需要先下載插件

    下載過程會通過回調(diào)讓用戶去實(shí)現(xiàn),比如顯示進(jìn)度,安裝等。

    if (download) {
        if (PluginTable.getPluginInfo(plugin) == null) {
            if (isNeedToDownload(context, plugin)) {
                return RePlugin.getConfig().getCallbacks().onPluginNotExistsForActivity(context, plugin, intent, process);
            }
        }
    }
    
  • 檢查插件狀態(tài)

    如果插件狀態(tài)不正確,或者首次加載大插件,會通過回調(diào)讓用戶處理,用戶可以可以在回調(diào)里定制自己的行為,比如彈出提示框,加載進(jìn)度條等。

    if (PluginStatusController.getStatus(plugin) < PluginStatusController.STATUS_OK) {
        return RePlugin.getConfig().getCallbacks().onPluginNotExistsForActivity(context, plugin, intent, process);
    }
    
    if (!RePlugin.isPluginDexExtracted(plugin)) {
        PluginDesc pd = PluginDesc.get(plugin);
        if (pd != null && pd.isLarge()) {
            return RePlugin.getConfig().getCallbacks().onLoadLargePluginForActivity(context, plugin, intent, process);
        }
    }
    
  • 尋找坑位,啟動坑位Activity

    調(diào)用在PluginLibraryInternalProxy.startActivity()中調(diào)用PluginCommImpl.loadPluginActivity來尋找坑位Activity。

    請注意注釋中的分支C,如果是第一次去獲取信息,會首先去加載插件的Dex文件以及資源等,并創(chuàng)建PluginDexClassLoader。這個(gè)分支我們在后面來講解。

    public ComponentName loadPluginActivity(Intent intent, String plugin, String activity, int process) {
        ActivityInfo ai = null;
        String container = null;
        PluginBinderInfo info = new PluginBinderInfo(PluginBinderInfo.ACTIVITY_REQUEST);
    
        try {
            ai = getActivityInfo(plugin, activity, intent); //分支C:獲取 ActivityInfo
            // 根據(jù) activity 的 processName,選擇進(jìn)程 ID 標(biāo)識
            if (ai.processName != null) {
                process = PluginClientHelper.getProcessInt(ai.processName);
            }
            // 容器選擇(啟動目標(biāo)進(jìn)程,如果有必要的話,一般默認(rèn)會使用UI進(jìn)程)
            IPluginClient client = MP.startPluginProcess(plugin, process, info);
          ......
            // 遠(yuǎn)程分配坑位
            container = client.allocActivityContainer(plugin, process, ai.name, intent);
        } catch (Throwable e) {
        }
    
        PmBase.cleanIntentPluginParams(intent);
      ......
        return new ComponentName(IPC.getPackageName(), container);
    }
    

    來重點(diǎn)看看坑位分配,這是一個(gè)遠(yuǎn)程調(diào)用,調(diào)用了Persistent進(jìn)程中的PluginProcessPer.allocActivityContainer函數(shù),進(jìn)一步調(diào)用bindActivity函數(shù)。

    final String bindActivity(String plugin, int process, String activity, Intent intent) {
        Plugin p = mPluginMgr.loadAppPlugin(plugin); //獲取插件對象
      ......
        ActivityInfo ai = p.mLoader.mComponents.getActivity(activity); //獲取ActivityInfo
      ......
        String container;
        // 自定義進(jìn)程
        if (ai.processName.contains(PluginProcessHost.PROCESS_PLUGIN_SUFFIX2)) {
            String processTail = PluginProcessHost.processTail(ai.processName);
            container = mACM.alloc2(ai, plugin, activity, process, intent, processTail);
        } else {
            container = mACM.alloc(ai, plugin, activity, process, intent);
        }
      ......
        return container;
    }
    

    這里mACM.alloc2調(diào)用allocLocked函數(shù)真正的執(zhí)行了坑位分配的任務(wù)。這段代碼簡單明了,注意坑位找好以后會返回一個(gè)AcitivtyState對象,這里面保存了坑位Activity和真實(shí)要啟動的Activity之間的對應(yīng)關(guān)系。并且這個(gè)對應(yīng)關(guān)系會被保存起來,在RepluginClassLoader在加載類的時(shí)候會被拿出來使用,以獲取要運(yùn)行的Activityclass對象。

    private final ActivityState allocLocked(ActivityInfo ai, HashMap<String, ActivityState> map, String plugin, String activity, Intent intent) {
        // 首先找上一個(gè)活的,或者已經(jīng)注冊的,避免多個(gè)坑到同一個(gè)activity的映射
        for (ActivityState state : map.values()) {
            if (state.isTarget(plugin, activity)) {
                return state;
            }
        }
        // 新分配:找空白的,第一個(gè)
        for (ActivityState state : map.values()) {
            if (state.state == STATE_NONE) {
                state.occupy(plugin, activity);
                return state;
            }
        }
        ActivityState found;
        // 重用:則找最老的那個(gè)
        found = null;
        for (ActivityState state : map.values()) {
            if (!state.hasRef()) {
                if (found == null) {
                    found = state;
                } else if (state.timestamp < found.timestamp) {
                    found = state;
                }
            }
        }
        if (found != null) {
            found.occupy(plugin, activity);
            return found;
        }
        // 強(qiáng)擠:最后一招,擠掉:最老的那個(gè)
        found = null;
        for (ActivityState state : map.values()) {
            if (found == null) {
                found = state;
            } else if (state.timestamp < found.timestamp) {
                found = state;
            }
        }
        if (found != null) {
            found.finishRefs();
            found.occupy(plugin, activity);
            return found;
        }
        return null;
    }
    

    坑位找到啦!PluginLibraryInternalProxy.startActivity()中開始啟動坑位Activity,就在分支D的位置,這個(gè)分支我們稍微延后一點(diǎn)來展開。

    public boolean startActivity(Context context, Intent intent, String plugin, String activity, int process, boolean download) {
        ......
        ComponentName cn = mPluginMgr.mLocal.loadPluginActivity(intent, plugin, activity, process);  // 找到坑位組件
        ......
        // 將Intent指向到“坑位”
        intent.setComponent(cn);
        ......
        context.startActivity(intent);  //分支D: 啟動坑位Activity
    
        return true;
    }
    

    看到這里你一定會疑惑,難道這樣插件的Activity就啟動起來啦嗎?這啟動的明明就是一個(gè)坑位Activity???別著急,接著就是前面一直強(qiáng)調(diào)的唯一hook點(diǎn)發(fā)揮作用的時(shí)候啦?。?/p>

  • Dex的加載以及Activity的加載啟動

    上面有一個(gè)分支C你還記得嗎?我們將它與Activity的加載流程放在一起來講,因?yàn)檫@兩者是緊密相關(guān)的。

    先來看分支C。

    • PluginCommImpl.getActivityInfo調(diào)用PmBase.loadAppPlugin獲取插件對象,從下面注釋的分支可以看出,Replugin 是支持使用 IntentFilter 來啟動組件的,完美支持原生特性,是不是很贊!

      public ActivityInfo getActivityInfo(String plugin, String activity, Intent intent){
          Plugin p = mPluginMgr.loadAppPlugin(plugin);  //獲取插件對象
          ......
          ActivityInfo ai = null;
      
          //activity 不為空時(shí),從插件聲明的 Activity 集合中查找
          if (!TextUtils.isEmpty(activity)) {
              ai = p.mLoader.mComponents.getActivity(activity);
          } else {
              //activity 為空時(shí),根據(jù) Intent 匹配
              ai = IntentMatcherHelper.getActivityInfo(mContext, plugin, intent);
          }
          return ai;
      }
      
    • PmBase.loadAppPlugin會最終調(diào)用Plugin.loadLocked()函數(shù),這個(gè)函數(shù)有兩個(gè)參數(shù),第一個(gè)是加載類型,一共有四種加載類型,在這里使用的是Plugin.LOAD_APP,因?yàn)檫\(yùn)行插件需要所有的東西。第二個(gè)參數(shù)是是否使用緩存,通常情況下我們會現(xiàn)在緩存中查找插件信息,這樣會更快。只是如果大量插件加載到內(nèi)存會不會占用太多的內(nèi)存,感興趣的同學(xué)可以自己研究研究。

      這里如果第一次加載失敗,Replugin還會做一次重試,相關(guān)代碼幾乎相同,這里就省略了。

      private boolean loadLocked(int load, boolean useCache) {
          // 這里先處理一下,如果cache命中,省了后面插件提?。ㄈ玑尫臞ar包等)操作,直接返回緩存數(shù)據(jù)
          if (useCache) {
              boolean result = loadByCache(load);
              if (result) {
                  return true;
              }
          }
          ......
          boolean rc = doLoad(logTag, context, parent, manager, load);  // 真正的加載
      
          if (rc) {
              try {
                  // 至此,該插件已開始運(yùn)行
                  PluginManagerProxy.addToRunningPluginsNoThrows(mInfo.getName());
              } catch (Throwable e) {
              }
              return true;
          }
          ......
          File odex = mInfo.getDexFile();
          if (odex.exists()) {
              odex.delete();
          }
          rc = doLoad(logTag, context, parent, manager, load);
          ......
          return true;
      }
      
    • Plugin.doLoad()當(dāng)然就是來加載插件的Dex文件,資源,以及so文件等等。

      private final boolean doLoad(String tag, Context context, ClassLoader parent, PluginCommImpl manager, int load) {
          if (mLoader == null) {
              // 中間省略這一段代碼是釋放so文件,請自行閱讀代碼,代碼清晰簡單
              ......
              // 加載Dex,獲取組件信息
              mLoader = new Loader(context, mInfo.getName(), mInfo.getPath(), this);
              if (!mLoader.loadDex(parent, load)) {
                  return false;
              }
              // 在Persistent進(jìn)程中更新插件信息,設(shè)置插件為“使用過的”
              try {
                  PluginManagerProxy.updateUsedIfNeeded(mInfo.getName(), true);
              } catch (RemoteException e) {
              }
      
              // 若需要加載Dex,則還同時(shí)需要初始化插件里的Entry對象
              if (load == LOAD_APP) {
                  // NOTE Entry對象是可以在任何線程中被調(diào)用到
                  if (!loadEntryLocked(manager)) {
                      return false;
                  }
              }
          }
      }
      
    • Loader.loadDex函數(shù)會獲取Dex中的組件的信息,包括Manifest中的組件屬性,比如進(jìn)程屬性,TaskAffinity屬性,注冊靜態(tài)廣播等等。但這里值得重點(diǎn)強(qiáng)調(diào)的是之前核心概念里提及的PluginDexClassLoader終于出現(xiàn)并被初始化了。

      final boolean loadDex(ClassLoader parent, int load) {
          try {
             ......  // 這里省略了一些基本的加載動作
              mClassLoader = Plugin.queryCachedClassLoader(mPath);
              if (mClassLoader == null) {
                  ......
                  mClassLoader = RePlugin.getConfig().getCallbacks().createPluginClassLoader(mPath, out, soDir, parent);     //  創(chuàng)建PluginDexClassLoader
                  ......  
              }
              ......
              mPkgContext = new PluginContext(mContext, android.R.style.Theme, mClassLoader, mPkgResources, mPluginName, this);   // 創(chuàng)建插件的Context對象
          } catch (Throwable e) {
              return false;
          }
      
          return true;
      }
      

      到此為止,插件Dex的加載算是全部完成,下面還剩最后一步,我們的插件就算真正的啟動運(yùn)行起來了。

    • 這里我們要接著前面啟動坑位Activity的地方接著講,要啟動Activity首先要去加載對應(yīng)的類,系統(tǒng)會調(diào)用ClassloaderloadClass方法,這里就是調(diào)用Replugin提供的替代者RepluginClassLoader的方法。接著又會調(diào)用PMF.loadClass,其實(shí)就是調(diào)用Pmbase.loadClass

      protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
          Class<?> c = null;
          c = PMF.loadClass(className, resolve);  
          if (c != null) {
              return c;
          }
          try {
              c = mOrig.loadClass(className); // 如果上面沒有找到,那就在Host當(dāng)中找
              return c;
          } catch (Throwable e) {
          }
          return super.loadClass(className, resolve);
      }
      
    • PmBase.loadClass看起來代碼很多,對于Activity來說,其實(shí)我們只需要關(guān)注下面這一小段即可,mClientPluginProcessPer的實(shí)例,而PluginProcessPerIPluginClient的實(shí)現(xiàn)類。

      final Class<?> loadClass(String className, boolean resolve) {
          ......
          if (mContainerActivities.contains(className)) {
              Class<?> c = mClient.resolveActivityClass(className);
              if (c != null) {
                  return c;
              }
              ......
          }
          ......
      }
      
    • 這里先從PluginContainers的實(shí)例對象mACM中去查找ActivityState,對這個(gè)類還有印象嗎?它就是在分配坑位的時(shí)候,我們用來保存坑位組件與真實(shí)組件對應(yīng)關(guān)系的類。然后在緩存中找到插件名對應(yīng)的插件對象,因?yàn)樵诜峙淇游坏臅r(shí)候插件信息已經(jīng)加載過了,不需要重新加載。接著取出插件的ClassLoader對象,這個(gè)對象正是加載插件時(shí)創(chuàng)建的PuginDexClassLoader的實(shí)例了。然后利用插件的PuginDexClassLoader對象來加載真實(shí)Activity的class對象。

      final Class<?> resolveActivityClass(String container) {
          String plugin = null;
          String activity = null;
          // 找到坑位Activity與真實(shí)Activity的對應(yīng)關(guān)系對象
          PluginContainers.ActivityState state = mACM.lookupByContainer(container);
          ......
          plugin = state.plugin;
          activity = state.activity;
          ......
          Plugin p = mPluginMgr.loadAppPlugin(plugin); //通過插件名從緩存中加載Plugin對象
          ......
          ClassLoader cl = p.getClassLoader();
          Class<?> c = null;
          try {
              c = cl.loadClass(activity);
          } catch (Throwable e) {
          }
          return c;
      }
      

      找到插件Activity的類對象后,Android系統(tǒng)就開始運(yùn)行Activity的啟動流程了,這些事情由ActivityManagerService和ActivityThread負(fù)責(zé)。就這樣,Replugin用插件中的Activity替換了坑位Activity,我們的插件被運(yùn)行起來啦!!很巧妙的設(shè)計(jì)~

小結(jié)

以上的內(nèi)容就是一個(gè)插件Activity要運(yùn)行起來,Replugin的基本代碼流程,這里要說明一下,源碼中的邏輯遠(yuǎn)不止這么點(diǎn),如果你有興趣可以跟著這篇文章在源碼中過一遍,有很多不是那么復(fù)雜的邏輯這里并沒有講到,當(dāng)然也有一些重要的地方因?yàn)榇a并不復(fù)雜也沒有講到。

下一篇Replugin 全面解析(3) 會對插件的加載和運(yùn)行做更完整和詳細(xì)的講解!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,677評論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內(nèi)容