【android activity重難點突破】這些知識還不會,面試八成被勸退

語言: CN / TW / HK

highlight: a11y-dark

Activity作為android四大元件之一,地位就不用多說了吧,該元件看起來是比較簡單的,但是也涉及到很多知識點,要想全部理解並在合適的業務場景下使用,也是需要一定的技術沉澱,本文主要是對activity一些重要知識點進行總結整理,可能平時不一定用到,但是一定要有所瞭解。

當然這些知識點並沒有設計過多原始碼部分,比如activity的啟動流程什麼的,主要是零散的知識點,對於activity的啟動流程網上文章太多了,後面自己也準備重新梳理下,好記性不如爛筆頭,在不斷學習整理的過程中,一定會因為某個知識點而豁然開朗。

image.png

1.生命週期

①.兩個頁面跳轉

MainActivity跳轉到SecordActivity的生命週期,重點關注MainonPauseonStopSecord幾個關鍵生命週期的順序,以及從Secord返回時與Main的生命週期的交叉:

image.png

可以發現Main頁面的onPause生命週期之後直接執行SecordonCreate,onStart,onResume,所以onPause生命週期內不要執行耗時操作,以免影響新頁面的展示,造成卡頓感。

②.彈出Dialog

  • 單純的彈出Dialog是不會影響Activity的生命週期的;
  • 啟動dialog themeActivity的時候,啟動的activity只會執行onPause方法,onStop不會執行,被啟動的activity會正常走生命週期,back的時候,啟動的Activity會對應執行onResume方法;

image.png

③.橫豎屏切換

  • AndroidManifest不配置configChanges時,橫豎屏切換,會銷燬重建Activity,生命週期會重新走一遍;
  • 當ActivityconfigChanges="orientation|screenSize"時,橫豎屏切換不會重新走Activity生命週期方法,只會執行onConfigurationChanged方法,如需要可以在此方法中進行相應業務處理;

如橫豎屏切換時需要對佈局進行適配,可在res下新建layout-portlayout-land目錄,並提供相同的xml佈局檔案,橫豎屏切換時即可自動載入相應佈局。(前提是未配置configChanges忽略橫豎屏影響,否則不會重新載入佈局)

④.啟動模式對生命週期的影響

1.A(singleTask)啟動(startActivity)B(standard),再從B啟動A,生命週期如下:

A啟動B:A_onPause、B_onCreate、B_onStart、B_onResume、A_onStop

第二步:B_onPause、A_onNewIntent、A_onRestart、A_onStart、A_onResume、B_onStop、B_onDestory

2.A(singleTask)啟動A,或者A(singleTop)啟動A

A_onPause、A_onNewIntent、A_Resume

3.singleInstance模式的activity

多次啟動A(singleInstance),只有第一次會建立一個單獨的任務棧(全域性唯一),再次啟動會呼叫A_onPause、A_onNewIntent、A_Resume

2.啟動模式

Activity的啟動模式一直是standardsingleTopsingleTasksingleInstance四種,Android 12新增了singleInstancePerTask啟動模式,在這裡不一一介紹,僅介紹重要知識點。

①.singleTask

1.Activity是一個可以跨程序、跨應用的元件,當你在 A App裡開啟 B AppActivity的時候,這個Activity會直接被放進A的Task裡,而對於B的Task,是沒有任何影響的。

從A應用啟動B應用,預設情況下啟動的B應用的Activity會進入A應用當前頁面所在的任務棧中,此時按home建,再次啟動B應用,會發現B應用並不會出現A啟動的頁面(前提是A應用啟動的不是B應用主activity,如果是必然一樣),而是如第一次啟動一般.

如果想要啟動B應用的時候出現被A應用啟動的頁面,需要設定B應用被啟動頁的launchmodesingleTask,此時從A應用的ActivityA頁面啟動B應用的頁面ActivityBlaunchmodesingleTask),發現動畫切換方式是應用間切換,此時ActivityBActivityA分別處於各自的任務棧中,並沒有在一個task中,此時按Home鍵後,再次點選啟動B應用,發現B應用停留在ActivityB頁面。

如果想要實現上述效果,除了設定launchmode之外,還可以通過設定allowTaskReparenting屬性達到同樣的效果,Activity 預設情況下只會歸屬於一個 Task,不會在多個Task之間跳來跳去,但你可以通過設定來改變這個邏輯,如果你不設定singleTask,而是設定allowTaskReparentingtrue,此時從A應用的ActivityA頁面啟動B應用的頁面ActivityB(設定了allowTaskReparentingtrue),ActivityB會進入ActivityA的任務棧,此時按Home鍵,點選啟動B應用,會進入ActivityB頁面,也就是說ActivityBActivityA的任務棧移動到了自己的任務棧中,此時點選返回,會依次退出ActivityB所在任務棧的各個頁面,直到B應用退出。

注意:allowTaskReparenting在不同Android版本上表現有所不同,Android9以下是生效的,Android9,10又是失效的,但Android11又修復好了,在使用時一定要好好測試,避免一些因版本差異產生的問題。

②.singleInstance

singleInstance具備singleTask模式的所有特性外,與它的區別就是,這種模式下的Activity會單獨佔用一個Task棧,具有全域性唯一性,即整個系統中就這麼一個例項,由於棧內複用的特性,後續的請求均不會建立新的Activity例項,除非這個特殊的任務棧被銷燬了。以singleInstance模式啟動的Activity在整個系統中是單例的,如果在啟動這樣的Activity時,已經存在了一個例項,那麼會把它所在的任務排程到前臺,重用這個例項。

③.singleInstancePerTask

釋義:singleInstancePerTask的作用和singleTask幾乎一模一樣,不過singleInstancePerTask不需要為啟動的Activity設定一個特殊的taskAffinity就可以建立新的task,換句話講就是設定singleInstancePerTask模式的activity可以存在於多個task任務棧中,並且在每個任務棧中是單例的。

多次啟動設定singleInstancePerTask模式的Activity並不會多次建立新的任務棧,而是如singleInstance模式一樣,把當前Activity所在的任務棧置於前臺展示,如果想每次以新的任務棧啟動需要設定FLAG_ACTIVITY_MULTIPLE_TASKFLAG_ACTIVITY_NEW_DOCUMENT,使用方式如下:

intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);

此時,每次啟動Activity就會單獨建立新的任務棧。

注意:測試需要在Android12的真機或者模擬器上,否則預設為Standard模式

3.taskAffinity

taskAffinity可以指定任務棧的名字,預設任務棧是應用的包名,前提是要和singleTask,singleInstance模式配合使用,standardsingleTop模式無效,當app存在多個任務棧時,如果taskAffinity相同,則在最近任務列表中只會出現處於前臺任務棧的頁面,後臺任務棧會“隱藏”在某處,如果taskAffinity不同,最近任務列表會出現多個任務頁面,點選某個就會把該任務棧至於前臺。

4.清空任務棧

activity跳轉後設置FLAG_ACTIVITY_CLEAR_TASK即可清空任務棧,並不是新建一個任務棧,而是清空並把當前要啟動的activity置於棧底,使用場景比如:退出登入跳轉到登入頁面,可以以此情況activity任務棧。 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK);

注意:FLAG_ACTIVITY_CLEAR_TASK必須與FLAG_ACTIVITY_NEW_TASK一起使用.

5.Activity.FLAG

FLAG_ACTIVITY_NEW_TASK

FLAG_ACTIVITY_NEW_TASK並不像起名字一樣,每次都會建立新的task任務棧,而是有一套複雜的規則來判斷:

  • 通過activity型別的context啟動,如果要啟動的ActivitytaskAffinity與當前Activity不一致,則會建立新的任務棧,並將要啟動的Activity置於棧底,taskAffinity一致的話,就會存放於當前activity所在的任務棧(注意啟動模式章節第三點taskAffinity的知識點);
  • taskAffinity一致的情況下,如果要啟動的activity已經存在,並且是棧根activity,那麼將沒有任何反應(啟動不了要啟動的activity)或者把要啟動的activity所在的任務棧置於前臺;否則如果要啟動的activity不存在,將會在當前任務棧建立要啟動的activity例項,併入棧;
  • taskAffinity一致的情況下,如果要啟動的activity已經存在,但不是棧根activity,依然會重新建立activity示例,併入棧(前提是:要啟動的activitylaunchModestandard,意思就是是否會建立新例項會受到launchMode的影響);
  • activitycontext啟動activity時(比如在service或者broadcast中啟動activity),在android7.0之前和9.0之後必須新增FLAG_ACTIVITY_NEW_TASK,否則會報錯(基於android-32的原始碼,不同版本可能不同):

``` //以下程式碼基於android 12 public void startActivity(Intent intent, Bundle options) { warnIfCallingFromSystemProcess(); final int targetSdkVersion = getApplicationInfo().targetSdkVersion;

//檢測FLAG_ACTIVITY_NEW_TASK
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
        && (targetSdkVersion < Build.VERSION_CODES.N
                || targetSdkVersion >= Build.VERSION_CODES.P)
        && (options == null
                || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
    //未設定FLAG_ACTIVITY_NEW_TASK,直接丟擲異常
    throw new AndroidRuntimeException(
            "Calling startActivity() from outside of an Activity "
                    + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                    + " Is this really what you want?");
}
//正常啟動activity
mMainThread.getInstrumentation().execStartActivity(
        getOuterContext(), mMainThread.getApplicationThread(), null,
        (Activity) null, intent, -1, options);

} ```

注意:FLAG_ACTIVITY_NEW_TASK的設定效果受到taskAffinity以及其他一些配置的影響,實際使用過程中一定要進行充分測試,並且不同的android版本也會表現不同,極端場景下要仔細分析測試,選擇最優方案;

提示:通過adb shell dumpsys activity activities命令可以檢視activity任務棧;

6.多程序

正常情況下,app執行在以包名為程序名的程序中,其實android四大元件支援多程序,通過manifest配置process屬性,可以指定與包名不同的程序名,即可執行在指定的程序中,從而開啟多程序,那麼,開啟多程序有什麼優缺點呢?

多程序下,可以分散記憶體佔用,可以隔離程序,對於比較重的並且與其他模組關聯不多的模組可以放在單獨的程序中,從而分擔主程序的壓力,另外主程序和子程序不會相互影響,各自做各自的事,但開啟了多程序後,也會帶來一些麻煩事,比如會引起Application的多次建立,靜態成員失效,檔案共享等問題。

所以是否選擇使用多程序要看實際需要,我們都知道app程序分配的記憶體是有限的,超過系統上限就會導致記憶體溢位,如果想要分配到更多的記憶體,多程序不失為一種解決方案,但是要注意規避或處理一些多程序引起的問題;

設定多程序的方式:

``` android:process=":childProcess" //實際上完整的程序名為:包名:childProcess,這種方式宣告的屬於私有程序。

android:process="com.child.process" //完整的程序名即為宣告的名字:com.child.process,這種方式宣告的屬於全域性程序。 ```

7.excludeFromRecents

excludeFromRecents如果設定為true,那麼設定的Activity將不會出現在最近任務列表中,如果這個Activity是整個Task的根Activity,整個Task將不會出現在最近任務列表中.

8.startActivityForResult被棄用

使用Activity Result Api代替,使用方式如下:

``` private val launcherActivity = registerForActivityResult( ActivityResultContracts.StartActivityForResult()) { Log.e("code","resultCode = "+it.resultCode) }

findViewById