[筆記]Activity的棧與跳轉

Activity是Android四大組件中用來顯示界面和操作互動的,一般來說,我們手機上都會打開多個App,每個App又有多個Activity,在用戶看來,這些Activity的創建、回退、跳轉、復用等,都是用戶體驗,甚至是業務邏輯的一部分,所以Android提供了完備的管理機制。
先來梳理一下基礎知識。

Process、AMS、ActivityStack、ActivityTask

  • Process,系統為App提供的進程(Process)資源,默認一個App一個Process,我們也可以要求為一個App提供多個Process。
  • AMS,系統開機啟動時,會啟動ActivityManagerService(簡稱AMS)來管理四大組件以及Intent、pendingintent、apk進程、task和釋放組件內存等。AMS是一個獨立的進程(SystemServie、AMS、PMS、WMS等系統服務,實質都是一個個App,各自擁有自己的進程),系統只有一個AMS,而且因為AMS在獨立的進程上,所以AMS和App之間,需要通過IPC機制進行跨進程通信。
  • ActivityStack ,AMS為了管理Activity實例,會建立并管理一個ActivityStack,用ActivityStackSupervisor來更新這個棧,因為系統只有一個AMS,所以ActivityStack也只有一個。ActivityStack是一個管理者的角色,會管理協調ActivityRecord和TaskRecord,以及所有的任務棧。
  • ActivityTask,系統中的所有Activity,在面向用戶時可能分成多個組,這是按照任務棧(Task)來組織的,我們常說的Activity啟動模式,其實是操作任務棧(Task),而不是操作Activity棧(Stack)

組織形式大概是這樣的:


組織形式

(我們可以用gethashcode()獲取Activity實例的唯一標識、用gettaskaffinity()獲取Activity所在的任務棧,用adb shell dumpsys activity命令可以列出所有的Activity)

可見,Activity管理的核心在于AMS,AMS中分兩個維度來管理Activity,一個是ActivityStack,只有一個,用ActivityRecord來記錄對應的Activity信息,主要用來管理系統中有哪些Activity;另一個是ActivityTask,可能有多個,用TaskRecord來記錄對應的Activity信息,主要用來把Activity組合成為多個Task棧,以便按照特定的業務邏輯展示給用戶。
所以,要管理多個Activity之間的關系,主要依靠任務棧Task。

任務棧(Task)

長按Home鍵(有的設備是長按菜單鍵),那些最近打開的App記錄,就是任務棧Task。
圖片來自Activity啟動模式圖文詳解

Task任務棧也叫BackStack回退棧,是后進先出棧,在一個Task棧里一直按Back鍵,就會不斷回退到上一個Activity(包括其他App的Activity),直到回退至Home,所以,用戶感知到的Activity界面回退邏輯,是由Task棧決定的,管理Activity的回退跳轉邏輯,其實就是管理Task任務棧。
Task不止一個,我們有可能會把現有的Task整體移動到后臺,然后創建一個新的Task,點開Notification、點擊Home鍵(或在Activity中調用moveTaskToBack(true),false值表示當前Activity必須在棧底)、App創建NewTask等行為,都可能把Task移動到后臺,創建新的Task。
對Task任務棧的管理,主要包括Activity啟動模式和IntentFlag創建方式兩部分。

Activity的四種啟動模式

啟動Activity的重點在于,如何復用已經存在的Activity實例,所以四種啟動模式實際上是四種復用模式,針對復用策略,Android中定義了四種啟動模式(android:launchMode):

  • "standard",默認,不復用,每次創建新實例。
  • "singleTop",棧頂復用,檢查棧頂,如果不是要啟動的Activity,創建新實例;如果是,就復用。
  • "singleTask",棧中復用,檢查任務棧,如果沒有要啟動的Activity,創建新實例;如果有,就把該Activity上方的其他Activity全部推出任務棧,復用該Activity。
  • "singleInstance",單例模式,獨享一個任務棧,且只有一個任務棧,只有
    一個Activity實例。

這四種啟動模式可以在manifest文件中定義,也可以在intent中用代碼動態賦值(例如添加FLAG_ACTIVITY_SINGLE_TOP標識,等同于設置singleTop屬性)。
這四種模式中,standard和singleTop類型的Activity可能出現多個實例,singleTask和singleInstance類型則只能有一個實例。
如果要使用startActivityForResult,在5.0以前還有使用限制,只允許standard<->singleTop、singleTask->standard、singleTask->singleTop,其他跳轉因為無法確?;氐皆瑼ctivity,可能會遇到RESULT_CANCELD問題導致無法接收返回值。

這四種模式大概可以這樣表示:
四種啟動模式

如果Activity被復用了,就會回調onNewIntent()函數。
正常啟動的Activity生命周期是onCreate()-->onStart()-->onResume()...。
復用的Activity生命周期是onNewIntent()-->onResart()-->onStart()--> onResume()...

一些常見的IntentFlag

除了上一節的四種啟動模式之外,我們還可以在代碼中設置intentFlag,用更豐富的形式來定義Activity的創建和復用策略。
intentFlag有時候需要和taskAffinity屬性(android:taskAffinity)配合使用,taskAffinity指的是Activity的任務親和性,即Activity與哪個Task更親和,默認值是應用的包名,taskAffinity屬性在2種情況下起作用:啟動 activity的Intent對象添加了FLAG_ACTIVITY_NEW_TASK標記,或activity的allowTaskReparenting屬性為true。
一些常見的IntentFlag如下:

  • FLAG_ACTIVITY_NEW_TASK (默認)
    其實這種標識下,系統會根據Activity的taskAffinity去創建一個新的Task任務棧,并向任務棧中壓入一個新創建的Activity實例。
    不過,一般情況下App中的taskAffinity都是同一個默認值(包名),這個Task已經存在了,這樣的話,運行效果就相當于standard啟動模式,在已有的Task中壓入一個新建的Activity。
    還需要說明的是,如果要用startActivityForResult的形式來調起新的Activity并等待返回結果,就不能使用FLAG_ACTIVITY_NEW_TASK標記,否則調用方的Activity可能會立即收到onActivityResult,實際上就是回調失效了。這是因為使用startActivityForResult時,兩個Activity需要在同一個Task里,Android認為不同Task之間的Activity是不能傳遞數據的。
  • FLAG_ACTIVITY_SINGLE_TOP
    相當于singleTop啟動模式。
  • FLAG_ACTIVITY_CLEAR_TOP
    模式情況下,會把目標Activity頂部的所有Activity都銷毀,同時連同目標Activity也銷毀,然后重建目標Activity。
    如果和FLAG_ACTIVITY_SINGLE_TOP一起使用,即:
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

就會復用目標Activity,這時相當于singleTask啟動模式(CLEAR_TOP這個名字也揭示了singleTask的實質)。
FLAG_ACTIVITY_CLEAR_TOP往往和FLAG_ACTIVITY_NEW_TASK一起使用。用2個標記可以定位已存在的activity并讓它處于可以響應intent的位置。

  • FLAG_ACTIVITY_CLEAR_TASK
    清空目標TASK,把新建的Activity作為棧底的Activity,必須與FLAG_ACTIVITY_NEW_TASK一起使用。
  • FLAG_ACTIVITY_REORDER_TO_FRONT
    如果Task中已經有實例,把該實例挪到棧頂。(優先級低于FLAG_ACTIVITY_CLEAR_TOP)。
  • FLAG_ACTIVITY_BROUGHT_TO_FRONT
    這個標志一般不是由程序代碼設置的,而是在launchMode中設置singleTask模式時系統幫你設定。
  • FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
    任務重置,在Activity導致新建Task或挪動到Task頂部時使用。
  • FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
    為Task設置一個還原點,當Task恢復到前臺時,找到有這個屬性的Activity,把這個Activity頂部的其他Activity,連同這個Activity一起銷毀。
  • FLAG_ACTIVITY_MULTIPLE_TASK
    Android系統為了節省資源,默認是復用Activity的,在打開Activity時,總是先搜索存在的task棧,去尋找匹配intent的一個activity。
    但是,如果使用了FLAG_ACTIVITY_MULTIPLE_TASK標識(和FLAG_ACTIVITY_NEW_DOCUMENT或者FLAG_ACTIVITY_NEW_TASK一起使用),就會跳過復用搜索步驟,直接創建一個新的task棧,并且在里面啟動新的activity。
  • FLAG_ACTIVITY_NO_USER_ACTION,用戶行為標識,用來區分離開當前Activity是用戶行為還是系統行為(比如呼入電話,會導致當前Activity退至后臺,但這不是用戶行為),如果不是用戶行為,就不會執行生命周期中的onUserLeaveHint(),這樣我們可以知道用戶有沒有做某些操作(比如是否看到了推送的通知)。
  • FLAG_ACTIVITY_NO_ANIMATION
    Activity切換時不展示動畫。

其他一些與棧相關的Activity屬性:

  • android:taskAffinity
    親和性,在intentFlag標識中和FLAG_ACTIVITY_NEW_TASK一起作用;或者與android:allowTaskReparenting共同起作用。
  • android:allowTaskReparenting
    允許重排任務棧,就是說Activity可以更換所在的Task任務棧,Activity可能在TaskA中,此時系統把一個TaskB移動到前臺,如果TaskA和TaskB的affinity相同,那么Activity就會被挪到TaskB。
  • android:excludeFromRecents
    Activity不添加到最近打開的應用列表中。
  • android:alwaysRetainTaskState
    保存Task棧中的Activity,當Task被移動到后臺后,如果長期沒有使用,Android可能會去回收內存,僅保留棧底的Activity,其他Activity全部回收掉,如果alwaysRetainTaskState屬性為true,系統就不會進行回收。
  • android:clearTaskOnLaunch
    催促回收Task棧(與alwaysRetainTaskState相反),這個屬性會被賦給棧底的Activity,一旦用戶離開Task,系統就會立即回收棧中的Activity,僅保留棧底的Activity。
  • android:finishOnTaskLaunch
    催促回收Activity,被賦予這個屬性的Activity,一旦用戶退出Task,Activity就會立即被系統回收,Task中不再保留這個Activity,如果恰好是棧底Activity,那么Task也就清空了。
  • android:noHistory
    清空歷史,離開Activity后,立即回收Activity,Task任務棧中不保存該Activity。noHistory和finishOnTaskLaunch的區別在于銷毀Activity的時機,noHistory是在離開Activity時銷毀,finishOnTaskLaunch是在切換Task時銷毀。

Scheme跳轉(頁面內跳轉)

我們知道,Activity跳轉是通過Intent來實現的,在定義Intent時,有三種形式:

  • 類名
    定義intent時設置目標Activity的類名
    intent.setClass(context,ActivityA.class);
    setClass其實就是setComponent,相當于
    intent.setComponent(new ComponentName(context, ActivityA.class));
  • Action
    需要在manifest中定義action
    <intent-filter> <action android:name="your.action.name" />
    定義intent時設置Action
    intent.setAction(your.action.name);
  • Scheme
    需要在manifest中定義scheme
    定義intent時設置url
    new Intent(Intent.ACTION_VIEW, Uri.parse(Url) )

在這三種形式中,Scheme是最靈活,適用最廣的,因為只需要一個url,就可以實現跳轉。
Scheme是一種頁面內跳轉協議,在Android和IOS都有使用,Activity向系統注冊URL Scheme,就可以通過特定的URL,直接打開App中的某些頁面,常用于支持以下幾種場景:

  • 服務器下發跳轉路徑,客戶端根據服務器下發跳轉路徑,創建Intent,(new Intent(Intent.ACTION_VIEW, Uri.parse(Url) ) ) ,實現跳轉;
  • Web頁面點擊,根據具體跳轉路徑判斷,如果(url.indexOf(H5Constant.SCHEME) != -1),就去創建Intent,(Intent intent = new Intent(Intent.ACTION_VIEW, uri) ) ,實現跳轉;
  • App端收到服務器端下發的通知,從消息中解析NotifeConstant.VALID_ACTION_URL得到URL,然后創建Intent,(Intent intent =H5Constant.buildSchemeFromUrl(Url)),實現跳轉。

Scheme協議中,URL的格式為:
[scheme:][//domain][:port][path][?query params][#fragment]
其中,scheme、domain、path都需要在manifest中定義。

<activity android:name=".TargetActivity">
   <intent-filter>
     <action android:name="android.intent.action.VIEW"/>
     <category android:name="android.intent.category.DEFAULT"/>
     <!--允許從瀏覽器打開App-->
     <category android:name="android.intent.category.BROWSABLE"/>
     <!--可以定義多組scheme,多個URL指向同一個Activity-->
     <data android:scheme="biz1" 
        android:host="com.buy"
        android:path="/path"/>
     <data android:scheme="biz2"
       android:host="com.earn"
       android:path="/route"/>
   </intent-filter>
</activity>

被啟動的Activity可以從Intent中找到URL中的值

Intent intent = getIntent();    
String scheme = intent.getScheme();    
Uri uri = intent.getData();   
String tab= uri.getQueryParameter("your param"); 

引用

Activity啟動模式圖文詳解:standard, singleTop, singleTask 以及 singleInstance
Android中Activity四種啟動模式和taskAffinity屬性詳解
android深入解析Activity的launchMode啟動模式,Intent Flag,taskAffinity
一次搞定Process和Task

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容