本文從一例業(yè)務(wù)中遇到的問題出發(fā),以FLAG_ACTIVITY_NEW_TASK這一flag作為切入點,帶大家探究Activity啟動前的一項重要的工作——棧校驗。
文中列舉一系列業(yè)務(wù)中可能遇到的異常狀況,詳細(xì)描述了使用FLAG_ACTIVITY_NEW_TASK時可能遇到的“坑”,并從源碼中探究其根源。只有合理使用flag、launchMode,才能避免因為棧機(jī)制的特殊性,導(dǎo)致一系列與預(yù)期不符的啟動問題。
一、問題及背景
應(yīng)用間相互聯(lián)動、相互跳轉(zhuǎn),是實現(xiàn)系統(tǒng)整體性、體驗一致性的重要手段,也是最簡單的一種方法。
當(dāng)我們用最常用的方法去startActivity時,竟也會遇到失敗的情況。在真實業(yè)務(wù)中,就遇到了這樣一例異常:用戶點擊某個按鈕時,想要“簡簡單單”跳轉(zhuǎn)另一個應(yīng)用,卻沒有任何反應(yīng)。
經(jīng)驗豐富的你,腦海中是否涌現(xiàn)出了各種猜想:是不是目標(biāo)Activity甚至目標(biāo)App不存在?是不是目標(biāo)Activty沒有對外開放?是不是有權(quán)限的限制或者跳轉(zhuǎn)的action/uri錯了……
真實的原因被flag、launchMode、Intent等特性層層藏匿,可能超出你此時的思考。
本文將從源碼出發(fā),探究前因后果,展開講講在startActivity()真正準(zhǔn)備啟動一個Activity前,需要經(jīng)過哪些“磨難”,怎樣有據(jù)可依地解決由棧問題導(dǎo)致的啟動異常。
1.1 業(yè)務(wù)中遇到的問題
業(yè)務(wù)中的場景是這樣的,存在A、B、C三個應(yīng)用。
(1)從應(yīng)用A-Activity1跳轉(zhuǎn)至應(yīng)用B-Activity2;
(2)應(yīng)用B-Activity2繼續(xù)跳轉(zhuǎn)到應(yīng)用C-Activity3;
(3)C內(nèi)某個按鈕,會再次跳轉(zhuǎn)B-Activity2,但點擊后沒有任何反應(yīng)。如果不經(jīng)過前面A到B的跳轉(zhuǎn),C直接跳到B是可以的。
1.2 問題代碼
3個Activity的Androidmanifest配置如下,均可通過各自的action拉起,launchMode均為標(biāo)準(zhǔn)模式。
A-1到B-2的代碼,指定flag為
FLAG_ACTIVITY_NEW_TASK
private void jumpTo_B_Activity2_ByAction_NewTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }
B-2到C-3的代碼,未指定flag
private void jumpTo_C_Activity3_ByAction_NoTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_C_PAGE3"); startActivity(intent); }
C-3到B-2的代碼,與A-1到B-2的完全一致,指定flag為 FLAG_ACTIVITY_NEW_TASK
private void jumpTo_B_Activity2_ByAction_NewTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }
1.3 代碼初步分析
仔細(xì)查看問題代碼,在實現(xiàn)上非常簡單,有兩個特征:
(1)如果直接通過C-3跳B-2,沒有任何問題,但A-1已經(jīng)跳過B-2后,C-3就失敗了。
(2)在A-1和C-3跳到B-2時,都設(shè)置了flag為FLAG_ACTIVITY_NEW_TASK。
依據(jù)經(jīng)驗,我們推測與棧有關(guān),嘗試將跳轉(zhuǎn)前棧的狀態(tài)打印出來,如下圖。
由于A-1跳到B-2時設(shè)置了FLAG_ACTIVITY_NEW_TASK,B-2跳到C-3時未設(shè)置,所以1在獨立棧中,2、3在另一個棧中。示意如下圖。
C-3跳轉(zhuǎn)B-2一般有3種可能的預(yù)期,如下圖:預(yù)想1,新建一個Task,在新Task中啟動一個B-2;預(yù)想2,復(fù)用已經(jīng)存在的B-2;預(yù)想3,在已有Task中新建一個實例B-2。
但實際上3種預(yù)期都沒有實現(xiàn),所有Activity的任何聲明周期都沒有變化,界面始終停留在C-3。
看一下FLAG_ACTIVITY_NEW_TASK的官方注釋和代碼注釋,如下圖:
重點關(guān)注這一段:
When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in.
使用此flag時,如果你正在啟動的Activity已經(jīng)在一個Task中運(yùn)行,那么一個新Activity不會被啟動;相反,當(dāng)前Task將簡單地顯示在界面的前面,并顯示其最后的狀態(tài)。
——顯然,官方文檔與代碼注釋的表述與我們的異?,F(xiàn)象是一致的,目標(biāo)Activity2已經(jīng)在Task中存在,則不會被啟動;Task直接顯示在前面,并展示最后的狀態(tài)。由于目標(biāo)Activty3就是來源Activity3,所以頁面沒有任何變化。
看起來官方還是很靠譜的,但實際效果真的能一直與官方描述一致嗎?我們通過幾個場景來看一下。
二、場景拓展與驗證
2.1 場景拓展
在筆者依據(jù)官方描述進(jìn)行調(diào)整、復(fù)現(xiàn)的過程中,發(fā)現(xiàn)了幾個比較有意思的場景。
PS:上面業(yè)務(wù)的案例中,B-2和C-3在不同應(yīng)用內(nèi),又在相同的Task內(nèi),但實際上是否是同一個應(yīng)用,對結(jié)果的影響并不大。為了避免不同應(yīng)用和不同Task造成閱讀混亂,同一個棧的跳轉(zhuǎn),我們都在本應(yīng)用內(nèi)進(jìn)行,故業(yè)務(wù)中的場景等價于下面的【場景0】
【場景0】把業(yè)務(wù)中B-2到C-3的應(yīng)用間跳轉(zhuǎn)改為B-2到B-3的應(yīng)用內(nèi)跳轉(zhuǎn)
// B-2跳轉(zhuǎn)B-3 public static void jumpTo_B_3_ByAction_Null(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3"); context.startActivity(intent); }
如下圖,A-1設(shè)置NEW_TASK跳轉(zhuǎn)B-2,再跳轉(zhuǎn)B-3,最終設(shè)置NEW_TASK想跳轉(zhuǎn)B-2。雖然跳C-3改為了跳B-3,但與之前問題的表現(xiàn)一致,沒有反應(yīng),停留在B-3。
有的讀者會指出這樣的問題:如果同一個應(yīng)用內(nèi)使用NEW_TASK跳轉(zhuǎn),而不指定目標(biāo)的taskAffinity屬性,實際是無法在新Task中啟動的。請大家忽略該問題,可以認(rèn)為筆者的操作是已經(jīng)加了taskAffinity的,這對最終結(jié)果并沒有影響。
【場景1】如果目標(biāo)Task和來源Task不是同一個,情況是否會如官方文檔所說復(fù)用已有的Task并展示最近狀態(tài)?我們改為B-3啟動一個新Task的新Activity C-4,再通過C-4跳回B-2
// B-3跳轉(zhuǎn)C-4 public static void jumpTo_C_4_ByAction_New(Context context) { Intent intent = new Intent("com.zkp.task.ACTION_TO_C_PAGE4"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } // C-4跳轉(zhuǎn)B-2 public static void jumpTo_B_2_ByAction_New(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); }
如下圖,A-1設(shè)置NEW_TASK跳轉(zhuǎn)B-2,再跳轉(zhuǎn)B-3,再設(shè)置NEW_TASK跳轉(zhuǎn)C-4,最終設(shè)置NEW_TASK想跳轉(zhuǎn)B-2。
預(yù)想的結(jié)果是:不會跳到B-2,而是跳到它所在Task的頂層B-3。
實際的結(jié)果是:與預(yù)期一致,確實是跳到了B-3。
【場景2】把場景1稍做修改:C-4到B-2時,我們不通過action來跳,改為通過setClassName跳轉(zhuǎn)。
// C-4跳轉(zhuǎn)B-2 public static void jumpTo_B_2_ByPath_New(Context context) { Intent intent = new Intent(); intent.setClassName("com.zkp.b", "com.zkp.b.Activity2"); // 直接設(shè)置classname,不通過action intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); }
如下圖,A-1設(shè)置NEW_TASK跳轉(zhuǎn)B-2,再跳轉(zhuǎn)B-3,再設(shè)置NEW_TASK跳轉(zhuǎn)C-4,最終設(shè)置NEW_TASK想跳轉(zhuǎn)B-2。
預(yù)想的結(jié)果是:與場景0一致,會跳到B-2所在Task的已有頂層B-3。
實際的結(jié)果是:在已有的Task2中,產(chǎn)生了一個新的B-2實例。
僅僅是改變了一下重新跳轉(zhuǎn)B-2的方式,效果就完全不一樣了!這與官方文檔中提到該flag與"singleTask" launchMode值產(chǎn)生的行為并不一致!
【場景3】把場景1再做修改:這次C-4不跳棧底的B-2,改為跳轉(zhuǎn)B-3,且還是通過action方式。
// C-4跳轉(zhuǎn)B-3 public static void jumpTo_B_3_ByAction_New(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); }
如下圖,A-1設(shè)置NEW_TASK跳轉(zhuǎn)B-2,再跳轉(zhuǎn)B-3,再設(shè)置NEW_TASK跳轉(zhuǎn)C-4,最終設(shè)置NEW_TASK想跳轉(zhuǎn)B-3。
預(yù)想的結(jié)果是:與場景0一致,會跳到B-2所在Task的頂層B-3。
實際的結(jié)果是:在已有的Task2中,產(chǎn)生了一個新的B-3實例。
不是說好的,Activity已經(jīng)存在時,展示其所在Task的最新狀態(tài)嗎?明明Task2中已經(jīng)有了B-3,并沒有直接展示它,而是生成了新的B-3實例。
【場景4】既然Activity沒有被復(fù)用,那Task一定會被復(fù)用嗎?把場景3稍做修改,直接給B-3指定一個單獨的affinity。
如下圖,A-1設(shè)置NEW_TASK跳轉(zhuǎn)B-2,再跳轉(zhuǎn)B-3,再設(shè)置NEW_TASK跳轉(zhuǎn)C-4,最終設(shè)置NEW_TASK想跳轉(zhuǎn)B-3。
——這次,連Task也不會再被復(fù)用了……Activity3在一個新的棧中被實例化了。
再回看官方的注釋,就會顯得非常不準(zhǔn)確,甚至?xí)?a target="_blank">開發(fā)者對該部分的認(rèn)知產(chǎn)生嚴(yán)重錯誤!稍微改變過程中的某個毫無關(guān)聯(lián)的屬性(如跳轉(zhuǎn)目標(biāo)、跳轉(zhuǎn)方式……),就會產(chǎn)生很大差異。
在看flag相關(guān)注釋時,我們要樹立一個意識:Task和Activity跳轉(zhuǎn)的實際效果,是launchMode、taskAffinity、跳轉(zhuǎn)方式、Activity在Task中的層級等屬性綜合作用的結(jié)果,不要相信“一面之詞”。
回到問題本身,究竟是哪些原因造就了上面的不同效果呢?只有源碼最值得信賴了。
三、場景分析與源碼探索
本文以Android 12.0源碼為基礎(chǔ),進(jìn)行探究。上述場景在不同Android版本上的表現(xiàn)是一致的。
3.1 源碼調(diào)試注意事項
源碼的調(diào)試方法,許多文章已經(jīng)有了詳細(xì)的教學(xué),本文不再贅述。此處只簡單總結(jié)其中需要注意的事項:
下載模擬器時,不要使用Google Play版本,該版本類似user版本,無法選擇system_process進(jìn)程進(jìn)行斷點。
即使是Google官方模擬器和源碼,在斷點時,也會有行數(shù)嚴(yán)重不對應(yīng)的情況(比如:模擬器實際會運(yùn)行到方法A,但在源碼中打斷點時,實際不能定位到方法A的對應(yīng)行數(shù)),該問題并沒有很好的處理方法,只能盡量規(guī)避,如使模擬器版本與源碼版本保持一致、多打一些斷點增加關(guān)鍵行數(shù)被定位到的幾率。
3.2 初步斷點,明確啟動結(jié)果
以【場景0】為例,我們初步確認(rèn)一下,為什么B-3跳轉(zhuǎn)B-2會無反應(yīng),系統(tǒng)是否告知了原因。
3.2.1 明確啟動結(jié)果及其來源
在Android源碼的斷點調(diào)試中,常見的有兩類進(jìn)程:應(yīng)用進(jìn)程和system_process進(jìn)程。
在應(yīng)用進(jìn)程中,我們能獲取到應(yīng)用啟動結(jié)果的狀態(tài)碼result,這個result用來告訴我們啟動是否成功。涉及堆棧如下圖(標(biāo)記1)所示:
Activity類::startActivity()
→ startActivityForResult()
→Instrumentation類::execStartActivity(),
返回值result則是ATMS
(ActivityTaskManagerService)執(zhí)行的結(jié)果。
如上圖(標(biāo)記2)標(biāo)注,ATMS類::startActivity()方法,返回了result=3。
在system_process進(jìn)程中,我們看一下這個result=3是怎樣被賦值的。略去詳細(xì)斷點步驟,實際堆棧如下圖(標(biāo)注1)所示:
ATMS類::startActivity() →startActivityAsUser()
→ActivityStarter類::execute()
→executeRequest()
→startActivityUnchecked()
→ startActivityInner()
→ recycleTask(),在recycleTask()中返回了結(jié)果。
如上圖(標(biāo)注2)所示,result在mMovedToFront=false時被賦值,即result=START_DELIVERED_TO_TOP=3,而START_SUCCESS=0才代表創(chuàng)建成功。
看一下源碼中對START_DELIVERED_TO_TOP的說明,如下圖:
Result for IActivityManaqer.startActivity: activity wasn't really started, but the given Intent was given to the existing top activity.
(IActivityManaqer.startActivityActivity的結(jié)果:Activity并未真正啟動,但給定的Intent已提供給現(xiàn)有的頂層Activity。)
“Activity并未真正啟動”——是的,因為可以復(fù)用
“給定的Intent已提供給現(xiàn)有的頂層Activity”——實際沒有,頂層Activity3并沒有收到任何回調(diào),onNewIntent()未執(zhí)行,甚至嘗試通過Intent::putExtra()傳入新的參數(shù),Activity3也沒有收到。官方文檔又帶給了我們一個疑問點?我們把這個問題記錄下來,在后面分析。
滿足什么條件,才會造成
START_DELIVERED_TO_TOP的結(jié)果呢?筆者的思路是,通過與正常啟動流程對比,找出差異點。
3.3 過程斷點,探索啟動流程
一般來說,在定位問題時,我們習(xí)慣通過結(jié)果反推原因,但反推的過程只能關(guān)注到與問題強(qiáng)關(guān)聯(lián)的代碼分支,并不能能使我們很好地了解全貌。
所以,本節(jié)內(nèi)容我們通過順序閱讀的方法,正向介紹startActivity過程中與上述【場景01234】強(qiáng)相關(guān)的邏輯。再次簡述一下:
【場景0】同一個Task內(nèi),從頂部B-3跳轉(zhuǎn)B-2——停留在B-3
【場景1】從另一個Task內(nèi)的C-4,跳轉(zhuǎn)B-2——跳轉(zhuǎn)到B-3
【場景2】把場景1中,C-4跳轉(zhuǎn)B-2的方式改為setClassName()——創(chuàng)建新B-2實例
【場景3】把場景1中,C-4跳轉(zhuǎn)B-2改為跳轉(zhuǎn)B-3——創(chuàng)建新B-3實例
【場景4】給場景3中的B-3,指定taskAffinity——創(chuàng)建新Task和新B-3實例
3.3.1 流程源碼概覽
源碼中,整個啟動流程很長,涉及的方法和邏輯也很多,為了便于大家理清方法調(diào)用順序,方便后續(xù)內(nèi)容的閱讀,筆者將本文涉及到的關(guān)鍵類及方法調(diào)用關(guān)系整理如下。
后續(xù)閱讀中如果不清楚調(diào)用關(guān)系,可以返回這里查看:
// ActivityStarter.java ActivityStarter::execute() { executeRequest(intent) { startActivityUnchecked() { startActivityInner(); } } ActivityStarter::startActivityInner() { setInitialState(); computeLaunchingTaskFlags(); Task targetTask = getReusableTask(){ findTask(); } ActivityRecord targetTaskTop = targetTask.getTopNonFinishingActivity(); if (targetTaskTop != null) { startResult = recycleTask() { setTargetRootTaskIfNeeded(); complyActivityFlags(); if (mAddingToTask) { return START_SUCCESS; //【場景2】【場景3】從recycleTask()返回 } resumeFocusedTasksTopActivities() return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;//【場景1】【場景0】從recycleTask()返回 } } else { mAddingToTask = true; } if (startResult != START_SUCCESS) { return startResult;//【場景1】【場景0】從startActivityInner()返回 } deliverToCurrentTopIfNeeded(); resumeFocusedTasksTopActivities(); return startResult; }
3.3.2 關(guān)鍵流程分析
(1)初始化
startActivityInner()是最主要的方法,如下列幾張圖所示,該方法會率先調(diào)用setInitialState(),初始化各類全局變量,并調(diào)用reset(),重置ActivityStarter中各種狀態(tài)。
在此過程中,我們記下兩個關(guān)鍵變量mMovedToFront和mAddingToTask,它們均在此被重置為false。
其中,mMovedToFront代表當(dāng)Task可復(fù)用時,是否需要將目標(biāo)Task移動到前臺;mAddingToTask代表是否要將Activity加入到Task中。
(2)計算確認(rèn)啟動時的flag
該步驟會通過computeLaunchingTaskFlags()方法,根據(jù)launchMode、來源Activity的屬性等進(jìn)行初步計算,確認(rèn)LaunchFlags。
此處重點處理來源Activity為空的各類場景,與我們上文中的幾種場景無關(guān),故不再展開講解。
(3)獲取可以復(fù)用的Task
該步驟通過調(diào)用getReusableTask()實現(xiàn),用來查找有沒有可以復(fù)用的Task。
先說結(jié)論:場景0123中,都能獲取到可以復(fù)用的Task,而場景4中,未獲取到可復(fù)用的Task。
為什么場景4不可以復(fù)用?我們看一下getReusableTask()的關(guān)鍵實現(xiàn)。
上圖(標(biāo)注1)中,putIntoExistingTask代表是否能放入已經(jīng)存在的Task。當(dāng)flag含有NEW_TASK且不含MULTIPLE_TASK時,或指定了singleInstance或singleTask的launchMode等條件,且沒有指定Task或要求返回結(jié)果 時,場景01234均滿足了條件。
然后,上圖(標(biāo)注2)通過findTask()查找可以復(fù)用的Task,并將過程中找到的棧頂Activity賦值給intentActivity。最終,上圖(標(biāo)注3)將intentActivity對應(yīng)的Task作為結(jié)果。
findTask()是怎樣查找哪個Task可以復(fù)用呢?
主要是確認(rèn)兩種結(jié)果mIdealRecord——“理想的ActivityRecord” 和 mCandidateRecord——"候選的ActivityRecord",作為intentActivity,并取intentActivity對應(yīng)的Task作為復(fù)用Task。
什么ActivityRecord才是理想或候選的ActivityRecord呢?
在mTmpFindTaskResult.process()中確認(rèn)。
程序會將當(dāng)前系統(tǒng)中所有的Task進(jìn)行遍歷,在每個Task中,進(jìn)行如上圖所示的工作——將Task的底部Activity realActivity與目標(biāo)Activity cls進(jìn)行對比。
場景012中,我們想跳轉(zhuǎn)Activity2,即cls是Activity2,與Task底部的realActivity2相同,則將該Task頂部的Activity3 r作為“理想的Activity”;
場景3中,我們想跳轉(zhuǎn)Activity3,即cls是Activity3,與Task底部的realActivity2不同,則進(jìn)一步判斷task底部Activity2與目標(biāo)Activity3的棧親和行,具有相同親和性,則將Task的頂部Activity3作為“候選Activity”;
場景4中,所有條件都不滿足,最終沒能找到可復(fù)用的Task。在執(zhí)行完getReusableTask()后將mAddingToTask賦值為true
由此,我們就能解釋【場景4】中,新建了Task的現(xiàn)象。
(4)確定是否需要將目標(biāo)Task移動到前臺
如果存在可復(fù)用的Task,場景0123會執(zhí)行recycleTask(),該方法中會相繼進(jìn)行幾個操作:setTargetRootTaskIfNeeded()、
complyActivityFlags()。
首先,程序會執(zhí)行
setTargetRootTaskIfNeeded(),用來確定是否需要將目標(biāo)Task移動到前臺,使用mMovedToFront作為標(biāo)識。
在【場景123】中,來源Task和目標(biāo)Task是不同的,differentTopTask為true,再經(jīng)過一系列Task屬性對比,能夠得出mMovedToFront為true;
而場景0中,來源Task和目標(biāo)Task相同,differentTopTask為false,mMovedToFront保持初始的false。
由此,我們就能解釋【場景0】中,Task不會發(fā)生切換的現(xiàn)象。
(5)通過對比flag、Intent、Component等確認(rèn)是否要將Activity加入到Task中
還是在【場景0123】中,recycleTask()會繼續(xù)執(zhí)行complyActivityFlags(),用來確認(rèn)是否要將Activity加入到Task中,使用mAddingToTask作為標(biāo)識。
該方法會對FLAG_ACTIVITY_NEW_TASK、
FLAG_ACTIVITY_CLEAR_TASK、
FLAG_ACTIVITY_CLEAR_TOP等諸多flag、Intent信息進(jìn)行一系列判斷。
上圖(標(biāo)注1)中,會先判斷后續(xù)是否需要重置Task,resetTask,判斷條件則是FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,顯然,場景0123的resetTask都為false。繼續(xù)執(zhí)行。
接著,會有多種條件判斷按順序執(zhí)行。
在【場景3】中,目標(biāo)Component(mActivityComponent)是B-3,目標(biāo)Task的realActivity則是B-2,兩者不相同,進(jìn)入了resetTask相關(guān)的判斷(標(biāo)注2)。
之前resetTask已經(jīng)是false,故【場景3】的mAddingToTask脫離原始值,被置為true。
在【場景012】中,相對比的兩個Activity都是B-2(標(biāo)注3),可以進(jìn)入下一級判斷——isSameIntentFilter()。
這一步判斷的內(nèi)容就很明顯了,目標(biāo)Activity2的已有Intent 與 新的Intent做對比。很顯然,場景2中由于改為了setClassName跳轉(zhuǎn),Intent自然不一樣了。
故【場景2】的mAddingToTask脫離原始值,被置為true。
總結(jié)看一下:
【場景123】的mMovedToFront最先被置為true,而【場景0】經(jīng)歷重重考驗,保持初始值為false。
——這意味著當(dāng)有可復(fù)用Task時,【場景0】不需要把Task切換到前列;【場景123】需要切換到目標(biāo)Task。
【場景234】的mAddingToTask分別在不同階段被置為true,而【場景01】,始終保持初始值false。
——這意味著,【場景234】需要將Activity加入到Task中,而【場景01】不再需要。
(6)實際啟動Activity或直接返回結(jié)果
被啟動的各個Activity會通過resumeFocusedTasksTopActivities()等一系列操作,開始真正的啟動與生命周期的調(diào)用。
我們關(guān)于上述各個場景的探索已經(jīng)得到答案,后續(xù)流程便不再關(guān)注。
四、問題修復(fù)及遺留問題解答
4.1 問題修復(fù)
既然前面總結(jié)了這么多必要條件,我們只需要破壞其中的某些條件,就可以修復(fù)業(yè)務(wù)中遇到的問題了,簡單列舉幾個的方案。
方案一:修改flag。B-3跳轉(zhuǎn)B-2時,增加FLAG_ACTIVITY_CLEAR_TASK或FLAG_ACTIVITY_CLEAR_TOP,或者直接不設(shè)置flag。經(jīng)驗證可行。
方案二:修改intent屬性,即【場景2】。A-1通過action方式隱式跳轉(zhuǎn)B-2,則B-3可以通過setClassName方式,或修改action內(nèi)屬性的方式跳轉(zhuǎn)B-2。經(jīng)驗證可行。
方案三:提前移除B-2。B-2跳轉(zhuǎn)B-3時,finish掉B-2。需要注意的是,finish()要在startActivity()之前執(zhí)行,以避免遺留的ActivityRecord和Intent信息對后續(xù)跳轉(zhuǎn)的影響。尤其是當(dāng)你把B-2作為自己應(yīng)用的deeplink分發(fā)Activity時,更值得警惕。
4.2 遺留問題
還記得我們在文章開端的某個疑惑嗎,為什么沒有回調(diào)onNewIntent()?
onNewIntent() 會通過deliverNewIntent()觸發(fā),而deliverNewIntent()僅通過以下兩個方法調(diào)用。
complyActivityFlags()就是上文3.3.1.5中我們著重探討的方法,可以發(fā)現(xiàn)complyActivityFlags()中所有可能調(diào)用deliverNewIntent()的條件均被完美避開了。
而deliverToCurrentTopIfNeeded()方法則如下圖所示。
mLaunchFlags和mLaunchMode,無法滿足條件,導(dǎo)致dontStart為false,無緣
deliverNewIntent()。
至此,onNewIntent()的問題得到解答。
五、結(jié)語
通過一系列場景假設(shè),我們發(fā)現(xiàn)了許多出乎意料的現(xiàn)象:
文檔提到FLAG_ACTIVITY_NEW_TASK等價于singleTask,與事實并不完全如此,只有與其他flag搭配才能達(dá)到相似的效果。這一flag的注釋非常片面,甚至?xí)l(fā)誤解,單一因素?zé)o法決定整體表現(xiàn)。
官方文檔提到
START_DELIVERED_TO_TOP會將新的Intent傳遞給頂層Activity,但事實上,并不是每一種START_DELIVERED_TO_TOP都會把新的Intent重新分發(fā)。
同一個棧底Activity,前后兩次都通過action或都通過setClassName跳轉(zhuǎn)到時,第二次跳轉(zhuǎn)竟然會失敗,而兩次用不同方式跳轉(zhuǎn)時,則會成功。
單純使用FLAG_ACTIVITY_NEW_TASK時,跳棧底Activity和跳同棧內(nèi)其他Activity的效果大相徑庭。
業(yè)務(wù)中遇到的問題,歸根結(jié)底就是對Android棧機(jī)制不夠了解造成的。
在面對棧相關(guān)的編碼時,開發(fā)者務(wù)必要想清楚,承擔(dān)新開應(yīng)用棧的Activty在應(yīng)用全局承擔(dān)怎樣的使命,要對Task歷史、flag屬性、launchMode屬性、Intent內(nèi)容等全面評估,謹(jǐn)慎參考官方文檔,才能避免棧陷阱,達(dá)成理想可靠的效果。
審核編輯:劉清
-
Android
+關(guān)注
關(guān)注
12文章
3917瀏覽量
127061 -
Flag
+關(guān)注
關(guān)注
0文章
12瀏覽量
8123 -
模擬器
+關(guān)注
關(guān)注
2文章
863瀏覽量
43133 -
ATMS
+關(guān)注
關(guān)注
0文章
4瀏覽量
8513
原文標(biāo)題:明修"棧"道——越過Android啟動棧陷阱
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論