面試的時候,面試官經常會和你聊聊Activity的啟動模式,但Activity啟動牽扯的知識點其實很多,并非能單單用四個啟動模式就能概括的,默認的啟動模式的表現會隨著Intent Flag的設置而改變。今天就來深入聊聊Activity的啟動模式。
基本的啟動模式
學Android的都知道,Activity有4種啟動模式:
- 標準模式 standard : 每次都會啟動一個新的Activity實例,這是默認的啟動模式。此模式下啟動的Activity會進入啟動它的Activity所在的任務棧中。
- 棧頂復用模式 singleTop :如果Activity實例位于當前任務棧頂,就重用棧頂實例,而不新建,并回調該實例onNewIntent()方法,否則走新建流程。
- 棧內復用模式 singleTask : 只要該Activity在一個任務棧中存在,就不會重新創建,而是將目標Activity上面的Activity全部出棧,然后回調 onNewIntent(intent)。 方法。如果不存在,系統會先尋找是否存在需要的棧,如果不存在該棧,就創建一個任務棧,并把該Activity放進去;如果存在,就會創建到已經存在的棧中。
- 單例模式 singleInstance : 具有此模式的Activity只能單獨位于一個任務棧中,且此任務棧中只有唯一一個實例。singleInstance算是加強型的singleTask模式,也就是說它也有棧內復用的特性,多次請求不會重復創建,也會調用onNewIntent(intent)方法。
相信上面的內容大家都知道,不過上面的場景僅僅適用于Activity啟動Activity,并且采用的都是默認Intent,沒有額外添加任何Flag,否則表現就可能跟上面的完全不一致。
其他啟動模式
大家都知道,啟動Activity是通過Intent的,而Intent是有一個Flags屬性的,可以用intent.setFlags()方法來設置。當我們設置了一個或多個Flag后,Activity的啟動方式有可能就會和之前的描述不同了。
taskAffinity
每個Activity都有taskAffinity屬性,這個屬性指出了它希望進入的Task。
如果一個Activity沒有顯式的指明該Activity的taskAffinity,那么它的這個屬性就等于Application指明的taskAffinity,如果Application也沒有指明,那么該taskAffinity的值就等于包名。而Task也有自己的affinity屬性,它的值等于它的根Activity的taskAffinity的值。
Flags
Flag 有很多,我就重點說幾個重要的。
FLAG_ACTIVITY_NEW_TASK
Intent.FLAG_ACTIVITY_NEW_TASK是啟動模式中最關鍵的一個Flag。是棧內復用模式的關鍵屬性。
我們知道,非Activity的Context(如Service)是沒有任務棧的,那么用standard模式啟動Activity時就會出現問題,需要給Intent設置flag,Intent.FLAG_ACTIVITY_NEW_TASK,強制Activity選擇新的任務棧(這就和singleTask一樣了)。
不設置會直接報異常,在startActivity()方法的底層ContextImpl.startActivity()中是有檢驗的。不過,在Android7.0、8.0系統源碼有Bug,不會報異常,9.0恢復正常。
//ContextImpl Android6.0
@Override
public void startActivity(Intent intent) {
warnIfCallingFromSystemProcess();
startActivity(intent, null);
}
@Override
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
//檢驗是否添加了 FLAG_ACTIVITY_NEW_TASK
if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity)null, intent, -1, options);
}
依據該Flag啟動模式可以分成兩類,設置了該屬性的與未設置該屬性的。singleTask及singleInstance在AMS中被預處理后,會自動設置為Intent.FLAG_ACTIVITY_NEW_TASK。standard及singletTop的Activity不會被設置Intent.FLAG_ACTIVITY_NEW_TASK,可以通過顯示的Intent.setFlags進行設置。
Intent intent = new Intent(TestService.this, SecondActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
FLAG_ACTIVITY_CLEAR_TASK
個屬性必須同FLAG_ACTIVITY_NEW_TASK配合使用。
設置了FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK后,如果目標task已經存在,將清空已存在的目標Task,否則,新建一個Task棧,之后,新建一個Activity作為根Activity。
Intent.FLAG_ACTIVITY_CLEAR_TASK的優先級非常高,基本可以無視所有的配置,包括啟動模式及其他的Intent Flag,哪怕是singleInstance也會被finish,并重建。
FLAG_ACTIVITY_CLEAR_TOP
這個Flag有點復雜,它涉及到的情況較多。
如果單獨使用Intent.FLAG_ACTIVITY_CLEAR_TOP,并且沒有設置特殊的launchmode,那么,Google官方的示例是:如果ABCD Task中的D采用FLAG_ACTIVITY_CLEAR_TOP喚起B,這個時候首先會將CD出棧,但是至于B是否會重建,要視情況而定。
如果沒有設置FLAG_ACTIVITY_SINGLE_TOP,則會將B finish掉,之后創建新的入棧。如果當前棧中沒有目標Activity則直接新建,而不會去其他棧中尋找。
如果同時設置了FLAG_ACTIVITY_SINGLE_TOP,則在當前棧已有的情況下就不會重建,而是直接回調B的onNewIntent()。當前棧沒有依舊是直接新建。
如果同時使用了FLAG_ACTIVITY_NEW_TASK,這個時候,目標是Activity自己所屬的Task棧,如果在自己的Task中能找到一個Activity實例,則將其上面的及自身清理掉,之后重建。
如果同時在加上FLAG_ACTIVITY_SINGLE_TOP,則會更特殊一些,如果topActivity不是目標Activity,就會去目標Task中去找,并喚起。如果topActivity是目標Activity,就直接回調topActivity的onNewIntent,無論topActivity是不是在目標Task中。
FLAG_ACTIVITY_SINGLE_TOP
這個Flag和singleTop作用是一樣的,在Task棧頂有的話,就不新建,棧頂沒有的話,就新建,這里的Task可能是目標棧,也可能是當前Task棧。
IntentFilter相關
大家都知道,啟動Activity分為兩種方式,顯式和隱式調用。隱式調用需要Intent能夠匹配目標組件的IntentFilter中所設置的過濾信息,如果不匹配是無法啟動目標組件的。
IntentFilter中過濾的信息有action、category、data,保存在Activity配置文件的intent-filter標簽下。只有同時匹配有action、category、data才算是匹配成功。但是一個Activity可以有多個intent-filter標簽,只要有一組intent-filter標簽匹配,即可啟動對應的Activity。
一個普通的 <intent-filter>標簽如下:
<activity android:name=".demo.MyActivity">
<intent-filter>
<action android:name="bupt.edu.demo.a1"/>
<action android:name="bupt.edu.demo.a2"/>
<category android:name="bupt.edu.demo.c1"/>
<category android:name="bupt.edu.demo.c2"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
action的匹配
action是一個字符串,系統本身是有些預定義的action的,不過我們也可以自己指定action。action的匹配規則是:
Intent中的action必須和action標簽下的字符串完全一致,但是當<intent-filter>標簽下有多個action標簽時,只需要匹一個action就算是成功匹配。例如在上面的例子中,只要我們的action值為"bupt.edu.demo.a1"或者"bupt.edu.demo.a2"即可算是匹配成功。
不過,需要注意的是Intent中如果沒有指定action,那么匹配失敗。另外,action是區分大小寫的,大小寫不同也會匹配失敗。
即,action的匹配要求Intent中的action必須存在且和過濾規則中的一個action完全一致。
category
category也是一個字符串,系統本身也有些預定義的category,我們也可以自己指定category。category的匹配規則是:
Intent中如果存在category,無論存在幾個,每個都必須能夠匹配到<intent-filter>標簽下的category才算匹配成功。不過,Intent如果沒設置category是可以匹配成功的。因為系統在調用startActivity或startActivityForResult時會默認在intent中添加android.intent.category.DEFAULT這個category。
因此我們在配置activity時,必須加上android.intent.category.DEFAULT這個category,才能被隱式調用。
data
data的語法有些復雜,主要由mimeType(用于指定媒體類型)和URI組成。在匹配時通過intent.setDataAndType(Uri data, String type)方法對data進行設置。
setData()方法和setType()方法會將對方的值清除設為null,因此不能同時調用兩個方法來設置data。
另外,如果我們要啟動隱式啟動Activity,應該先判斷是否有對應的Activity可以啟動。可以用PackageManager的resolveActivity方法或者Intent的resolveActivity方法判斷是否有Activity匹配該隱式Intent。
PackageManager的resolveActivity方法會返回最佳匹配的Activity信息,沒匹配到的話會返回null。
Intent的resolveActivity方法,會返回所有匹配到的Activity信息,沒匹配到的話會返回null。
同樣,針對Service、BoardcastReceiver也有相應的方法可以用來判斷。