Android 面試黑洞——當我按下 Home 鍵再切回來,會發生什麼?

不少 Android 工程師在投簡歷找工做以前,會去補習一下 Activity 的啓動模式(launchMode),由於面試的時候常常會考。但真正把它搞懂的人是不多的——包括很多拿它作面試題的面試官。

就像我在視頻標題裏說的,當用戶在使用 App 的時候按下了 Home 鍵,而後再切回來,或者在多個 App 之間切來切去,App 的內容會不會改變、會怎麼改變、要怎麼讓它按你的需求去變或不變,這些問題都須要你對 launchMode 有足夠的瞭解。並且不僅是 launchMode,這是一個以 Activity 的回退棧(Back Stack)爲中心的大話題。android

插圖:面試

  • 的 launchMode:
    • standard
    • singleTop
    • singleTask
    • singleInstance
  • Intent.FLAG_ACTIVITY_***
    • FLAG_ACTIVITY_NEW_TASK
    • FLAG_ACTIVITY_SINGLE_TOP
    • FLAG_ACTIVITY_CLEAR_TOP
    • FLAG_ACTIVITY_MULTIPLE_TASK
    • FLAG_ACTIVITY_NEW_DOCUMENT
    • FLAG_ACTIVITY_REORDER_TO_FRONT
    • FLAG_ACTIVITY_PREVIOUS_IS_TOP
    • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    • FLAG_ACTIVITY_RETAIN_IN_RECENTS
    • FLAG_ACTIVITY_TASK_ON_HOME
  • 的 android:taskAffinity
  • 的 android:allowTaskReparenting
  • 的 android:clearTaskOnLaunch
  • Activity 的回退棧(Task)
  • Android 的最近任務列表(Recents / Overview)切換
  • 啓動器(桌面)的 App 圖標點擊
  • ……


你把這個大話題弄明白了,才能夠指哪打哪,爲所欲爲。面試官有時候問一些比較刁鑽的 launchMode 的問題,其實也不是爲了刁難你,這都是對實際開發有用的,只是它比較難掌握而已。

因此今天,我就把 launchMode 以及和它相關的這一大套東西,給你們講清楚。安全帶繫好了。 安全

視頻先行

要看視頻的能夠直接去 嗶哩嗶哩 或者 YouTube 觀看。微信

強烈建議掃碼看視頻版本!
markdown

強烈建議掃碼看視頻版本!
ide

強烈建議掃碼看視頻版本!
oop

本期視頻用了大量的 3D 動畫來配合講解,好比這樣: 微信圖片_20201015144036.gif 因此有條件的話強烈建議觀看視頻版本,由於本期的文字版可能會比較不適合閱讀。測試


下面的文字是本期視頻的腳本,爲了方便閱讀才修改爲了文章的格式。因此若是你點開視頻,下面的文字就不用看了。
動畫

Task 和回退棧

你們好,我是扔物線朱凱。spa

先問個問題:當咱們在 Android 手機裏點了最近任務的方塊鍵,咱們看到的這是一個個的……什麼?


一個個…… Activity?一個個…… App?咱們看到的是一個個……Task,任務。

當咱們的 App 圖標在桌面上被點擊的時候,App 的默認 Activity——也就是那個配置了 MAIN + LAUNCHER 的 intent-filter 的 Activity——會被啓動,而且這個 Activity 會被放進系統剛建立的一個 Task 裏。咱們經過最近任務鍵能夠在多個 App 之間進行切換,但其實更精確地說,咱們是在多個 Task 之間切換。

每一個 Task 都有一個本身的回退棧,它按順序記錄了用戶打開的每一個 Activity,這樣就能夠在用戶按返回鍵的時候,按照倒序來依次關閉這些 Activity。當回退棧裏最後一個 Activity 被關閉,這個 Task 的生命也就結束了。


但它並不會在最近任務列表裏消失。系統依然會保留這個 Task 的一個殘影給用戶,目的是讓用戶能夠方便地「切回去」;只是這種時候的所謂「切回去」,實際上是對 App 的從新啓動,由於原先的那個 Task 已經不存在了。

因此,在最近任務裏看見的 Task,未必是還活着的。

singleTask

Activity 是一個能夠跨進程、跨應用的組件。當你在 A App 裏打開 B App 的 Activity 的時候,這個 Activity 會直接被放進 A 的 Task 裏,而對於 B 的 Task,是沒有任何影響的。


爲何?爲何這麼設計?

首先咱們想想:咱們爲何要打開別的 App 的 Activity?由於它提供了一個通用的功能,對吧?好比通信錄 App 可能會提供一個添加聯繫人的 Activity 供其餘 App 使用。那麼這些通用的功能,它的邏輯是和誰相關的?好比我從短信 App 裏點擊一個電話號碼,選擇「新建聯繫人」,而後通信錄 App 提供的添加聯繫人 Activity 就會被打開,對吧?這個 Activity 它的邏輯是和哪一個 App 相關的?和短信相關嗎?相關的,由於它是從短信跳過來的嘛,它們是在一整個邏輯鏈條上的。換句話說,若是我如今按了返回鍵,我會回到剛纔的短信界面。是吧?那它和通信錄相關嗎?是不相關的。所謂不相關,就是在這個時候用戶若是按下最近任務的方塊鍵,他不該該看到通信錄的 Task;而若是他如今回到桌面,點擊通信錄的圖標,他看到的也不該該是這個添加聯繫人的頁面,而應該是一個聯繫人列表,由於用戶的這個操做大機率是要查看通信錄;相反,在這個時候他再切回短信 App,他應該回到剛纔的添加聯繫人頁面,繼續編輯聯繫人信息。因此對於「添加聯繫人」這個頁面來講,它是和打開它的那個 App 有相關性,而不是和提供它的 App,對吧?更確切地說,也不是和打開它的 App 相關,而是和打開它的 Task 相關。是這回事吧?而這個邏輯,實際上也是 Android 默認的規則。當你在不一樣的 Task 裏打開相同的 Activity 的時候,這個 Activity 會被建立出不一樣的實例,分別放在每個 Task 裏,互不干擾。這是符合產品邏輯,也是符合用戶心理的。

可是!這只是默認的規則。有的時候咱們會須要不一樣的產品邏輯。好比我在短信裏點擊的不是電話號碼,而是一個郵箱地址,那麼個人郵箱 App 提供的編寫郵件的 Activity 就會被打開,對吧?這個時候,這個編寫郵件的 Activity,它的邏輯是和哪一個 App 相關的?首先,依然是和短信 App 相關的,對吧?緣由跟剛纔同樣,它是從短信打開的。那麼它和郵箱 App 相關嗎?也是相關的。由於按照用戶使用郵件的習慣,若是如今按下最近任務鍵,用戶會指望看到郵箱 App 的 Task 出如今短信 Task 的旁邊,而且當它點擊這個 Task,或者當它切回桌面點擊郵箱 App 的圖標,他都會指望回到寫郵件的界面繼續寫。編寫郵件和添加聯繫人這兩件事並無本質的不一樣,只是用戶不一樣的心理預期決定了咱們要有不一樣的產品邏輯。因此若是大家也作通信錄或者郵箱,並且產品邏輯和我說的不同,不要緊,這是產品經理負責的事,我在說的是若是你有怎樣的產品邏輯,你應該怎麼寫。

那麼若是我要作這種邏輯的郵箱,我應該怎麼辦呢?很簡單,只要在 AndroidManifest.xml 裏把這個編寫郵件的 的 launchMode 設置爲 singleTask 就好了。


singleTask 可讓 Activity 被別的 App 啓動的時候不會進入啓動它的 Task 裏,而是會在屬於它本身的 Task 裏建立,放在本身的棧頂,而後把這整個 Task 一塊兒拿過來壓在啓動它的 Task 上面。這種邏輯能夠保證,無論是從哪一個 App 啓動,被標記爲 singleTask 的 Activity 老是會被放在本身的 Task 裏。若是你仔細留意也會發現,這種方式打開的 Activity 的入場動畫是應用間切換的動畫,而不是普通的 Activity 入場動畫。這種不一致並非 Android 不拘小節不修邊幅,相反,這是在刻意地提醒用戶:你在進行跨任務操做。這時候用戶若是點返回鍵,界面會顯示你的 App 裏的上一個 Activity,而不是直接返回到以前的 App。直到用戶反覆按返回鍵,把這個 App 全部的 Activity 全都關閉了,上面的 Task 消失,下面的 Task 纔會出來,也就是對於咱們的例子來講,短信 App 纔會露出來,並且此次,又變成了應用間切換的動畫——確切地說,是 Task 間切換的動畫。


也就是說,不止 Activity 在 Task 內部能夠疊成棧,不一樣的 Task 之間也能夠疊起來。不過有一點:Task 的疊加,只適用於前臺 Task,前臺疊加的多個 Task 在進入後臺的第一時間就會被拆開。前臺 Task 進入後臺最多見的場景有兩種:按 Home 鍵回到桌面,以及按最近任務鍵查看最近任務。須要注意的是:前臺 Task 是在顯示最近任務的時候就已經進入了後臺,而不是在你切換到其餘應用以後。因此若是用戶從短信進入郵箱之後沒有按直接返回鍵,而是先查看一下最近任務再立刻按返回鍵切回去,這個時候雖然表面上看着沒變,但實際上前臺 Task 已經只剩下了一個。如今若是用戶再連續按返回鍵關掉郵件 App 的 Task,他就不會回到短信了,而是直接回到桌面。


我以爲這個其實有點反用戶直覺的。我只是切出去再切回來,怎麼就變了?可是,Android 就是這麼工做的。

allowTaskReparenting

除了 singleTask,對於新建郵件這種場景,還有一種解決方案是使用一個叫作 allowTaskReparenting 的屬性。Activity 默認狀況下只會歸屬於一個 Task,不會在多個 Task 之間跳來跳去,但你能夠經過設置來改變這個邏輯。若是你不是用 singleTask 來設置編寫郵件的 Activity,而是把它的 allowTaskReparenting 屬性設置爲 true,那麼當用戶從短信裏打開這個 Activity 的時候,它雖然依然會進入短信 App 的 Task 裏,但當稍後用戶再從桌面點開郵件 App 的時候,原先那個放在短信 Task 裏的 Activity 會被挪過來,放進郵件 App 的 Task 裏,在回退棧的頂端被顯示出來;而這時候你再切回短信,也會發現那個 Activity 已經不見了。這也就是所謂的「Task Reparenting」。你打開個人時候,我在你的 Task 裏;稍後我又能夠回到我本來所屬的 Task 來。

這跟 singleTask 比起來,由於 Activity 剛被打開的時候並無發生 Task 切換,因此也沒有 Task 切換的誇張的入場動畫,對於用戶是無感知的;並且由於只有一個 Task,用戶切到後臺再切回來的時候也不會像 singleTask 那樣被切斷本身的回退路徑。

好用吧?

不過!很噁心的是,我發現從 Android 9 開始,這個屬性失效了!不知道是否是由於這個屬性用的人太少了,致使 Android 團隊把這個屬性改壞了也沒發現,就這麼發佈出來了。(冷笑——摔手機。)並且我還發現,在最新的 Android 11 上,這個屬性又被修好了,工做正常了!總之,這個屬性的設計是很好的,但它在 Android 9 和 10 的手機上是壞的——我還專門拿個人三星 S20 也測試了一下,確認了三星也沒有修復這個問題。可是用戶可不會怪手機,更不會怪系統,他們只會怪你的 App 難用。因此這個 allowTaskReparenting,雖然很好用,但若是你要用,請作好測試以及各類心理準備。

singleInstance

singleTask 除了保證 Activity 在固定的 Task 裏建立,還有一個行爲規則:若是啓動的時候這個 Task 的棧裏已經有了這個 Activity,那麼就再也不建立新的對象,而是直接複用這個已有的對象;同時,由於 Activity 沒有被重建,系統也就不會調用它的 onCreate() 方法,而是調用它的 onNewIntent() 方法,讓它能夠從 Intent 裏解析數據來刷新界面(若是須要的話);另外在調用 onNewIntent() 以前,若是這個 Activity 的上面壓着的有其餘 Activity,系統也會把這些 Activity 所有清掉,來確保咱們要的 Activity 出如今棧頂。

那麼這樣 singleTask 實際上是既保證了「只有一個 Task 裏有這個 Activity」,又保證了「這個 Task 裏最多隻有一個這個 Activity」,因此雖然它名字叫 singleTask,但它在實質上限制了它所修飾的 Activity 在全局只有一個對象。

在 singleTask 以外,Android 還提供了一種更完全的 launchMode 的選項:singleInstance。

剛纔我說,singleTask 實際上是個事實上的全局單例,是吧?那這個 singleInstance 單一實例又是什麼意思呢?它的行爲邏輯和 singleTask 基本是一致的,只是它多了個更嚴格的限制:它要求這個 Activity 所在的 Task 裏只有這麼一個 Activity——下面沒有舊的,上面也不準有新的。

具體來講,好比我把編寫郵件的 Activity 設置成了 singleInstance,那麼當用戶在短信 App 裏點擊了郵件地址以後,郵件 App 不只會建立這個 Activity 的對象,並且會建立一個單獨的 Task 來這個 Activity 放進去,或者若是以前已經建立過這個 Task 和 Activity 了,那就像 singleTask 同樣,直接複用這個 Activity,調用它的 onNewIntent();另外,這個 Task 也會被拿過來壓在短信 Task 的上面,入場動畫是切換 Task 的動畫。這時候若是用戶點擊返回,上面的 Task 裏由於只有一個 Activity,因此手機會直接回到短信 App,出場動畫也是切換 Task 的動畫;而若是用戶沒有直接點擊返回,而是先看了一下最近任務又返回來,這時候由於下面的短信的 Task 已經被推到後臺,因此用戶再點返回的話,就會回到桌面,而不是回到短信 App;而若是用戶既沒有點擊返回也沒有切後臺,而是在編寫郵件的 Activity 裏又啓動了新的 Activity,那麼因爲 singleInstance 的限制,這個新打開的 Activity 並不會進入當前的 Task,而是會被裝進另外一個 Task 裏,而後隨着這個 Task 一塊兒被拿過來壓在最上面。


這就是 singleInstance 和 singleTask 的區別:singleTask 強調的只是惟一性:我只會在一個 Task 裏出現;並且這個 Task 裏也只會有一個個人實例。而 singleInstance 除了惟一性,還要求獨佔性:我要獨自霸佔一個完整的 Task。

那麼在實際的操做中,它們的區別就是:在被啓動以後,用戶按返回鍵時,singleTask 會在本身的 App 裏進行回退,而 singleInstance 會直接回到原先的 App;以及用戶稍後從桌面點開 Activity 所在的 App 的時候,singleTask 的會看到這個 Activity 依然在棧頂,而 singleInstance 的會看到這個 Activity 已經不見了——它在哪?它並無被殺死,而是在後臺的某個地方默默蹲着,當你再次啓動它,它就會再次跑到前臺來,並被再獲得一次 onNewIntent() 的回調。

剛纔我說,在最近任務裏看見的 Task 未必還活着;那麼這裏就能夠再加一句:在最近任務裏看不見的 Task,也未必就死了,好比 singleInstance。

taskAffinity

那既然它還活着,爲何會被藏起來呢?由於它們的 taskAffinity 衝突了。

在 Android 裏,一個 App 默認只能有一個 Task 顯示在最近任務列表裏。但其實用來甄別這份惟一性的並非 App,而是一個叫作 taskAffinity 的東西。Affinity 就是類似、有關聯的的意思,在 Android 裏,每一個 Activity 都有一個 taskAffinity,它就至關因而對每一個 Activity 預先進行的分組。它的值默認取自它所在的 Application 的 taskAffinity,而 Application 的 taskAffinity 默認是 App 的包名。

另外,每一個 Task 也都有它的 taskAffinity,它的值取自棧底 Activity 的 taskAffinity;咱們能夠經過 AndroidManifest.xml 來定製 taskAffinity,但在默認狀況下,一個 App 裏全部的 Task 的 taskAffinity 都是同樣的,就是這個 App 的包名。當咱們啓動一個新的 Task 的時候——好比開機後初次點開一個 App——這個 Task 也會獲得一個 taskAffinity,它的值就是它所啓動的第一個 Activity 的 taskAffinity。當咱們繼續從已經打開的 Activity 再打開新的 Activity 的時候,taskAffinity 就會被忽略了,新的 Activity 會直接入棧,無論它來自哪;但若是新的 Activity 被配置了 singleTask,Android 就會去檢查新的 Activity 和當前 Task 的 taskAffinity 是否是相同,若是相同就繼續入棧,而若是不一樣,新 Activity 就會進入和它本身的 taskAffinity 相同的 Task,或者建立一個新的 Task。

因此當你在 App 裏啓動一個配置了 singleTask 的 Activity,若是這個 Activity 來自別的 App,就會發生 Task 的切換;而若是這個 Activity 是你本身 App 裏的,你會發現它直接進入了當前 Task 的棧頂,由於這種狀況下新 Activity 和當前的 Task 的 taskActivity 是相同的。而你若是再給這個 Activity 設置一個獨立的 taskAffinity,你又會發現,哪怕是同一個 App,這個 Activity 也會被分拆到另外一個 Task 裏。並且若是這個獨立設置的 taskAffinity 剛好和另外一個 App 的 taskAffinity 同樣,這個 Activity 還會直接進入別人的 Task 去。

當咱們查看最近任務的時候,不一樣的 Task 會並列展現出來,但有一個前提:它們的 taskAffinity 須要不同。在 Android 裏,同一個 taskAffinity 能夠被建立出多個 Task,但它們最多隻能有一個顯示在最近任務列表。這也就是爲何剛纔例子裏 singleInstance 的那個 Activity 會從最近任務裏消失了:由於它被另外一個相同 taskAffinity 的 Task 搶了排面。

說到這兒,有一點須要注意,Android 的官方文檔在 launchMode 方面的描述有不少的錯誤和自相矛盾。好比官方文檔裏說 singleTask 「只會出如今棧底」,但其實徹底沒有這回事。咱們在官方文檔裏看到的錯誤通常是什麼呢:錯別字,或者有歧義、有誤導性的表達。可是這個錯誤說實話讓我有點莫名其妙,就是你根本無法猜出來寫文檔的人的本來想表達的是什麼意思,給個人感受就跟造謠似的。總之你若是在官方文檔裏看到一些和你的測試結果不符的描述,以你的測試爲準;或者若是你發現它有一些話自相矛盾,你就當它沒說。

singleTop

launchMode 除了剛纔講的默認的——也就是 standard——和 singleTask 以及 singleInstance 以外,還有一種叫作 singleTop。singleTop 雖然名字上也帶有一個 single,但它的關係和默認的 standard 其實更近一些。它和默認同樣,也是會直接把 Activity 建立以後加入到當前 Task 的棧頂,惟一的區別是:若是棧頂的這個 Activity 剛好就是要啓動的 Activity,那就不新建了,而是調用這個棧頂的 Activity 的 onNewIntent()。

簡單說來就是,默認的 standard 和 singleTop 是直接摞在當前的 Task 上;而 singleTask 和 singleInstance 則是兩個「跨 Task 打開 Activity」的規則,雖然也不是必定會跨 Task,但它們的行爲規則展示出了很強的跨 App 交互的意圖。在實戰上,咱們會比較多地在 App 內部使用默認和 singleTop;singleInstance 會比較多用於那些開放出來給其餘 App 一塊兒用的共享 Activity;而 singleTask 則是個兼容派,內部交互和外部共享都用得着。至於具體用誰,就要根據需求具體分析了。

總結

講了這麼多,其實一直都在圍繞任務啓動和任務切換的問題,瞄準的就是更精準可控的界面導航。若是記不全,Task 的工做模型必定要記住,這是最核心最重要的。別的你均可以忘,這個模型必定記清楚了,這能讓你站在一個更高的高度去理解 Android 的 Activity 啓動和任務切換,對工做會很是有幫助,並且這些內容是你不管在網上現有的博客仍是官方文檔裏都很難看到的。 至於更多的細節,好比這些啓動模式的一些坑,Intent 的 FLAG_ACTIVITY_ 打頭的 Flag,以及 AndroidManifest.xml 裏更多的配置參數,我就不一一細講了。只要你把我今天說的 Task 的工做模型搞清楚,再把剛纔講的這四種 launchMode 想明白,那些細節很容易就能夠掌握。學技術,就是要學到本質,以不變應萬變。 若是你實在連這最後一步也懶得研究,就是想躺着把各類細節都學了,來我知識星球吧,全都有。 另外若是你想全方位提高本身的 Android 技能,快速升級、快速跳槽提薪,個人系列化課程應該會更適合。 那麼今天的內容就到這裏,你們喜歡的話別忘了三連和轉發,讓更多須要的人看到。我是扔物線,我不和你比高低,我只助你成長,咱們下期見。

相關文章
相關標籤/搜索