Android系統中的Activity可以說一件很贊的設計,它在內存管理上良好的設計,使得多任務管理在Android系統中運行游刃有余。但是Activity絕非啟動展示在屏幕而已,其啟動方式也大有學問,本文講具體介紹Activity的啟動模式的諸多細節,糾正一些開發中可能錯誤的觀點,幫助大家深入理解Activity。
在正式行文之前,先介紹一些文章提到的概念
本文圖片較多,在看圖時,請注意觀察Activity頂部的title,來區分具體Activity。
應用中的每一個Activity都是進行不同的事物處理。以郵件客戶端為例,InboxActivity目的就是為了展示收件箱,這個Activity不建議創建成多個實例。而ComposeMailActivity則是用來撰寫郵件,可以實例化多個此Activity對象。合理地設計Activity對象是否使用已有的實例還是多次創建,會使得交互設計更加良好,也能避免很多問題。至於想要達到前面的目標,就需要使用今天的Activity啟動模式。
使用很簡單,只需要在manifest中對應的Activity元素加入android:launchMode屬性即可。如下述代碼
1 2 3 4 5
<activity
android:name=".SingleTaskActivity"
android:label="singleTask launchMode"
android:launchMode="singleTask">
</activity>
接下來就是介紹launchMode的四個值的時刻了。
這是launchMode的默認值,Activity不包含android:launchMode或者顯示設置為standard的Activity就會使用這種模式。
一旦設置成這個值,每當有一次Intent請求,就會創建一個新的Activity實例。舉個例子,如果有10個撰寫郵件的Intent,那麼就會創建10個ComposeMailActivity的實例來處理這些Intent。結果很明顯,這種模式會創建某個Activity的多個實例。
這種Activity新生成的實例會放入發送Intent的Task的棧的頂部。下圖為啟動同一程序內的Activity。
下面的圖片展示跨程序之間調用,新生成的Activity實例會放入發送Intent的Task的棧的頂部,盡管它們屬於不同的程序。
但是當我們打開任務管理器,則會有一點奇怪,應為顯示的任務是Gallery,展示的界面確實另一個程序的Activity(因為其位於Task的棧頂)。
這時候如果我們從Gallery應用切換到撥號應用,再返回到Gallery,看到的還是這個非Gallery的Activity,如果我們想要對Gallery進行操作,必須按Back鍵返回到Gallery界面才可以。確實有點不太合理。
對於同一應用內部Activity啟動和5.0之前表現一樣,變化的就是不同應用之間Activity啟動變得合理了。
跨應用之間啟動Activity,會創建一個新的Task,新生成的Activity就會放入剛創建的Task中。如下圖
同時任務管理器查看任務也顯得更加合理了。
假設之前存在我們的測試程序,然後從Gallery又分享文件到我們的測試程序,則對應的任務管理器展示效果如下。
使用場景:standard這種啟動模式適合於撰寫郵件Activity或者社交網絡消息發布Activity。如果你想為每一個intent創建一個Activity處理,那麼就是用standard這種模式。
singleTop其實和standard幾乎一樣,使用singleTop的Activity也可以創建很多個實例。唯一不同的就是,如果調用的目標Activity已經位於調用者的Task的棧頂,則不創建新實例,而是使用當前的這個Activity實例,並調用這個實例的onNewIntent方法。 在singleTop這種模式下,我們需要處理應用這個模式的Activity的onCreate和onNewIntent兩個方法,確保邏輯正常。
關於singleTop一個典型的使用場景就是搜索功能。假設有一個搜索框,每次搜索查詢都會將我們引導至SearchActivity查看結果,為了更好的交互體驗,我們在結果頁頂部也放置這樣的搜索框。
假設一下,SearchActivity啟動模式為standard,那麼每一個搜索都會創建一個新的SearchActivity實例,10次查詢就是10個Activity。當我們想要退回到非SearchActivity,我們需要按返回鍵10次,這顯然太不合理了。
但是如果我們使用singleTop的話,如果SearchActivity在棧頂,當有了新的查詢時,不再重新創建SearchAc實例,而是使用當前的SearchActivity來更新結果。當我們需要返回到非SearchActivity只需要按一次返回鍵即可。使用了singleTop顯然比之前要合理。
singleTask這個模式和前面提到的standard和singleTop截然不同。使用singleTask啟動模式的Activity在系統中只會存在一個實例。如果這個實例已經存在,intent就會通過onNewIntent傳遞到這個Activity。否則新的Activity實例被創建。
如果系統中不存在singleTask Activity的實例,那麼就需要創建這個Activity的實例,並且將這個實例放入和調用者相同的Task中並位於棧頂。
如果singleTask Activity實例已然存在,那麼在Activity回退棧中,所有位於該Activity上面的Activity實例都將被銷毀掉(銷毀過程會調用Activity生命周期回調),這樣使得singleTask Activity實例位於棧頂。與此同時,Intent會通過onNewIntent傳遞到這個SingleTask Activity實例。
然而在Google關於singleTask的文檔有這樣一段描述
The system creates a new task and instantiates the activity at the root of the new task.
意思為 系統會創建一個新的Task,並創建Activity實例放入這個新的Task的底部。
然而實際並非如此,在我的例子中,singleTask Activity並創建並放入了調用者所在的Task,而不是放入新的Task,使用adb shell dumpsys activity
便可以進行驗證。
1 2 3 4 5 6 7 8 9
Task id #239
TaskRecord{428efe30 #239 A=com.thecheesefactory.lab.launchmode U=0 sz=2}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
Hist #1: ActivityRecord{429a88d0 u0 com.thecheesefactory.lab.launchmode/.SingleTaskActivity t239}
Intent { cmp=com.thecheesefactory.lab.launchmode/.SingleTaskActivity }
ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}
Hist #0: ActivityRecord{425fec98 u0 com.thecheesefactory.lab.launchmode/.StandardActivity t239}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}
然而想要實現文檔的描述也並非不可能,我們需要在設置launchMode為singleTask的同時,再加上taskAffinity屬性即可。
1 2 3 4 5 6
<activity
android:name=".SingleTaskActivity"
android:label="singleTask launchMode"
android:launchMode="singleTask"
android:taskAffinity="">
</activity>
完成上面的修改,我們看一下效果,Task的變化如下圖
同時,系統中的任務管理器效果也會相應變化
在跨應用Intent傳遞時,如果系統中不存在singleTask Activity的實例,那麼講創建一個新的Task,然後創建SingleTask Activity的實例,將其放入新的Task中。Task變化如下。
系統的任務管理器也會如下變化
如果singleTask Activity所在的應用進程存在,但是singleTask Activity實例不存在,那麼從別的應用啟動這個Activity,新的Activity實例會被創建,並放入到所屬進程所在的Task中,並位於棧頂位置。
更復雜的一種情況,如果singleTask Activity實例存在,從其他程序被啟動,那麼這個Activity所在的Task會被移到頂部,並且在這個Task中,位於singleTask Activity實例之上的所有Activity將會被正常銷毀掉。如果我們按返回鍵,那麼我們首先會回退到這個Task中的其他Activity,直到當前Task的Activity回退棧為空時,才會返回到調用者的Task。
在上圖中,當Task2中的相冊啟動分享調用Task1中的singleTask Activity,而該Activity實例存在,並位於Task1中回退棧中的第三個位置(從上到下順序),那麼位於該Activity上面的兩個Activity實例將會被銷毀掉,使得該Activity實例位於棧頂。此時Task1中的回退棧只剩兩個Activity,如果點擊返回,那麼會退到的不是相冊應用,而是singleTask Activity棧位置下面的Activity,再次點擊返回方可返回相冊應用。
該模式的使用場景多類似於郵件客戶端的收件箱或者社交應用的時間線Activity。上述兩種場景需要對應的Activity只保持一個實例即可,但是也要謹慎使用這種模式,因為它可以在用戶未感知的情況下銷毀掉其他Activity。
這個模式和singleTask差不多,因為他們在系統中都只有一份實例。唯一不同的就是存放singleInstance Activity實例的Task只能存放一個該模式的Activity實例。如果從singleInstance Activity實例啟動另一個Activity,那麼這個Activity實例會放入其他的Task中。同理,如果singleInstance Activity被別的Activity啟動,它也會放入不同於調用者的Task中。
雖然是兩個task,但是在系統的任務管理器中,卻始終顯示一個,即位於頂部的Task中。
另外當我們從任務管理器進入這個應用,是無法通過返回鍵會退到Task1的。
好在有辦法解決這個問題,就是之前提到的taskAffinity=""
,為launchMode為singleInstance的Activity加入這個屬性即可。
1 2 3 4 5 6
<activity
android:name=".SingleInstanceActivity"
android:label="singleInstance launchMode"
android:launchMode="singleInstance"
android:taskAffinity="">
</activity>
再次運行修改的代碼,查看任務管理器,這樣的結果就合理了。
這種模式的使用情況比較罕見,在Launcher中可能使用。或者你確定你需要使Activity只有一個實例。建議謹慎使用。
除了在manifest文件中設置launchMode之外,還可以在Intnet中設置flag達到同樣的效果。如下述代碼就可以讓StandardActivity已singleTop模式啟動。
1 2 3
Intent intent = new Intent(StandardActivity.this, StandardActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
關於Intent Flags這裡暫不做重點介紹,具體可以參考官方文檔
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11