activity 启动模式_Intent#FLAG_ACTIVITY_CLEAR_TOP 真的会 clear top 吗
一、背景
前段時間處理了一個 App 內草稿丟失的反饋,很多用戶反饋連續存了多個草稿之后,草稿箱都只有一個草稿,顯然是發生了草稿丟失。從用戶反饋的數據來看,反饋用戶的系統版本都在 Android 7.0 以下。
經過一段時間的排查,最后發現是草稿被覆蓋了,直接原因是:存草稿后拍攝鏈路頁面(錄制頁、編輯頁、發布頁)未被關閉,再次進拍攝之后錄制頁復用(狀態也復用了)導致存草稿的時候發生了草稿替換。
那么問題來了,為什么會發生這樣的情況呢?這得從我們存草稿那一刻說起。
二、發現問題:為什么 clear top 不生效?
正常來說,存草稿之后我們會關閉拍攝鏈路的頁面,把拍攝鏈路上的一串 Activity 都關閉掉。從代碼中可以看到,我們是使用 Intent#FLAG_ACTIVITY_CLEAR_TOP | Intent#FLAG_ACTIVITY_NEW_TASK 來實現這一目的的:
邏輯簡單明了,就是加個 clear_top 的 flag,然后 setClass 指定跳轉到首頁,這里的 getPublishContainerActivityClass 返回的是 MainActivity,看起來沒什么異常。
試著打了一個包在小米10 Pro 上試一下,存草稿,沒有復現問題,回到首頁之后按返回,直接退出 App 了,說明拍攝鏈路的頁面被關閉了。萬幸,在換了好幾臺手機之后,終于在一臺 Android 5.1 上復現了這個 case,發現存完草稿回到首頁如果再按返回鍵,就回到了發布頁,拍攝鏈路還在。
所以核心問題就是:為什么在這臺 Android 5.1 上 clear_top 這個 flag 沒生效?
三、提出問題:FLAG_ACTIVITY_CLEAR_TOP 真的會 clear top 嗎?
關于 Intent#FLAG_ACTIVITY_CLEAR_TOP,簡單地概括一下就是:設置這個 flag 后,如果發現目標 Activity 已經存在,會將目標 Activity 所在的 Task 移到前臺,然后 finish 掉目標 Activity 上層的所有 Activity,最后打開目標 Activity。至于如何判斷目標 Activity 已經存在了,注釋中并沒有提到。
回到 Aweme 工程。一般情況下,首頁、錄制頁、編輯頁、發布頁是在同一個任務棧里,從 adb 打印的 activity 堆棧信息也可以看出這一點。
adb shell dumpsys activity activities | grep 'com.zhiliaoapp.musically'
看了下這幾個 Activity 的啟動模式,并沒發現什么不一樣的地方: VideoRecordNewActivity: singleTask VideoPublishEditActivity: standard(default) VideoPublishActivity: singleTask
大膽猜想一下,難道是不同版本的 ROM 對 Intent#FLAG_ACTIVITY_CLEAR_TOP 的處理有差異導致這個 flag 沒生效?
四、嘗試在 Demo 上復現問題
建了個 Demo 工程,創建了 A, B, C 三個空頁面,分別對應首頁、拍攝頁、發布頁,跳轉路徑是:A --> B --> C --> A。其中 A、B、C 的啟動模式分別為 singleTop, singleTask, singleTask,與 App 內的拍攝鏈路一致。
在線上問題復現的這臺 Android 5.1 上測試發現,C 用 clear_top 回到 A 之后,整個棧就清空了,在 A 點返回直接退出 App 了,沒有發現任何異常。
陷入沉思,Demo 上沒復現,難道是 Aweme 工程里對 Activity 或者 Intent 這塊做了騷操作?(盲猜一手)。打印一下復現問題的這臺 Android 5.1 的 App 存草稿之后的任務棧,發現了一些不一樣的東西:
從上圖我們可以看到,任務棧里最底層的竟然是 SplashActivity,那我們的首頁呢?MainActivity 哪去了,我們明明是從首頁進拍攝然后到發布頁的......隨便在 MainActivity 中打個斷點可以發現斷點能生效,說明展示的確實是 MainActivity,但是任務棧中指向的是 SplashActivity,這是什么操作?是的,這就是 activity-alias,Android 1.0 開始就支持的一個機制。
在 AndroidManifest 中查看 SplashActivity 和 MainActivity 的聲明,可以發現 SplashActivity 被聲明成了 activity-alias,其 targetActivity 指向的是 MainActivity。
關于 activity-alias 的細節暫且不深入,先改下 Demo,加個 Splash,聲明為 activity-alias。現在 Demo 的啟動流程變成了:Splash --> A --> B --> C --> A。
修改好之后跑起來試試,結果,在 Android 5.1 的測試機上竟然復現了:C 用 clear top 打開 A 之后并沒有將 B 和 C 關閉掉!那么有沒有可能是這個 ROM 的問題?在模擬器上運行試試,發現也復現了,難道是 Android 的 Bug?不會吧,不會吧,不會吧......
五、解決問題
Talk is cheap. Show me the code.
既然在模擬器中運行 Demo App 也復現了,可以嘗試從 AOSP 源碼中追溯這個問題。
5.1 先回顧一下啟動流程中的幾個概念
5.1.1 ActivityRecord
An entry in the history stack, representing an activity.
Activity 以 ActivityRecord 對象的形式存放在任務棧中。在 Activity 的啟動過程中會創建 ActivityRecord,代表待啟動的 Activity。
- who started this entry, so will get our reply
5.1.2 ActivityInfo
Information you can retrieve about a particular application activity or receiver. This corresponds to information collected from the AndroidManifest.xml's and tags.
存放的是我們在 AndroidManifest 中聲明 Activity 時指定的一堆 Activity 配置。
5.1.3 TaskRecord
代表一個任務棧,棧中可能有一堆同棧的 Activity。
- The original intent that started the task
- List of all activities in the task arranged in history order
- The ActivityStack it belongs to.
5.1.4 ActivityStack
State and management of a single stack of activities.
任務棧由 ActivityStack 持有。
- The back history of all previous (and possibly still running) activities. It contains #TaskRecord objects.
- 0 ~ size - 1, the last is the top task.
ActivityRecord, TaskRecord, ActivityStack 的關系可以簡單用下面這個圖來表示:
5.1.5 activity-alias
Activity 別名,這是 AndroidManifest 中支持的一個標簽,使用方式和 activity 標簽類似,用于表示某個 Activity 是另一個 Activity 的別名 (targetActivity)。通過 Intent 啟動 activity-alias 類型的 Activity 時,最終只會啟動 targetActivity,不會走 activity-alias 這個 Activity 本身的任何生命周期。
5.2 從源碼看 Activity 啟動時 clear top 的處理邏輯
5.2.1 Android 6.0
通過查閱 Android 6.0 的源碼,我們可以整理出如下的 Activity 啟動鏈路調用時序圖。
注意:
TaskRecord
從上面的時序圖可以看到,啟動 Activity 時會先幫它找到一個任務棧,找到這個任務棧之后,會根據 Intent 的 flag 對這個任務棧進行處理。如果設置了 Intent#FLAG_ACTIVITY_CLEAR_TASK,則會清空這個任務棧中已有的 Activity。
如果設置了 Intent#FLAG_ACTIVITY_CLEAR_TOP,則會執行 clear top 操作,將任務棧中目標 Activity 之上的其他 Activity 給 finish 掉。具體的處理邏輯如下:
TaskRecord#performClearTaskLocked
在執行 performClearTaskLocked 的過程中,會對任務棧中的 Activity 進行遍歷,如果判斷某個 Activity 的 realActivity 屬性和待啟動 Activity 的 realActivity 是同一個,就會執行 clear top 操作,將任務棧中這個 Activity 之上的 Activity 都干掉。
ActivityRecord
那么 ActivityRecord#realActivity 是在哪里設置的呢?繼續看 ActivityRecord 的源碼,可以發現 realActivity 是 ActivityRecord 中的一個 final 成員變量,事情貌似變得簡單了。
在 Activity 的啟動過程中,在執行到 ActivityStackSupervisor#startActivityLocked 的時候,會創建一個 ActivityRecord 對象,這是待啟動 Activity 的 ActivityRecord 對象。在初始化這個 ActivityRecord 的時候,會對其 realActivity 進行賦值:
ActivityRecord.java
在對 realActivity 賦值的時候,滿足三個條件之一就會將傳入 Intent 的 component 設置給 realActivity,這三個條件是:
- AndroidManifest 中指定 activity-alias 時才會指定 targetActivity
到此為止,我們已經知道了 Android 6.0 是如何處理 clear top 的。此處的 realActivity 的賦值邏輯很關鍵,下面會重新提到。
5.2.2 Android 7.0
Android 7.0 和 Android 6.0 的調用鏈路有一定變化,主要是把原先 ActivityStackSupervisor 中的啟動邏輯拆到了一個新的 ActivityStarter 類中。
依舊是在 TaskRecord#performClearTaskLocked 里處理 clear top。
TaskRecord
這里執行 clear top 的邏輯沒有變化,依舊是對 realActivity 的判斷。
TaskRecord#performClearTaskLocked
ActivityRecord
也可以看到,realActivity 依舊是 final 變量。
ActivityRecord.java
但是!Android 7.0 對 ActivityRecord#realActivity 的賦值邏輯做了調整,新加了個判斷:aInfo.targetActivity.equals(_intent.getComponent().getClassName()
Android 7.0 新加的這個判斷導致的差異可以用如下這個例子來說明:
需要說明的是聲明為 activity-alias 的 Activity 不支持指定 launchMode,所以它的 launchMode 是默認值 standard,也很容易理解,畢竟只是個占位符。
為什么 clear top 會生效呢,因為在啟動 targetActivity 時創建的 ActivityRecord 的 realActivity 也是指向的 targetActivity,所以當執行到 TaskRecord#performClearTaskLocked 的時候,就會發現和啟動 activity-alias 時創建的 ActivityRecord 的 realActivity 相等,因為都是指向的 targetActivity,從而 clear top 正常生效。
在 Android 7.0 之前,執行上述同樣的兩步操作,clear top 不會生效,任務棧不會被清理。因為直接啟動 activity-alias 時所創建 ActivityRecord 對象的 realActivity 指向 activity-alias 本身,而再次直接啟動 targetActivity 時創建的 ActivityRecord#realActivity 指向的是 targetActivity。
5.2.3 Android 10.0
Android 10.0 的啟動流程有很多變化,但是 clear_top 執行的核心邏輯與 Android 7.0 沒有區別。
TaskRecord
TaskRecord#performClearTaskLocked
ActivityRecord
那么 ActivityRecord#mActivityComponent 是在哪里設置的呢?
ActivityRecord.java
5.2.4 總結一下
用 Intent 啟動某個 launchMode 為 singleTop 的 Activity 時,如果在 Intent 中設置了 clear_top 的 flag,Android 在處理 clear_top 的時候,會遍歷整個任務棧,通過判斷 ActivityRecord#realActivity 在任務棧中尋找已經存在的 Activity 實例。如果找到了目標 Activity,就會將目標 Activity 之上的 Activity 全部 finish 掉。
ActivityRecord#realActivity 是在 ActivityRecord 的構造方法中初始化的,其初始化邏輯在 Android 6.0 以下(包含 6.0)和 Android 7.0+ 有差異。
Android 5.0/6.0(含以下)在初始化 ActivityRecord 的時候,未對 activity-alias 做判斷,realActivity 指向的是 activity-alias 這個 Activity 自身。Android 7.0+ 以后對 activity-alias 進行了判斷,realActivity 指向的是 activity-alias 的 targetActivity。
上述這個差異也就導致了:如果待啟動 Activity 是某個 activity-alias 的 targetActivity,Android 6.0 和 Android 7.0+ 在處理 Intent 中 clear_top flag 時可能會有不同表現,前者 clear top 不生效,后者 clear top 生效。
5.3 真相大白
回到我們最初的問題:為什么我們的 App 在 Android 7.0 以下的版本上,發布頁加 clear top flag 跳轉到首頁 MainActivity 沒能清空任務棧?
再來看下我們是怎么跳首頁的。從下圖可以看到跳首頁是指定的 class 是 AVEnv.APPLICATION_SERVICE.getPublishContainerActivityClass(),看了下具體的實現發現這個類就是 MainActivity,所以確實是直接跳到首頁的。
在前面我們有提到,我們 App 的 Launch Activity 是 SplashActivity,但是它只是個 activity-alias,其 targetActivity 指向 MainActivity,MainActivity 的 launchMode 是 singleTop。經過上面對 Android 6.0, Android 7.0, Android 10.0 啟動流程的分析,我想答案已經比較明顯了。
在 Android 5.1 上啟動 App 時,啟動 Launch Activity 實際啟動的是 MainActivity,但是插入到任務棧中的 ActivityRecord 的 realActivity 指向的是 SplashActivity 。當走完發布流程,在發布頁點擊存草稿時,再次啟動 MainActivity,創建的這個 ActivityRecord 的 realActivity 指向 MainActivity,在任務棧中找不到 realActivity 等于 MainActivity 的,所以 clear top 不會生效。
在 Android 7.0+ 上的版本 clear top 會生效,原因不再贅述。
如果要避免上述這種差異導致的 clear_top flag 不生效的問題,將 clear_top 的 Intent 指向 activity-alias 這個 Activity 即可。所以修復(準確地說是適配)這個問題的話可以把存草稿后打開首頁的 intent 的 class 設置成 SplashActivity,這么改動之后在 Android 5.1 的測試機上運行發現問題確實修復了。
當然,既然 AOSP 是從 Android 7.0+ 做出的邏輯改動,也就解釋了為什么反饋用戶都是 Android 7.0 以下的系統。
5.4 在 Demo 上驗證
在 Android 5.1 上啟動 Demo App,然后用 adb 打印一下任務棧看看。打印出來發現任務棧中 ActivityRecord#realActivity 指向 Splash
adb shell dumpsys activity activities | grep 'com.yongf.android.myapplication'
同樣的包在 Android 7.0 上打印的信息如下,可以看到任務棧中 ActivityRecord#realActivity 已經不再指向 Splash,而是指向 A 了。
adb shell dumpsys activity activities | grep 'com.yongf.android.myapplication'
將 Demo 上的啟動鏈路改成:Splash --> A --> B --> C --> Splash,可以發現在 Android 5.1 的測試機和模擬器上 B 和 C 都被干掉了,clear top 生效了。
行文到最后,再提一個有意思的點,估計是 6.0 上才發現這個問題,所以在 Android 7.0 上修復了這個問題,因為在 ActivityRecord 構造方法里 realActivity 的賦值邏輯上源碼里 7.0 新增了這樣一段注釋:
最后,回到本文的標題,Intent#FLAG_ACTIVITY_CLEAR_TOP 真的會 clear top 嗎,你知道了嗎?^_^
六、Demo 工程
待上傳。
七、推薦幾個工具
- Carbon is the easiest way to create and share beautiful images of your source code.
- Android Code Search
八、關于作者
微信公眾號:
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的activity 启动模式_Intent#FLAG_ACTIVITY_CLEAR_TOP 真的会 clear top 吗的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python list_Python中的
- 下一篇: linux 修改时区_【003】一文全面