Activity的生命週期
生命週期和啓動模式以及IntentFilter的匹配規則分析。
Activity的生命週期分爲兩個部分:
- 典型情況下的生命週期
- 異常情況下的生命週期
1.典型情況下的生命週期分析
- onCreate :表示
Activity
正在被創建。在這裏可以做一些初始化的工作。 - onRestart :表示
Activity
正在重新啓動。噹噹前Activity
從不可見重新變成可見狀態。 - onStart :表示
Activity
正在被啓動。已經可見,但不在前臺,無法交互。 - onResume :表示
Activity
已經可見,並且出現在前臺可以交互。 - onPause :表示
Activity
正在停止。在這裏可以做一些儲存數據,停止動畫等工作,但不能太耗時,因爲必須onPause
執行完成之後新的Activity
才能Resume
。 - onStop :表示
Activity
即將停止。可以進行一些稍微重量級的回收工作,不能太耗時。 - onDestroy :表示
Activity
即將被銷燬。可以進行一些回收工作和最終的資源釋放。
注意:
- onStart和onStop是從Activity是否可見這個角度來回調的
- onResum和onPause是從Activity是否在前臺這個角度來回調的
2.異常情況下的生命週期分析
情況 1:資源相關的系統配置發生改變導致Activity被殺死並重新創建
比如說橫屏手機和豎屏手機會拿到兩張不同的圖片(設定了landscape或者portrait狀態下的圖片)。本來手機在豎屏狀態,突然旋轉屏幕,由於系統配置發生了變化,在默認情況下,Activity會被銷燬並且重新創建,當然我們也可以阻止系統重新創建我們的Activity。
當系統配置發生改變後,Activity會調用 onPause -> onStop -> onDestroy。
由於是異常情況終止,系統會在onStop
之前調用onSaveInstanceState
來保存當前Activity
的狀態。(與onPause
沒有時序關係)
當Activity
被系統重新創建後,系統會調用onRestoreInstanceState
,把之前onSaveInstanceState
方法所保存的Bundle
對象作爲參數同時傳給onRestoreInstanceState
和onCreate
方法。(從時序來說,onRestoreInstanceState
的調用時機在onStart
之後)
而在視圖方面,當Activity在異常情況下需要重新創建時,系統會默認爲我們保存當前Activity的視圖結構,並且在Activity重啓後爲我們恢復這些數據。
其實每個View都有onSaveInstanceState
和onRestoreInstanceState
,關於保存和恢復View層級結構,系統的工作流程如下:
onSaveInstanceState方法,系統只會在Activity即將被銷燬並且有機會重新顯示的情況下才會去調用它。
情況 2:資源內存不足導致低優先級的Activity被殺死
其實這種情況的數據存儲與恢復過程與情況 1完全一致。
Activity的優先級情況:
- 前臺的
Activity
—— 正在和用戶交互的Activity
,優先級最高 - 可見但非前臺的
Activity
—— 比如Activity
中彈出了一個對話框,導致Activity
可見但是位於後臺,無法和用戶進行直接交互 - 後臺的
Activity
—— 已經被暫停的Activity
,比如執行了onStop
,優先級最低
當系統內存不足時,系統就會按照上述優先級去殺死目標Activity
所在的進程,並在後續通過onSaveInstanceState
和onRestoreInstanceState
來存儲和恢復數據。而將後臺工作放入Service
中是一個比較好的方法。
當系統配置改變後,Activity如何不被重新創建
由於系統配置中有很多內容,如果當某項內容發生改變後,不想系統重新創建Activity
,可以給Activity
指定configChanges
屬性:
android:configChanges="orientation|keyboardHidden"
項目 | 含義 |
---|---|
mcc | SIM卡唯一標識IMSI(國際移動用戶識別碼)中的國家代碼,由3位數組成,中國爲460.此項 標識mcc代碼發生了改變 |
mnc | SIM卡唯一標識IMSI(國際移動用戶識別碼)中的運營商代碼,由兩位數字組成,中國移動TD系統爲00,中國聯通爲01,中國電信爲03。此項標識mnc發生改變 |
locale | 設備的本地位置發生了改變們一般指切換了系統語言 |
touchscreen | 觸摸屏發生了改變,正常情況下無法發生,可以忽略它 |
keyboard | 鍵盤類型發生了改變,比如用戶使用了外插鍵盤 |
keyboardHidden | 鍵盤的可訪問性發生了改變,比如用戶調出了鍵盤 |
navigation | 系統導航方式發生了改變,比如採用了軌跡球導航,很難發生,可以忽略 |
screenLayout | 屏幕布局發生了改變,很可能是用戶激活了另一個顯示設備 |
fontScale | 系統字體縮放比如發生了改變,比如用戶選擇了一個新字號 |
uiMode | 用戶界面模式發生了改變,比如是否開啓了夜間模式(API8新添加) |
orientation | 屏幕方向發生了改變,這個是最常用的,比如旋轉了手機屏幕 |
screenSize | 當屏幕的尺寸信息發生了改變,當旋轉設備屏幕時,屏幕尺寸會發生變化,這個選項比較特殊,它和編譯選項有關,當編譯選項中的minSdkVersion和targetSdkVersion 均低於13時,此選項不會導致Activity重啓,否則會導致Activity重啓(API13新添加) |
smallestScreenSize | 設備的物理屏幕尺寸發生了改變,這個項目和屏幕的方向沒有關係,僅僅表示在實際的物理屏幕的尺寸改變的時候發生,比如用戶切換到了外部的顯示設備,這個選項和screenSize一樣,當編譯選項中的minSdkVersion和targetSdkVersion均低於13時,此選項不會導致Activity重啓,否則會導致Activity重啓(API13新添加) |
layoutDirection | 當佈局方向發生變化,這個屬性用的比較少,正常情況下無須修改佈局的layoutDirection屬性(API17新添加) |
如果我們沒有在Activity
的configChanges
屬性中指定該選項的話,當配置發生改變後就會導致Activity
重新創建。
最常用的只有locale
、orientation
和keyboardHidden
。
需要修改的代碼很簡單,只需要在AndroidMenifest.xml中加入Activity的聲明即可:
<activity
android:name="com.dimon.MainActivity"
android:configChanges="orientation|screenSize"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
Log.d(TAG,"onConfigurationChanged,newOrientation:" + newConfig.orientation);
}
Activity
沒有重新創建,並且沒有調用onSaveInstanceState
和onRestoreInstanceState
來存儲和恢復數據,而是系統調用了Activity
的onConfigurationChanged
方法,這個時候我們可以加入一些自己的特殊處理了。
Activity的啓動模式
Activity 的 LaunchMode
複習一點:啓動
Activity
時,系統會創造實例並把他們放入任務棧裏,而任務棧是一種“後進先出”的棧結構。
Activity
的四種啓動模式:
-
standard:標準模式、默認模式。每次啓動一個
Activity
都會重新創建一個新的實例,不管這個實例是否已經存在。在這種模式下,某個Activity
啓動了一號Activity
,那麼一號Activity
就運行在啓動它的那個Activity
所在的棧中。 -
singleTop:棧頂複用模式。如果新的
Activity
已經位於任務棧的棧頂,那麼此Activity
就不會被重新創建,同時它的onNewIntent
方法會被回調,並且可以根據此方法的參數獲得當前請求的信息。 -
singleTask:棧內複用模式。在這種單實例模式下,只要
Activity
在一個棧中存在,那麼多次啓動此Activity
都不會重新創建實例,系統也會調用其onNewIntent
。
- singleInstance:單實例模式。這是一種加強的singleTask模式,除了具有singleTask模式的所有特性外,還加強了一點,那就是具體此種模式的
Activity
只能單獨地位於一個任務棧中。
注:在任何跳轉的時候,首先調用本
Activity
的onPause
,然後跳轉。如果被跳轉的activity
由於啓動方式而沒創建新的實例,則會先調用onNewIntent
,然後按照正常的生命週期調用。
如:
- A→B,A:onPause;B:onCreate,onStart,onResume。
- A(singleTop)→A,A:onPause;A:onSaveInstanceState;A:onResume。
一些具體問題與情況
- 1:首先要說明:任務棧分爲前臺任務棧和後臺任務棧,後臺任務棧中的
Activity
位於暫停狀態。
當在前臺任務棧AB啓動Activity D,而D正好是後臺任務棧CD的棧頂。現在假設後臺任務棧裏的Activity的啓動模式是singleTask
,請求啓動D,那麼整個後臺任務棧都會被切換到前臺,直接佔據前臺任務棧的棧頂,即ABCD。如果之前請求啓動的是C,那麼就會變成ABC。
singleTask
模式的Activity
切換到棧頂會導致在它之上的棧內的Activity
出棧。
- 2:
TaskAffinity
:任務相關性。標識一個Activity
所需要的任務棧的名字。adnroid:taskAffinity="com.dimon.task1"
默認情況下Activity
所需要的任務棧的名字爲應用的包名。
如何給Activity指定啓動模式呢?
第一種方法:通過AndroidMenifest
爲Activity
指定啓動模式。
<activity
android:name="com.dimon.SecondActivity"
android:configChanges="screenLayout"
adnroid:taskAffinity="com.dimon.task1"
android:launchMode="singleTask"
android:label="@string/app_name"/>
第二種方法:通過在Intent
中設置標誌位來爲Activity
指定啓動模式。Intent intent = new Intent();
intent.setClass(MainActivity.this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
區別
- 優先級:第二種方法比第一種優先級高,兩種都存在時,以第二種爲準。
- 限定範圍:第一種無法設置
FLAG_ACTIVITY_NEW_TASK
標識,而第二種無法指定singleTask
模式。
Acticity 中的一些比較常用的 Flags
FLAG_ACTIVITY_NEW_TASK
這個標記位的作用是爲Activity
指定“singleTask
”啓動模式,其效果和在XML中指定該啓動模式相同。
FLAG_ACTIVITY_SINGLE_TOP
這個標記位的作用是爲Activity
指定“singleTop
”啓動模式,其效果和在XML中指定該啓動模式相同。
FLAG_ACTIVITY_CLEAR_TOP
具有此標記位的Activity
,當它啓動時,在同一個任務棧中所有位於他上面的Activity
都要出棧。singleTask
默認就具有這個標記位的效果。如果被啓動的Activity
採用了standard
啓動模式,那麼它連通它之上的Activity
都要出棧,系統會創建新的Activity
實例並放入棧頂。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
具有這個標記的Activity
不會出現在歷史Activity
的列表中,當某種情況下我們不希望用戶通過歷史列表回到我們的Activity
的時候這個標記比較有用。它等同於在XML中指定Activity的屬性android:excludeFromRecents="true"
。
IntentFilter的匹配規則
action的匹配規則
action
的匹配要求Intent
中的action
存在且必須和過濾規則中的一個action
相同。區分大小寫。
categoryd的匹配規則
如果Intent
中含有category
,那麼所以的category
都必須和過濾規則中的其中一個category
相同。
如果Intent
中不含有category
,那麼也能匹配,因爲系統在調用startActivity
或者startActivityForResult
的時候會默認爲Intent
加上“android.intent.category.DEFAULT
”這個category
,所以爲了我們的Activity
能夠接收隱式調用,就必須在intent-filter
中指定“android.intent.category.DEFAULT
”。
data的匹配規則
規則與action
相似。
<intent-filter>
<data android:mimeType="image/*"/>
...
</intent-filter>
以上規則說明Intent
中的mimeType
屬性必須爲“image/*
”才能匹配。雖然沒有定義URI
,但是還是得有默認值,URI
的默認值爲content
和file
。也就是說沒有URI
也得在Intent
中的URI
部分的schema
必須爲content
或者file
才能匹配。
intent.setDataAndType(uri.parse("file://abc"),"image/png");
如果要爲Intent指定完整的data,必須調用setDataAndType方法,不能先調用setData再調用setType。因爲這兩個方法會彼此清除對方的值。源碼如下:
public Intent setData(Uri data){
mData = data;
mType = null;
return this;
}
而data
的結構略微複雜,語法如下:
<data android:scheme="string"
android:host="string"
android:port="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:mimeType="string"/>
data
由兩部分組成,mimeType
和URI
。
mimeType
指媒體類型,例如image/jpeg、audio/mpeg4-generic和video/*等。
URI
的結構如下:
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
- Scheme:URI的模式,比如http、file、content等。
- Host:URI的主機名,比如www.baidu.com
- Port:URI的端口號。
- Path、pathPattern和pathPrefix:這三個表示路徑信息,path完整路徑信息;pathPattern也表示完整的路徑信息,但是它裏面可以包括通配符“*”,注意正則表達式;pathPrefix表示路徑的前綴信息。
IntentFilter整合例子
<intent-filter>
<action android:name="com.dimon.1"/>
<action android:name="com.dimon.2"/>
<category android:name="com.dimon.1a"/>
<category android:name="com.dimon.2b"/>
<category android:name="android.intent.category.DEFAULF"/>\
<data android:mimeType="text/plain"/>
</intent-filter>
Intent intent = new Intent("com.dimon.1");
intent.addCategory("com.dimon.2b");
intent.setDataAndType(Uri.parse("file://abc"),"text/plain");
startActivity(intent)
Some Tips
使用隱式方式啓動Activity
時,最好做一下判斷,看看是否有Activity
能夠匹配我們的隱式Intent
。
方法有兩個:採用PackageManager
的resolveActivity
方法或者Intent
的resolveActivity
方法,如果他們匹配不了就返回null
。
另外PackageManager
還提供了queryIntentActivities
方法:它不是返回最佳匹配的Activity
信息而是返回所有成功匹配的Activity
信息。
public abstract List<ResolveInfo> queryIntentActivities(Intent intent, int flags);
public abstract ResolveInfo resolveActivity(Intent intent, int flags);
第二個參數我們使用MATCH_DEFAULT_ONLY
這個標記位,這個標記位僅僅匹配那些在intent-filter
中聲明瞭<category android:name="android.intent.category.DEFAULT"/>
這個category
的Activity
。
只要上述兩個方法不返回null
,那麼startActivity
一定可以成功。因爲不含有DEFAULT
這個category
的Activity
是無法接收隱式Intent
的。