今天想來說說Android的啟動模式,一來自己做一個總結,二來如果能幫助到別人就更好了~
首先來看一個實際的業務場景。我之前在公司實習的時候,我所在的部門只負責一個品類(國際機票),那用戶從機票首頁開始搜索機票到最終完成訂單并支付大致分為以下幾個流程(實際流程因為考慮的問題比較多,所以要比這稍微復雜些):
1. 用戶在機票首頁確認好出發、到達目的地、日期及人數后,就可以點擊“搜索”進入到搜索結果頁
2. 用戶根據意愿選擇相應航班并點擊進入確認訂單頁
3. 用戶確認好訂單內容點擊下單,開始支付,支付完成跳支付完成頁
4. 用戶可以選擇回到首頁,或者去公共訂單系統查看訂單詳情(這屬于公共部分,不在我們討論的范圍內了)
經過上面4個步驟整個購票流程就算結束了。但是,這里有個問題,就是用戶從首頁到最后的支付完成頁之間經歷了這么多的Activity,那用戶完成了支付后想回到首頁是不是要一層一層的往回退才行?如果是這樣的話,那就太不人性化了,那怎樣才能在完成支付后一鍵回到首頁呢?有人可能會說,那就直接從支付完成頁跳轉到首頁不就可以了嗎?這樣當然是可以的,但是這樣做那之前打開的那么多Activity怎么辦呢?難道就讓它們待在Activity任務棧中嗎?這樣豈不是太浪費內存了!請看我畫的示意圖:
如上圖所示,如果我們在支付完成之后啟動首頁Activity那頁面1-頁面5之間的所有Activity對用戶來說都是無用的,并且占用的內存,極大地浪費這本來可能就很緊張的Android內存資源。那有沒有一些優雅的方式來解決這個問題呢? 我們可以想象一下,有沒有可能在從頁面4跳轉首頁的時候把原來處于首頁之上的所有Activity全部干掉呢?這樣不就剛好解決了我們剛剛所說浪費資源的問題了嗎?就像下圖一樣:
很顯然,無論從用戶體驗的角度還是從內存優化的角度來看第二種方式都是最為優雅的。
那怎么實現這種需求呢?有人可能會說,可以將任務棧中頁面1之上的所有Activity一層一層地執行finish()方法銷毀掉,這樣就可以回退到頁面1。這樣確實是可行的,但是有個問題:如果希望從頁面4跳轉到頁面1時傳遞一些數據回去,比如說訂單號、支付完成等信息,還需要在finish()執行前執行setResult(int resultCode, Intent intent)將要往回傳遞的數據放在Intent里面。
這樣做其實是有風險的,因為Android系統并不保證Activity任務棧中那些不可見的Activity的狀態是一直被保存著的,如果出現系統內存不足的情況,Android系統是可以回收那些處于不可見狀態的Activity的。也就是說,一旦中間有一Activity被銷毀了,那這個傳遞鏈就失去功效了。
那應該怎么辦呢?別著急,Google的Android工程師早就已經為我們想到這種需求了。那應該怎么做呢?其實就是通過設置Activity的啟動模式來實現。好,問題拋出了,下面讓我們一步一步來看。
Activity有哪幾種啟動模式?
Activity一共有4種啟動模式,分別是:
- standard
- singleTop
- singleTask
- singleInstance
下面我來分別做介紹。
一、standard
顧名思義,standard英文意思就是“標準的”。
也就是說這種啟動模式是默認的,我們平時在開發中使用最多的就是Standard模式的。
如果一個Activity的啟動模式被設置成standard,那么它可以無限制的創建。你每一次通過Intent去啟動這種模式的Activity都會重新創建一個。
大家可以想象一下郵箱里的收件箱(假設我們將打開郵件的Activity的啟動模式設置為Standard,當然這也是默認的模式)里有10封郵件。我們給查看郵件的Activity起名為CheckEmailActivity,我點擊第一封郵件將會打開一個CheckEmailActivity,當我看完之后點擊下一封郵件,另一個CheckEmailActivity又會被創建,這樣如果我們將10封郵件全部看完,那在Activity任務棧中將會有10個CheckEmailActivity,而且如果我想回到收件箱頁面還必須點10次返回鍵!想想是不是很可怕?
所以說standard模式雖然很常用,但也不是適用于任何場合。
另外說一點,standard模式在Android 5.0(Lollipop)之前和之后是有區別的。
** Android Lollipop之前**
standard模式的Activity總是會被創建在啟動它的Activity同一個任務棧中頂端(任務棧是一個棧結構,先進后出 First In Last Out),就算他們來自不同的應用。
想象一個場景,如果你在A應用中要分享一個本地圖片,這樣會打開系統的圖片查看應用中的圖片選擇器Activity,雖然這兩個Activity來自不同的應用,但Android系統仍將會把他們放在同一個任務棧中,即A應用的任務棧中。
Android Lollipop之后
如果將要啟動的Activity和啟動它的Activity來自同一個應用,那沒話說,和Lollipop之前一樣,新的Activity會被創建在當前任務棧中的頂端。
但是如果它們來自不同的應用,那就會創建一個新的任務棧,再把要啟動的Activity放在新的任務棧中,這時這個新啟動的Activity就是新創建的任務站點的根Activity。如下圖所示:
二、singleTop
顧名思義,singleTop的意思就是“在頂部只能有一個”。
這種啟動模式非常類似于standard,但是也有一些 區別:
如果在啟動這種模式的Activity的時候,當前任務棧的頂端已經存在了相同的Activity,那系統就不會再創建新的,而是回調任務棧中已經存在的該Activity的onNewIntent( )方法。請看下面的示意圖:
也正因為SingleTop啟動模式的特殊性,所以在開發時,如果指定了一個Activity的啟動模式是singleTop的那就應該既要重寫onCreated()方法用于應對第一次創建的情況,也要重寫onNewIntent( )方法來應對重復創建的情況。
其實大家可以想象一下,這種啟動模式的應用場景。Android既然提供了這種啟動模式,說明肯定有應有場景需要這樣的方式。其實最常用的場景就是搜索,比方說我們在搜索框中輸入想要搜索的內容點擊搜索進入SearchResultActivty(搜索結果頁)查看搜索的結果(一般我們也會在搜索結果頁提供搜索框,這樣用戶無需點擊返回鍵回到上一個頁面再在搜索框中輸入搜索內容點擊搜索),如果此時用戶還想搜點別的東西,就可以直接在當前的搜索結果頁SearchResultActivty中的搜索框輸入搜索內容繼續搜索。
大家想象一下,如果我們把SearchResultActivty的啟動模式設置為Standard的話會是什么樣的景象。比如我們連著搜了10個內容,那就會啟動10個不同的SearchResultActivty,然而這些SearchResultActivty功能完全一樣,完全沒有必要創建這么多,而且還有一個和上一節中的郵箱一樣的問題,就是用戶搜索結束想回到首頁,那就還得按10次返回鍵才能回到首頁,- -!
這時,singleTop啟動模式就派上用場了,我們首先把SearchResultActivty的啟動模式設置為singleTop,這樣用戶在SearchResultActivty頁面中繼續搜索的時候,我們只需把用戶要搜索的內容放在Intent里面然后啟動SearchResultActivty,這時系統并不會重新創建新的SearchResultActivty,而是回調當前任務棧棧頂的SearchResultActivty的onNewIntent()方法來接收帶有用戶搜索內容信息的Intent,然后我們拿到用戶搜索內容后調搜索接口,并根據接口返回內容重新刷新布局即可,似不似很神奇?其實我們在上一節提到的郵箱的問題,也是用這種方式來解決的,原理和搜索一樣的。
三、singleTask
這種啟動模式的Activity在Android系統中只允許存在一個實例。
如果系統中已經存在了該種啟動模式的目標Activity,則系統并不會重新創建一個目標Activity,而是首先將持有目標Activity的整個任務棧都會被置于前臺(用戶可見),并且通過onNewIntent( )方法將啟動目標Activity的Intent傳遞給目標Activity,置于目標Activity拿到這個Intent之后要做什么操作,系統就不管了,隨便你拿來干什么,哼~。
但是這里有個問題,就是目標Activity和源Activity是不是來自同一應用。
源Activity和目標Activity來自同一個應用
這種情況還要分兩種情況說:
當前系統中還沒有目標Activity的實例
這種情況最簡單,直接在當前的任務棧中創建SingleTask模式的Activity并置于棧頂即可。
當前系統中已經存在目標Activity的實例
這種情況比較特殊,因為系統會把任務棧中目標Activity之上的所有Activity銷毀,以讓目標Activity處在棧頂的位置。
這里還要還要再提醒大家的是,因為目標Activity已經存在,系統不會重新創建,而是通過onNewIntent()的方式把Intent傳遞過來,這點和singleTop模式有些類似。注意了,這里讓我們回想一下文章開頭的我所說的場景,如何讓用戶在支付完成頁直接跳轉到首頁,并把不需要的Activity銷毀?SingleTask啟動模式是不是剛好和我們的需求一致?請看下面的示意圖:
源Activity和目標Activity來自不同應用
這種情況也要分兩種情況說:
當前系統中還沒有目標Activity的實例
這時系統首先會看任務管理器中是否有目標Actvity所在應用的任務棧?如果有的話,那就直接在目標Activity所在應用的任務棧的棧頂創建即可。
如果任務管理器中沒有目標Activity所在應用的任務棧,系統就會創建其所在應用的任務棧和目標Activity,并且把目標Activity作為新建任務棧的根Activity。如下圖所示:
** 當前系統中已經存在目標Activity的實例**
目標Activity所在任務棧會被置于前臺(即用戶可見),而且也會把目標Activity之上的所有Actvity全部銷毀。
四、singleInstance
這種啟動模式和singleTask幾乎一樣,它也只允許系統中存在一個目標Activity,包括上面我們所說的SingleTask的一些特性singleInstance都有。唯一不同的是,持有目標Activity的任務棧中只能有目標Activity一個Actvitiy,不能再有別的Activity,對! 就是承包了這個任務棧!哈哈~。
其實從這種啟動模式的名字也可以看出來它表示的意思,singleInstance直譯過來就是“單一實例”,什么意思呢?這話啊有兩層意思,我來給你分析分析:1. 跟系統說,“我是獨一無二的,不許和我一樣的人存在!”,這就是說系統中存在一個目標Activity。;2. 跟任務棧說,“我是獨一無二的,不許你心里再裝別的人!”,這就是說持有目標Activity的任務棧中只能有目標Activity一個Activity。這樣說是不是好理解一些,哈哈~
所以,如果要啟動singleInstance模式的Activity,那只能新創建一個任務棧用來放它,因為人家說了,“我是獨一無二的!”。同樣的,如果從這種啟動模式的Activity中啟動別的Activity,那不好意思,我不管你是不是和我處在同一個應用,我所在的任務棧只能擁有我一個人,您吶,另外讓系統給你創建一個任務棧待著去吧。
好了,至此我們介紹了Activity的4種啟動模式了,也大致了解了每種啟動模式的特點了,那接下里的問題就是怎么使用呢?問題又拋出來了,好,讓我們接著往下看。
怎么使用啟動模式?
有兩種方式來使用或者說設置Activity的啟動模式:
方式1:在AndroidMenifest.xml文件中設置:
看到沒有,在<activity>標簽中設置android:launchMode="****"屬性即可,****即我們上面所說的四種啟動模式。
方式2:通過為Intent添加標識來設置
看到沒有,這里使用Intent的addFlags()方法來添加一些標志,其實這個addFlags()不光可以用來設置Activity的啟動模式,還能做很多事情,它的作用是給Intent添加一些附加屬性。具體的可以參見Android api哈~
那我們想設置Activity的啟動模式應該給addFlags()方法設置哪些參數呢?來,接著往下看:
FLAG_ACTIVITY_NEW_TASK
與"singleTask"啟動模式的作用一樣。FLAG_ACTIVITY_SINGLE_TOP
與"singleTop"啟動模式的作用一樣。FLAG_ACTIVITY_CLEAR_TOP
這個標識的意思比較特殊。它不對應于我們上面所說的啟動模式中的任何一種,我們來看一下android api中對這個標識的說明:
“如果正在啟動的 Activity 已在當前任務中運行,則會銷毀當前任務頂部的所有 Activity,并通過onNewIntent()
將此 Intent 傳遞給 Activity 已恢復的實例(現在位于頂部),而不是啟動該 Activity 的新實例。”
大家可能會發現,通過addFlags()的方式來設置啟動模式有局限性,只能顯示的設置“singleTask”和“singleTop”兩種啟動模式,而并沒有對應“standard”和“singleInstance”啟動模式的標識。是的,android api文檔中確實只只有以上三種標識用來設置啟動模式,而且第三種“**FLAG_ACTIVITY_CLEAR_TOP
**”還不對應任何一種啟動模式,難道可以算作第5種啟動模式?遺憾的是,我現在也不清楚是怎么回事。
但是實際開發中,我們一般都是在AndroidMenifest.xml文件中去設置Activity的啟動模式。
好了,文章差不多就寫到這吧。謝謝觀看~
多說一句,關于Android任務和返回棧的相關知識可以參見Android api文檔,上面說的很詳細,其實我習慣把返回棧說成是任務棧,不知道這樣是不是合適,嘿嘿~
參考文獻:
https://developer.android.com/guide/topics/manifest/activity-element.html
https://developer.android.com/guide/components/tasks-and-back-stack.html
https://inthecheesefactory.com/blog/understand-android-activity-launchmode/en