【android activity重難點突破】這些知識還不會,面試八成被勸退
highlight: a11y-dark
Activity
作為android
四大元件之一,地位就不用多說了吧,該元件看起來是比較簡單的,但是也涉及到很多知識點,要想全部理解並在合適的業務場景下使用,也是需要一定的技術沉澱,本文主要是對activity
一些重要知識點進行總結整理,可能平時不一定用到,但是一定要有所瞭解。
當然這些知識點並沒有設計過多原始碼部分,比如activity
的啟動流程什麼的,主要是零散的知識點,對於activity
的啟動流程網上文章太多了,後面自己也準備重新梳理下,好記性不如爛筆頭,在不斷學習整理的過程中,一定會因為某個知識點而豁然開朗。
1.生命週期
①.兩個頁面跳轉
從MainActivity
跳轉到SecordActivity
的生命週期,重點關注Main
的onPause
和onStop
與Secord
幾個關鍵生命週期的順序,以及從Secord
返回時與Main
的生命週期的交叉:
可以發現Main
頁面的onPause
生命週期之後直接執行Secord
的onCreate,onStart,onResume
,所以onPause
生命週期內不要執行耗時操作,以免影響新頁面的展示,造成卡頓感。
②.彈出Dialog
- 單純的彈出
Dialog
是不會影響Activity
的生命週期的; - 啟動
dialog theme
的Activity
的時候,啟動的activity
只會執行onPause
方法,onStop
不會執行,被啟動的activity
會正常走生命週期,back
的時候,啟動的Activity
會對應執行onResume
方法;
③.橫豎屏切換
- AndroidManifest不配置
configChanges
時,橫豎屏切換,會銷燬重建Activity,生命週期會重新走一遍; - 當Activity
configChanges="orientation|screenSize"
時,橫豎屏切換不會重新走Activity生命週期方法,只會執行onConfigurationChanged
方法,如需要可以在此方法中進行相應業務處理;
如橫豎屏切換時需要對佈局進行適配,可在res下新建
layout-port
、layout-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的啟動模式一直是standard
、singleTop
、singleTask
、singleInstance
四種,Android 12新增了singleInstancePerTask
啟動模式,在這裡不一一介紹,僅介紹重要知識點。
①.singleTask
1.Activity
是一個可以跨程序、跨應用的元件,當你在 A App
裡開啟 B App
的Activity
的時候,這個Activity
會直接被放進A的Task
裡,而對於B的Task
,是沒有任何影響的。
從A應用啟動B應用,預設情況下啟動的B應用的Activity
會進入A應用當前頁面所在的任務棧中,此時按home建,再次啟動B應用,會發現B應用並不會出現A啟動的頁面(前提是A應用啟動的不是B應用主activity
,如果是必然一樣),而是如第一次啟動一般.
如果想要啟動B應用的時候出現被A應用啟動的頁面,需要設定B應用被啟動頁的launchmode
為singleTask
,此時從A應用的ActivityA
頁面啟動B應用的頁面ActivityB
(launchmode
為singleTask
),發現動畫切換方式是應用間切換,此時ActivityB
和ActivityA
分別處於各自的任務棧中,並沒有在一個task中,此時按Home鍵後,再次點選啟動B應用,發現B應用停留在ActivityB
頁面。
如果想要實現上述效果,除了設定launchmode之外,還可以通過設定allowTaskReparenting
屬性達到同樣的效果,Activity
預設情況下只會歸屬於一個 Task,不會在多個Task之間跳來跳去,但你可以通過設定來改變這個邏輯,如果你不設定singleTask
,而是設定allowTaskReparenting
為true
,此時從A應用的ActivityA
頁面啟動B應用的頁面ActivityB
(設定了allowTaskReparenting
為true
),ActivityB
會進入ActivityA
的任務棧,此時按Home鍵,點選啟動B應用,會進入ActivityB
頁面,也就是說ActivityB
從ActivityA
的任務棧移動到了自己的任務棧中,此時點選返回,會依次退出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_TASK
和FLAG_ACTIVITY_NEW_DOCUMENT
,使用方式如下:
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
此時,每次啟動Activity就會單獨建立新的任務棧。
注意:測試需要在Android12的真機或者模擬器上,否則預設為Standard模式
3.taskAffinity
taskAffinity
可以指定任務棧的名字,預設任務棧是應用的包名,前提是要和singleTask
,singleInstance
模式配合使用,standard
,singleTop
模式無效,當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
啟動,如果要啟動的Activity
的taskAffinity
與當前Activity
不一致,則會建立新的任務棧,並將要啟動的Activity
置於棧底,taskAffinity
一致的話,就會存放於當前activity
所在的任務棧(注意啟動模式章節第三點taskAffinity
的知識點); taskAffinity
一致的情況下,如果要啟動的activity
已經存在,並且是棧根activity
,那麼將沒有任何反應(啟動不了要啟動的activity
)或者把要啟動的activity
所在的任務棧置於前臺;否則如果要啟動的activity
不存在,將會在當前任務棧建立要啟動的activity
例項,併入棧;taskAffinity
一致的情況下,如果要啟動的activity
已經存在,但不是棧根activity
,依然會重新建立activity
示例,併入棧(前提是:要啟動的activity
的launchMode
為standard
,意思就是是否會建立新例項會受到launchMode
的影響);- 非
activity
的context
啟動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
9.Deep link
簡單理解,所謂Deep Link就是可以通過外部連結來啟動app或者到達app指定頁面的一想技術,比如可以通過點選簡訊或者網頁中的連結來拉起app到指定頁面,以達到提供日活或者其他目的,一般流程是可以通過在manifest
的activity
標籤中配置固定的schema
來實現這種效果,形如:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="jumptest"
android:host="work"
android:port="8801"
android:path="/main"
/>
</intent-filter>
然後在網頁中就可以通過如下方式來啟動當前activity:
<a href="jumptest://work:8801/main?data=123456">你好</a>
格式
<scheme>://<host>:<port>/<path>?<query>
被啟動的app可以通過如下方式拿到傳遞的引數以及schmea配置項:
val host = schemaIntent.data?.host
val path = schemaIntent.data?.path
val schema = schemaIntent.data?.scheme
val query = schemaIntent.data?.query
Log.e("schema","host = $host, path = $path, schema = $schema, query = $query")
結果:
注意:
1.
intent-filter
與Main主Activity搭配使用時,要單獨開啟一個intent-filter,否則匹配不到。
2.從android12開始,設定了intent-filter
標籤後,activity的exported必須設定成true,這個要注意(android12之前,其實添加了intent-filter,系統也會預設設定exported為true)。
①.app link
App link是一種特殊的Deep link,它的作用就是可以使通過網站地址開啟app的時候,不需要使用者選擇使用哪個應用來開啟,換種說法就是,我可以設定預設開啟次地址的應用,這樣一來,就可以直接引導到自己的app。
更多關於App link的可以參考這篇文章,或者看官網介紹。
10.setResult和finish的順序關係
通過startActivityForResult
啟動activity
,通常會在被啟動的activity
的合適時機呼叫setResult
來回調資料給上一個頁面,然後當前頁面返回的時候就會回撥onActivityResult
,這裡要注意setResult
的呼叫時機,請一定要在activity的finish()方法之前呼叫,否則可能不會生效(不會回撥onActivityResult)。
原因如下:
``` private void finish(int finishTask) { if (mParent == null) { int resultCode; Intent resultData; //會在finish的時候把回撥資料賦值 synchronized (this) { resultCode = mResultCode; resultData = mResultData; } ··· if (ActivityClient.getInstance().finishActivity(mToken, resultCode, resultData, finishTask)) { mFinished = true; } } else { mParent.finishFromChild(this); } ··· }
//setResult對mResultCode,mResultData賦值 public final void setResult(int resultCode) { synchronized (this) { mResultCode = resultCode; mResultData = null; } }
``
由上述程式碼可以看出,
setResult必須在
finish之前賦值,才能夠在
finish的時候拿到需要
callback的資料,以便在合適的時機回撥
onActivityResult`;
11.onSaveInstanceState()和onRestoreInstanceState()
activity
在非正常情況被銷燬的時候(非正常情況:橫豎屏切換,系統配置發生變化,記憶體不足後臺activity
被回收等),當重新回到該activity
,系統會重新例項化該物件,如果沒有對頁面輸入的內容進行儲存,就會存在內容丟失的情況,此時可以通過onSaveInstanceState
來儲存頁面資料,在onCreate
或者onRestoreInstanceState
中對資料進行恢復,形如:
``` override fun onSaveInstanceState(outState: Bundle) { outState.putString("SAVE_KEY","SAVE_DATA") outState.putString("SAVE_KEY","SAVE_DATA2") super.onSaveInstanceState(outState) } //需要判空,savedInstanceState不一定有值 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if(null != savedInstanceState){ saveData = savedInstanceState.getString("SAVE_KEY") ?: "" saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: "" } setContentView(R.layout.activity_main) }
//或者在onRestoreInstanceState恢復資料,無需判空,回撥此方法一定有值 override fun onRestoreInstanceState(savedInstanceState: Bundle) { saveData = savedInstanceState.getString("SAVE_KEY") ?: "" saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: "" super.onRestoreInstanceState(savedInstanceState) }
```
注意:請使用
onSaveInstanceState(outState: Bundle)
一個引數的方法,兩個引數的方法和Activity
的persistableMode
有關。
本文主要對Activity重難點知識進行整理和解釋,希望對大家有所幫助,當然難免存在錯誤,如有發現,希望指正,如果感覺不錯,麻煩點個贊,這將給我持續更文以更大的動力,後續如有其他知識點,也會持續更新。