1. 程式人生 > >《Android 開發藝術探索》讀書筆記(一)——Activity 的生命週期和啟動模式

《Android 開發藝術探索》讀書筆記(一)——Activity 的生命週期和啟動模式

Activity 作為 Android 四大元件之首,它作為和使用者互動的介面,在開發中使用得可謂極其頻繁,所以弄清楚 Activity 的生命週期和啟動方式是非常重要的,要牢記。

1 Activity 的生命週期全面分析

1.1 典型情況下的生命週期分析

onCreate():該方法呼叫時表示 Activity 被建立,可以在該方法中做一些初始化工作,如呼叫 setContentView() 方法載入佈局、初始化資料等。onStart():該方法呼叫時表示 Activity 被啟動,Acitivity 即將開始獲取,這是其實 Activity 已經顯示出來了,但是還沒有獲取到焦點,無法和使用者互動。onResume():

該方法呼叫時表示 Activity 可見並獲得焦點,此時 Activity 已經出現在前臺並開始活動,與 onStart() 相比兩個方法都表示 Activity 可見,只是 onStart() 時 Activity 還在後臺,onResume() 時才顯示到前臺。onRestart():該方法呼叫時表示 Activity 被重新啟動,一般情況下 Activity 從不可見狀態重新變為可見狀態時會呼叫該方法,這些情況一般是使用者行為導致的,如使用者按 Home 鍵返回到桌面再進入 App,或者從當前 Activity 跳轉到一個新的 Activity 然後再回到當前 Activity 等。onPause():
該方法呼叫時表示 Activity 正在停止,正常情況下 onStop() 會緊接著被呼叫。特殊情況下如果在這個時候如果快速回到當前 Activity 則 onResume() 方法會被呼叫,但這屬於極端情況,使用者操作很難出現這種場景。該方法中可以做一些儲存資料、停止動畫等操作,但不能太耗時,因為當前 Activity 的 onPause() 方法必須執行完成後才會執行新 Activity 的 onResume() 方法。onStop():該方法呼叫時表示 Activity 即將停止,可以做一些稍微重量級一點的回收工作,但仍然不能太耗時。onDestory():該方法呼叫時表示 Activity 即將被銷燬,可以做一些回收工作和最終資源的釋放。

我們可以將 onCreate() 和 onDestory() 看作是一對,onStart() 和 onStop() 是一對,onResume() 和 onPause() 是一對。

這張圖是 Activity 生命週期的經典圖:

假設有一個 Activity1:

public class Activity1 extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_1);
        Log.i("daolema", "Activity1--->onCreate");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.i("daolema", "Activity1--->onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.i("daolema", "Activity1--->onResume");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.i("daolema", "Activity1--->onRestart");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.i("daolema", "Activity1--->onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.i("daolema", "Activity1--->onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i("daolema", "Activity1--->onDestroy");
    }
}

啟動 Activity1 時,然後按 Home 鍵回到桌面,然後回到應用,然後按 Back 鍵退出應用,走的生命週期如下:

又有一個 Activity2,在 Activity1 中增加一個按鈕用於啟動 Activity2,修改 onCreate() 方法:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_1);
        Log.i("daolema", "Activity1--->onCreate");
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("daolema", "點選跳轉到 Activity2");
                startActivity(new Intent(Activity1.this, Activity2.class));
            }
        });
    }

Antivity2 的程式碼大致與 Activity1 相同,先啟動 Activity1,然後點選按鈕跳轉到 Activity2,然後點選 Activity2 中的按鈕呼叫 Activity2 的 finish() 方法,然後按 Back 鍵,所經歷的生命週期如下:

可以看到 Activity2 的 onResume() 方法是在 Activity1 的 onPause() 方法後執行的,那麼是不是必須得 Activity1 的 onPause() 方法執行完之後才能執行 Activity2 的 onResume() 方法呢?修改 Activity1 的 onPause() 方法,加一個延時:

    @Override
    protected void onPause() {
        super.onPause();
        Log.i("daolema", "Activity1--->onPause");
        SystemClock.sleep(3000);
    }

注意看紅框中的時間,可以看到確實是前一個 Activity 的 onPause() 方法執行完才去建立要啟動的 Activity,所以千萬不能在 onPause() 方法中執行耗時的操作,不然會影響下一個 Activity 的顯示。

如果要啟動的 Activity 採用的是透明主題,則不會執行前一個 Activity 的onStop() 方法,這是因為新 Activity 是透明的,也就意味著前一個 Activity 還是“可見”的。

1.2 異常情況下的生命週期分析

1.2.1 資源相關的系統配置發生改變導致 Activity 被殺死並重新建立

預設情況下如果系統配置發生改變,當前 Activity 就會被銷燬並重新建立,所以它的 onPause()、onStop()、onDestory() 方法均會被呼叫,然後重新執行 onCreate()、onStart()、onResume() 方法,並且這是異常終止,所以還會呼叫 onSaveInstanceState() 方法來儲存當前 Activity 的狀態,該方法會在 onStop() 之前呼叫,但是和 onPause() 方法沒有固定的先後順序,既可能在 onPause() 之前,也可能在 onPause() 之後。當  Activity 重新建立後會呼叫 onRestoreInstanceState() 方法恢復之前儲存的狀態onRestoreInstanceState() 方法是在 onStart() 之後。

Activity 在呼叫 onSaveInstanceState() 方法在儲存狀態時首先會委託 Window 去儲存資料,然後 Window 再委託上層的頂級容器儲存資料,頂層容器是一個 ViewGroup(一般來說是 DecorView),最後頂層容器一一通知子元素儲存資料。恢復資料時也是一樣。

以 TextView 的 onSaveInstanceState() 方法為例:

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        // Save state if we are forced to
        final boolean freezesText = getFreezesText();
        boolean hasSelection = false;
        int start = -1;
        int end = -1;

        if (mText != null) {
            start = getSelectionStart();
            end = getSelectionEnd();
            if (start >= 0 || end >= 0) {
                // Or save state if there is a selection
                hasSelection = true;
            }
        }

        if (freezesText || hasSelection) {
            SavedState ss = new SavedState(superState);

            if (freezesText) {
                if (mText instanceof Spanned) {
                    final Spannable sp = new SpannableStringBuilder(mText);

                    if (mEditor != null) {
                        removeMisspelledSpans(sp);
                        sp.removeSpan(mEditor.mSuggestionRangeSpan);
                    }

                    ss.text = sp;
                } else {
                    ss.text = mText.toString();
                }
            }

            if (hasSelection) {
                // XXX Should also save the current scroll position!
                ss.selStart = start;
                ss.selEnd = end;
            }

            if (isFocused() && start >= 0 && end >= 0) {
                ss.frozenWithFocus = true;
            }

            ss.error = getError();

            if (mEditor != null) {
                ss.editorState = mEditor.saveInstanceState();
            }
            return ss;
        }

        return superState;
    }

它儲存了 TextView 的文字內容和選中狀態,在恢復狀態時也確實恢復了這些資料,系統配置發生的這種情況一般發生在橫豎屏切換時,所以我們切換一下橫豎屏後觀察它的介面狀態和生命週期過程:

可以看到 Activity 第一次啟動時並沒有呼叫 onRestoreInstanceState() 方法,切換橫豎屏後銷燬時呼叫了 onSaveInstanceState() 方法,重新建立時才會呼叫 onRestoreInstanceState(),所以我們也可以通過這兩個方法來判斷 Activity 是否有重建。

1.2.2 資源記憶體不足導致低優先順序的 Activity 被殺死

現在手機配置越來越高,這種情況越來越不容易出現,它的狀態儲存和恢復狀態過程跟系統配置改變時一樣的,所以我們搞懂 Activity 的優先順序即可。

程序的優先順序順序為:前臺程序>可見程序>服務程序>後臺程序>空程序,

Activity 的優先順序順序也類似:前臺 Activity > 可見 Activity > 後臺 Activity。

前臺 Activity 就是當前正在與使用者互動的 Activity,可見 Activity 如當前 Activity 彈出一個對話方塊,導致 Activity 雖然可見但是無法與使用者直接互動,後臺 Activity 就是已經被暫停的 Activity。

系統記憶體不足時就會按照優先順序殺死目標程序,並後續通過 onSaveInstance() 和 onRestoreSaveInstance() 來儲存和恢復資料,如果一個程序沒有四大元件在執行,則很容易被殺死,所以一些後臺工作最好是放在 Service 中來保證程序有一定優先順序,不容易被輕易殺死。

1.2.3 configChanges 屬性

如何讓系統配置發生改變時 Activity 不重新建立呢,就是給 Activity 指定 configChanges 屬性,configChanges 的專案和含義如下:

含義
mcc SIM 卡中唯一標識 IMSI(國際移動使用者識別碼)中的國家程式碼,由三位數字組成 ,中國為 460,此項標識 mcc 程式碼發生了改變。
mnc SIM 卡中唯一標識 IMSI(國際移動使用者識別碼)中的運營商程式碼,由兩位數字組成, 中國移動為 00,中國聯通為 01,中國電信為 03,此項標識 mcc 程式碼發生了改變。
locale 裝置的本地位置發生改變,一般指切換了系統語言。
touchscreen 觸控式螢幕發生了改變,正常情況下很難發生,可以忽略。
keyboard 鍵盤型別發生了改變,如使用者使用了外接鍵盤。
keyboardHidden 鍵盤的可訪問性發生了改變,如使用者調出了鍵盤。
navigation 系統的導航方式發生了改變,如改用了軌跡球導航,正常情況下很難發生,可以忽略。
screenLayout 屏幕布局發生了改變,很可能是使用者激活了另一個顯示裝置。
fontScale 系統字型縮放比例發生了改變,如使用者選擇了一個新字號。
uiMode 使用者介面模式發生了改變,如開啟了夜間模式(API 8 新增)。
orientation 螢幕方向發生了改變,這個最常用,如旋轉了手機螢幕。
screenSize 螢幕的尺寸資訊發生改變,當旋轉螢幕裝置時螢幕尺寸會發生改變,該選項比較特殊,與編譯選項有關, 當編譯選項中的 minSdkVersion 和 targetSdkVersion 均低於 13 時,此選項不會導致 Activity 重啟, 否則會導致 Activity 重啟(API 13 新增)。
smallestScreenSize 裝置的物理尺寸發生改變,這個專案和螢幕的方向沒關係,僅僅表示實際的物理螢幕尺寸發生改變時發生, 如使用者切換到了外部的顯示裝置,這個選項也是當編譯選項中的 minSdkVersion 和 targetSdkVersion 均低於 13 時,此選項不會導致 Activity 重啟, 否則會導致 Activity 重啟(API 13 新增)。
layoutDirection 佈局方向發生改變,這個屬性用得比較少,正常情況下無需修改佈局的 layoutDirection 屬性(API 17新增)。

如果我們沒有在 AndroidManifest.xml 中為 Activity 的 configChanges 屬性指定某個值時,當相關配置發生了改變就會導致 Activity 重新建立,雖然 configChanges 的值很多,但是常用的只有 locale、orientation、keyboardHidden 和 screenSize 值,這是面試中經常問到的螢幕旋轉時 Activity 的生命週期的變化的問題,親測後記錄分別為 Activity1 指定不同的 configChanges 屬性,當螢幕旋轉時生命週期變化的列印結果。

測試專案的 sdkVersion 為:minSdkVersion 15 和 targetSdkVersion 26。

當為 Activity1 指定 android:configChanges="orientation" 時,切換兩次螢幕方向列印如下:

當為 Activity1 指定 android:configChanges="orientation|keyboardHidden" 時(這裡網上很多說設定成這樣後橫豎屏切換時 Activity 就不會重新建立,其實這跟 minSdkVersion 和 targetSdkVersion ,表中已經說明,均低於 13 時才不會重啟,高於 13 時還需要設定 screenSize),切換兩次螢幕方向列印如下:

當為 Activity1 指定 android:configChanges="orientation|keyboardHidden|screenSize" 時,切換兩次螢幕方向列印如下:

可以看到只有為 Activity 指定 android:configChanges="orientation|keyboardHidden|screenSize" 時橫豎屏切換才不會導致 Activity 重啟,也不會呼叫 onSavaInstance() 和 onRestoreSaveInstance() 方法,而是隻呼叫 onConfigrationChanged() 方法。

2 Activity 的啟動模式

除生命週期外,Activity 的啟動模式和各種標誌位也是很重要的,有時候為了滿足專案的特殊需求,就需要設定不同的啟動模式和標誌位。

2.1 Activity 的 LaunchMode

standard:標準模式,預設模式,每次啟動一個 Activity 不管該 Activity 的例項是否已存在都會建立一個新的例項。一個任務棧可以有多個例項,每個例項也可以屬於不同的任務棧,這種模式下,誰啟動了這個 Activity,這個 Activity 就執行在啟動它的那個 Activity 所在的棧中,如 Activity2 的 launchMode 為 standard,Activity1 啟動了 Activity2,那麼 Activity2 就會進入到 Activity1 所在的棧中。如果當前棧中有四個 Activity 的例項 1234,4 位於棧頂,如果 3 和 4 的 launchMode 均為 singleTop,此時如果再次啟動 3,那麼棧中為 12343,如果啟動 4,棧中也會變為 12344。

singleTop:棧頂複用模式,如果需要啟動的 Activity 已經位於任務棧的棧頂,那麼此 Activity 就不會重新建立例項,而是回撥 onNewIntent() 方法,該方法可以獲取當前請求的資訊,如果需要啟動的 Activity 已經存在但是不是位於棧頂,那麼仍然會建立新的例項,如果當前棧中有四個 Activity 的例項 1234,4 位於棧頂,如果 3 和 4 的 launchMode 均為 singleTop,此時如果再次啟動 3,那麼棧中變為 12343,但是如果啟動 4,棧中則仍為 1234。

singleTask:棧內複用模式,如果需要啟動的 Activity 已經在一個棧中存在,那麼此 Activity 就不會重新建立例項,會將該 Activity 調到棧頂並回調 onNewIntent() 方法。如果當前棧 S1 中有四個 Activity 的例項 1234,4 位於棧頂,如果 2 的 launchMode 為 singleTask,那麼啟動 2 時則會將 2 調到棧頂,並清掉 2 之上的 Activity,S1 變為 12。如果當前棧 S1 中有四個 Activity 的例項 1234,4 位於棧頂,如果需要啟動 5,並且需要的任務棧為 S2,那麼則會先建立棧 S2,再建立 5 併入棧到 S2,S1 仍為 1234。

singleInstance:單例項模式,這是一張加強的 singleTask 模式,以該模式啟動的 Activity 會單獨位於一個任務棧中。

2.2 Activity 的 Flags

Activity 的 Flags 的作用很多,可以設定 Activity 的啟動模式,也可以影響 Activity 的執行狀態,設定 Flags 一般在 Java 程式碼中:

Intent intent = new Intent(); intent.setClass(packageContext, cls); intent.addFlags(flags); startActivity(intent);

通常我們不需要設定 Activity,所以這個並不是很重要,常用的 Flags 如下:

FLAG_ACTIVITY_NEW_TASK:相當於在 AndroidManifest.xml 中設定 launchMode 為 singleTask。FLAG_ACTIVITY_SINGLE_TOP:相當於在 AndroidManifest.xml 中設定 launchMode 為 singleTop。FLAG_ACTIVITY_CLEAR_TOP:此標記位一般與 FLAG_ACTIVITY_NEW_TASK 配合使用,當具有此標記位的 Activity 啟動時,同一任務棧中位於它之上的 Activity 都將出棧。FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:具有此標記位的 Activity 不會出現在歷史 Activity 列表中,某些情況下不希望使用者通過歷史列表回到該 Activity 時該標記比較有用,等同於 AndroidManifest.xml 中設定 Activity 的屬性 android:excludeFromRecents="true"。

3 IntentFilter 的匹配規則

3.1 匹配規則

啟動 Activity 的方式分為顯式呼叫和隱式呼叫,我們一般常用的就是顯式呼叫,明確的指定被啟動元件的類名,而隱式呼叫則不需要指定元件資訊,而是通過匹配目標元件的 IntentFilter 中設定的過濾資訊來啟動目標 Activity。過濾資訊有 action、category 和 data。 匹配資訊時,需同時匹配上 action、category 和 data 三個資訊才算匹配成功。一個過濾列表中可以有多個 action、category 和 data,同類型匹配上任意一個該型別就算匹配成功,如有多個 action 資訊,匹配上任意一個即算 action 匹配成功。一對 <activity></activity> 標籤中可以有多對 <intent-filter></intent-filter>,成功匹配任意一對即可成功啟動對應 Activity。

action:action 是一個字串,系統有預定義一些 action,如 Intent.ACTION_DIAL(撥號介面)、Intent.ACTION_SENDTO(傳送簡訊)等,我們也可以自定義 action,只有字串完全一樣才能匹配成功。設定 action 是呼叫 setAction(String action) 方法,說明 Intent 只能設定一個 action,但是 <intent-filter></intent-filter> 可以設定多個 action,匹配上其中任何一個就算匹配成功。Intent 必須設定 action。 如果過濾資訊是這樣:

            <intent-filter>
                <action android:name="com.qinshou.demo.action1"/>
                <action android:name="com.qinshou.demo.action2"/>
    ...
            </intent-filter>

那麼 Intent 的 action 必須為 com.qinshou.demo.action1 或 com.qinshou.demo.action2 才能符合規則,如:

                Intent intent = new Intent("com.qinshou.demo.action1");

                Intent intent = new Intent();
                intent.setAction("com.qinshou.demo.action2");

category:category 是一個字串,系統也預定義了一些 category,最常見的是 android.intent.category.LAUNCHER,它表示開啟應用時啟動哪一個 Activity,我們也可以自定義 category,只有字串完全一樣才能匹配成功。新增 category 是呼叫 addCategory(String category) 方法,說明 Intent 可以有多個 category,但是無論設定有幾個,它都必須全部匹配,如果有任何一個匹配不上都算匹配不成功。如果是隱式啟動的話 <intent-filter></intent-filter> 中必須要有一個 <category android:name="android.intent.category.DEFAULT"/>。同 action 不同的是,Intent 可以不設定 category,系統會預設新增 "android.intent.category.DEFAULT"。 如果過濾資訊是這樣:

            <intent-filter>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="com.qinshou.demo.category1"/>
                <category android:name="com.qinshou.demo.category2"/>
    ...
            </intent-filter>

那麼 Intent 的 <category 必須為 android.intent.category.DEFAULT、com.qinshou.demo.category1、com.qinshou.demo.category2 其中零個或多個才能符合規則,如:

                intent.addCategory(Intent.CATEGORY_DEFAULT);
                intent.addCategory("com.qinshou.demo.category1");
                intent.addCategory("com.qinshou.demo.category2");

data:如果 <intent-filter></intent-filter> 有設定 <data></data> 那麼 Intent 中就必須設定,如果沒有設定則 Intent 不能設定。data 的語法如下:

                <data
                    android:mimeType="string"
                    android:host="string"
                    android:port="string"
                    android:path="string"
                    android:pathPattern="string"
                    android:pathPrefix="string"
                    android:scheme="string"/>

data 分為 mimeType 和 URI 兩部分組成,mimeType  是指媒體型別,如 image/jpeg、text/plain 等,它不是必須要指定的。URI 是由 scheme、host、port、path 等共同組成,它的結構為:<scheme>://<host>:<port>/[<path>|<pathPattern>|<pathPrefix>]。其實就跟我們平時瀏覽器中的網址差不多,如 https://www.baidu.com:80/xxx。 scheme:URI 的模式,如上面的https,還有常用的 file、content 等,也可以自定義,如果 URI 中沒有指定 scheme,那麼整個 URI 無效。 host:URI 的主機名,如上面的 www.baidu.com,如果 URI 中沒有指定 host,那麼整個 URI 無效。 port:URI 的埠號,如上面的 80,只有指定了 scheme 和 host 後 port 才有意義。 path、pathPattern、pathPrefix:分別代表完整的路徑資訊、路徑的正則匹配表示式、路徑的字首,如上面的 xxx。 mimeType  事實上在自定義 data 時一般很少指定,如果指定了則 Intent 中則必須設定 type,而且如果設定了 mimeType 而沒有設定 URI 的話,事實上 URI 是有預設值的,必須為 content 或 file,也就是說如果設定了 mimeType 而沒有設定 URI 那麼 URI 要麼不指定,要麼必須是形如 "content://xxx" 或 "file://xxx"。設定完整的 data 是呼叫 setDataAndType(Uri data,String type) 方法,不能分別呼叫 setData(Uri data) 和 setType(String type),因為它們兩個會互相清除對方的值。

    public @NonNull Intent setData(@Nullable Uri data) {
        mData = data;
        mType = null;
        return this;
    }

如果過濾資訊是這樣:

            <intent-filter>
                <action android:name="com.qinshou.demo.action1"/>
                <action android:name="com.qinshou.demo.action2"/>

                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="com.qinshou.demo.category1"/>
                <category android:name="com.qinshou.demo.category2"/>

                <data
                    android:mimeType="text/plain"/>
            </intent-filter>

那麼 Intent 必須這樣才能符合規則:

                intent.setType("text/plain");

                intent.setDataAndType(Uri.parse("file://xxx"), "text/plain");

如果過濾資訊是這樣:

            <intent-filter>
                <action android:name="com.qinshou.demo.action1"/>
                <action android:name="com.qinshou.demo.action2"/>

                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="com.qinshou.demo.category1"/>
                <category android:name="com.qinshou.demo.category2"/>

                <data
                    android:host="activity3"
                    android:path="/test"
                    android:port="123"
                    android:scheme="demo"/>
            </intent-filter>

那麼必須 Intent 這樣才能符合規則(沒有設定 mimeType):

                intent.setData(Uri.parse("demo://activity3:123/test"));

隱式啟動同樣可以應用於 Service 和 BroadcastReceiver,不過系統建議是顯式啟動 Service 比較好。

3.2 檢查 Activity 是否存在

當我們隱式啟動 Activity 時如果沒有成功匹配到 Activity 的話就會直接崩潰,所以我們在啟動之前判斷一下是否有成功匹配的 Activity,如果有才啟動。可以使用 PackageManager 的 resolveActivity(Intent intent, @ResolveInfoFlags int flags) 方法判斷,它會返回最佳的一個匹配結果,如果沒有成功匹配的則返回 null,或者 PackageManager 的 queryIntentActivities(Intent intent,@ResolveInfoFlags int flags) 方法判斷,它會返回所有成功匹配的 Activity 資訊,如果沒有成功匹配的則返回的集合為空。

3.3 實際應用

data 的 URI 就類似於網址,那麼我們是不是在網頁中可以直接啟動 Activity 呢?完全可以,只需要簡單設定一下,

我們建立一個 Activity4,設定過濾規則:

        <activity android:name=".Activity4">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>

                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>

                <data
                    android:host="activity4"
                    android:path="/test"
                    android:port="456"
                    android:scheme="demo"/>
            </intent-filter>
        </activity>

注意我們添加了一個 category “android.intent.category.BROWSABLE”,它是指瀏覽器在特定條件下可以開啟你的 Activity,然後寫一個這樣的頁面:

<!doctype html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <title>測試頁面</title>
</head>
<body>
<a href="demo://activity4:456/test">跳轉到 Activity4</a>
</body>
</html>

<a> 標籤中的超連結完全符合 Activity 中 <data> 的過濾規則,使用 WebView 載入該頁面你會神奇的發現:

而且我們還可以加引數:

<a href="demo://activity4:456/test?name=qinshou&age=24&sex=male">跳轉到 Activity4</a>

在目標 Activity 中解析引數:

        Intent intent = getIntent();
        Uri uri = intent.getData();
        //獲取跳轉過來攜帶所有引數的 鍵名
        if (uri != null) {
            Set<String> queryParameterNames = uri.getQueryParameterNames();
            for (String key : queryParameterNames) {
                Log.i("daolema", "key--->" + key + ",value--->" + uri.getQueryParameter(key));
            }
        }

在 H5 的介面就能輕鬆掉起 Android 原生介面,這在後端呼叫移動端頁面的時候是很有用的。