瀏覽器為什么能喚起App的Activity?
本文轉(zhuǎn)載自微信公眾號「咸魚正翻身」,作者M(jìn)Dove。轉(zhuǎn)載本文請聯(lián)系咸魚正翻身公眾號。
疑問的開端
大家有沒有想過一個問題:在瀏覽器里打開某個網(wǎng)頁,網(wǎng)頁上有一個按鈕點擊可以喚起App。
這樣的效果是怎么實現(xiàn)的呢?瀏覽器是一個app;為什么一個app可以調(diào)起其他app的頁面?
說到跨app的頁面調(diào)用,大家是不是能夠想到一個機(jī)制:Activity的隱式調(diào)用?
隱式啟動原理
當(dāng)我們有需要調(diào)起其他app的頁面時,使用的API就是隱式調(diào)用。
比如我們有一個app聲明了這樣的Activity:
- <activity android:name=".OtherActivity"
 - android:screenOrientation="portrait">
 - <intent-filter>
 - <action android:name="mdove"/>
 - <category android:name="android.intent.category.DEFAULT"/>
 - </intent-filter>
 - </activity>
 
其他App想啟動上邊這個Activity如下的調(diào)用就好:
- val intent = Intent()
 - intent.action = "mdove"
 - startActivity(intent)
 
我們沒有主動聲明Activity的class,那么系統(tǒng)是怎么為我們找到對應(yīng)的Activity的呢?其實這里和正常的Activity啟動流程是一樣的,無非是if / else的實現(xiàn)不同而已。
接下來咱們就回顧一下Activity的啟動流程,為了避免陷入細(xì)節(jié),這里只展開和大家相對“耳熟能詳”的類和調(diào)用棧,以串流程為主。
跨進(jìn)程
首先我們必須明確一點:無論是隱式啟動還是顯示啟動;無論是啟動App內(nèi)Activity還是啟動App外的Activity都是跨進(jìn)程的。比如我們上述的例子,一個App想要啟動另一個App的頁面,至少涉及3個進(jìn)程。
注意沒有root的手機(jī),是看不到系統(tǒng)孵化出來的進(jìn)程的。也就是我們常見的為什么有些代碼打不上斷點。
追過startActivity()的同學(xué),應(yīng)該很熟悉下邊這個調(diào)用流程,跟進(jìn)幾個方法之后就發(fā)現(xiàn)進(jìn)到了一個叫做ActivityTread的類里邊。
ActivityTread這個類有什么特點?有main函數(shù),就是我們的主線程。
很快我們能看到一個比較常見類的調(diào)用:Instrumentation:
- // Activity.java
 - public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
 - mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);
 - // 省略
 - }
 
注意mInstrumentation#execStartActivity()有一個標(biāo)黃的入?yún)ⅲ茿ctivityThread中的內(nèi)部類ApplicationThread。
ApplicationThread這個類有什么特點,它實現(xiàn)了IApplicationThread.Stub,也就是aidl的“跨進(jìn)程調(diào)用的客戶端回調(diào)”。
此外mInstrumentation#execStartActivity()中又會看到一個大名鼎鼎的調(diào)用:
- public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
 - // 省略...
 - ActivityManager.getService()
 - .startActivity(whoThread, who.getBasePackageName(), intent,
 - intent.resolveTypeIfNeeded(who.getContentResolver()),
 - token, target != null ? target.mEmbeddedID : null,
 - requestCode, 0, null, options);
 - return null;
 - }
 
我們點擊去getService()會看到一個標(biāo)紅的IActivityManager的類。
它并不是一個.java文件,而是aidl文件。
所以ActivityManager.getService()本質(zhì)返回的是“進(jìn)程的服務(wù)端”接口實例,也就是:
ActivityManagerService
public class ActivityManagerService extends IActivityManager.Stub
所以執(zhí)行到這就轉(zhuǎn)到了系統(tǒng)進(jìn)程(system_process進(jìn)程)。省略一下代碼細(xì)節(jié),看一下調(diào)用棧:
從上述debug截圖,看一看到此時已經(jīng)拿到了我們的目標(biāo)Activitiy的相關(guān)信息。
這里簡化一些獲取目標(biāo)類的源碼,直接引入結(jié)論:
PackageManagerService
這里類相當(dāng)于解析手機(jī)內(nèi)的所有apk,將其信息構(gòu)造到內(nèi)存之中,比如下圖這樣:
小tips:手機(jī)目錄中/data/system/packages.xml,可以看到所有apk的path、進(jìn)程名、權(quán)限等信息。
啟動新進(jìn)程
打開目標(biāo)Activity的前提是:目標(biāo)Activity的進(jìn)程啟動了。所以第一次想要打開目標(biāo)Activity,就意味著要啟動進(jìn)程。
啟動進(jìn)程的代碼就在啟動Activity的方法中:
resumeTopActivityInnerLocked->startProcessLocked。
這里便引入了另一個另一個大名鼎鼎的類:ZygoteInit。這里簡單來說會通過ZygoteInit來進(jìn)行App進(jìn)程啟動的。
ApplicationThread
進(jìn)程啟動后,繼續(xù)回到目標(biāo)Activity的啟動流程。這里依舊是一系列的system_process進(jìn)行的轉(zhuǎn)來轉(zhuǎn)去,然后IApplicationThread進(jìn)入目標(biāo)進(jìn)程。
注意看,在這里再次通過IApplicationThread回調(diào)到ActivityThread。
- class H extends Handler {
 - // 省略
 - public void handleMessage(Message msg) {
 - switch (msg.what) {
 - case EXECUTE_TRANSACTION:
 - final ClientTransaction transaction = (ClientTransaction) msg.obj;
 - mTransactionExecutor.execute(transaction);
 - // 省略
 - break;
 - case RELAUNCH_ACTIVITY:
 - handleRelaunchActivityLocally((IBinder) msg.obj);
 - break;
 - }
 - // 省略...
 - }
 - }
 - // 執(zhí)行Callback
 - public void execute(ClientTransaction transaction) {
 - final IBinder token = transaction.getActivityToken();
 - executeCallbacks(transaction);
 - }
 
這里所謂的CallBack的實現(xiàn)是LaunchActivityItem#execute(),對應(yīng)的實現(xiàn):
- public void execute(ClientTransactionHandler client, IBinder token,
 - PendingTransactionActions pendingActions) {
 - ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
 - mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
 - mPendingResults, mPendingNewIntents, mIsForward,
 - mProfilerInfo, client);
 - client.handleLaunchActivity(r, pendingActions, null);
 - }
 
此時就轉(zhuǎn)到了ActivityThread#handleLaunchActivity(),也就轉(zhuǎn)到了咱們?nèi)粘5纳芷诶镞?,調(diào)用棧如下:
上述截圖的調(diào)用鏈中暗含了Activity實例化的過程(反射):
- public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
 - return (Activity) cl.loadClass(className).newInstance();
 - }
 
瀏覽器啟動原理
Helo站內(nèi)的回流頁就是一個標(biāo)準(zhǔn)的,瀏覽器喚起另一個App的實例。
交互流程
html標(biāo)簽有一個屬性href,比如:。
我們常見的一種用法:。也就是點擊之后跳轉(zhuǎn)到百度。
因為這個是前端的標(biāo)簽,依托于瀏覽器及其內(nèi)核的實現(xiàn),跳轉(zhuǎn)到一個網(wǎng)頁似乎很“順其自然”(不然叫什么瀏覽器)。
當(dāng)然這里和android交互的流程基本一致:用隱式調(diào)用的方式,聲明需要啟動的Activity;然后傳入對應(yīng)的協(xié)議(scheme)即可。比如:
前端頁面:
- <head>
 - <meta charset="UTF-8">
 - <meta name="viewport" content="width=device-width, initial-scale=1.0">
 - </head>
 - <body>
 - <a href="mdove1://haha"> 啟動OtherActivity </a>
 - </body>
 
android聲明:
- <activity
 - android:name=".OtherActivity"
 - android:screenOrientation="portrait">
 - <intent-filter>
 - <data
 - android:host="haha"
 - android:scheme="mdove1" />
 - <action android:name="android.intent.action.VIEW" />
 - <category android:name="android.intent.category.BROWSABLE" />
 - <category android:name="android.intent.category.DEFAULT" />
 - </intent-filter>
 - </activity>
 
推理實現(xiàn)
瀏覽器能夠加載scheme,可以理解為是瀏覽器內(nèi)核做了封裝。那么想要讓android也能支持對scheme的解析,難道是由瀏覽器內(nèi)核做處理嗎?
很明顯不可能,做了一套移動端的操作系統(tǒng),然后讓瀏覽器過來實現(xiàn),是不是有點殺人誅心。
所以大概率能猜測出來,應(yīng)該是手機(jī)中的瀏覽器app做的處理。我們就基于這個猜想去看一看瀏覽器.apk的實現(xiàn)。
瀏覽器實現(xiàn)
基于上邊說的/data/system/packages.xml文件,我們可以pull出來瀏覽器的.apk。
然后jadx反編譯一下Browser.apk中WebView相關(guān)的源碼:
我們可以發(fā)現(xiàn)對href的處理來自于隱式跳轉(zhuǎn),所以一切就和上邊的流程串了起來。
尾聲
結(jié)尾留個小問題:如果我自己寫個WebView去load一個前端頁面,能隱式跳轉(zhuǎn)嗎?




























 
 
 






 
 
 
 