2. Activity的启动模式
2.0 前言
本文总结自任玉刚老师的《Android开发艺术探索》,文章中的【示例】在这里
2.1 Activity的LaunchMode(启动模式)
任务栈简介:“后进先出”的栈结构,每次back就会有一个Activity出栈,直到栈空为止,当栈中无任何Activity时候,系统就会回收这个任务栈。默认情况下,当多次启动同一个Activity的时候,系统会创建多个实例并把它们一一放入任务栈中。
- (1) standard:标准模式。也是系统的默认模式,每次启动一个Activity都会重新创建一个新的实例,不管这个实例是否已经存在。被创建的实例的生命周期符合典型情况下的Activity的生命周期。一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。比如Activity A启动了Activity B(B是标准模式),那么B就会进入A所在的栈中。
当我们用ApplicationContext去启动standard模式的Activity的时候会报错,错误如下:
E/AndroidRuntime(674): android.util.AndroidRuntimeException : Calling startActivity from outside of and Activity context requires the FLAG_ACTIVITY_NEW_TASK flag.Is this really what you want?
出现该错误是因为standard模式的Activity默认会进入启动它的Activity所属的任务栈中,但是由于非Activity类型的Context(如ApplicationContext)并没有所谓的任务栈,所以这就有问题了。解决这个问题的方法是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就会为它创建一个新的任务栈,这个时候待启动Activity实际上已singleTask模式启动的。
(2) singleTop:栈顶复用模式。这种模式下,如果新Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法会被回调,通过此方法的参数我们可以取出当前请求的信息。需要注意的是,这个Activity的onCreate、onStart不会被系统调用,因为它并没有发生改变。如果新Activity的实例已存在但不是位于栈顶,那么新Activity仍然会重新创建。举个例子,假设目前栈内的情况为ABCD,其中ABCD为4个Activity,A位于栈底,D位于栈顶,这个时候假设要再次启动D,如果D的启动模式为singleTop,那么栈内的情况仍然为ABCD;如果D的启动模式为standard,那么由于D被重新创建,导致栈内的情况就变为ABCDD。
(3) singleTask:栈内复用模式。这是一种单实例模式,这种模式下。只要Activity在一个栈中存在,那么多次启动此Activity都不会重新创建实例,和singleTop一样,系统也会回调其onNewIntent。具体一点,当一个具有singleTask模式的Activity请求启动后,比如Activity A,系统首先会寻找是否存在A想要的任务栈,如果不存在,就重新创建一个任务栈,然后创建A的实例后把A放到栈中。如果存在A所需的任务栈,这是要看A是否在栈中有实例存在,如果有,那么系统会把A调到栈顶并调用它的onNewIntent方法,如果实例不存在,就创建A的实例并把A压入栈中。
[ 实例(特殊情况):如果Activity D以singleTask模式请求启动,其所需的任务栈为S1,并且当前任务栈S1的情况为ADBC,根据栈内复用的原则,此时D不会被重新创建,系统会把D切换到栈顶并调用其onNewIntent方法,同时由于singleTask默认具有clearTop的效果,会导致栈内所有在D上面的Activity全部出栈,于是S1中的情况为AD。 ]
singleTask示例1:[图片上传失败...(image-63e361-1525671162550)]示例2:[图片上传失败...(image-4d2023-1525671162550)]
【示例:singleTask的使用和adb的输出情况(adb shell dumpsys activity),看Running activities(most recent first) 】
- (4)singleInstance:单实例模式。这是一种加强的singleTask模式,它除了具有singleTask模式的所有特性外,还加强了一点,那就是具有此种模式的Activity只能单独位于一个任务栈中。比如Activity A是singleInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A独自在这个新的任务栈中,由于栈内服用的特性,后续的请求均不会创建新的Activity,除非这个独特的任务栈被系统销毁了。
指定启动模式的两种方法(注意:第二种优先级 > 第一种;第一种无法直接为Activity设定FLAG_ACTIVITY_CLEAR_TOP表示,第二种无法为Activity指定singleIntance模式):
//(1)通过AndroidMenifest
<activity
android:name=".FifthActivity"
android:launchMode="standard"/>
//(2)通过Intent中指定标志位
Intent intent = new Intent();
intent.setClass(MainActivity.this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
2.1.1 TaskAffinity
在singleTask启动模式中,多次提到某个Activity所需的任务栈。究竟什么是Activity所需的任务栈,要从TaskAffinity(任务相关性)说起,这个参数标识了一个Activity所需要的任务栈的名字,默认情况下,所有Activity所需的任务栈的名字为应用的包名。当然我们可以为每个Activity都单独制定TaskAffinity属性,这个属性值不能和包名相同,否则就相当于没有指定。
TaskAffinity属性主要和singleTask启动模式或者allowTaskReparenting属性配对使用,在其他情况下没有任何意义。另外,任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity处于暂停状态,用户可以通过切换将后台任务栈再次调到前台。
当TaskAffinity和singleTask启动模式配对使用的时候,它是具有该模式的Activity的目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中。
当TaskAffinity和allowTaslReparenting结合的时候,这种情况较复杂,产生特殊的效果:当一个应用A启动了应用B的某个Activity C后,如果这个Activity C的allowTaskReparenting( Reparent:重定父级)属性为true的话,那么当应用B被启动后,此Activity会直接从应用A的任务栈转移到应用B的任务栈中。
[ 解释:由于A启动了C,这个时候C只能运行在A的任务栈中,但是C属于B应用,正常情况下它的TaskAffinity值肯定不可能与A的任务栈相同(因为包名不同)。所以,当B被启动后,B会创建自己的任务栈,这个时候系统发现C原本想要的任务栈已经被创建了,所以就把C从A的任务栈中转移过来了。 ]
- (1) 设定了android:launchMode="singleTask"的SixthActivity 连续6次用startActivity(intent)连续自己启动自己3次:
//在cmd中执行adb shell dumpsys activity后输出的结果
Running activities (most recent first):
TaskRecord{2b9827e #383 A=com.example.learn_001_activity U=0 StackId=1 sz=2}
Run #4: ActivityRecord{e99eb46 u0 com.example.learn_001_activity/.SixthActivity t383}
Run #3: ActivityRecord{7a5d672 u0 com.example.learn_001_activity/.MainActivity t383}
TaskRecord{5ac339a #299 A=com.android.gallery3d U=0 StackId=1 sz=1}
Run #2: ActivityRecord{4df2ed7 u0 com.android.gallery3d/.app.GalleryActivity t299}
TaskRecord{52714cb #278 A=com.example.demo103 U=0 StackId=1 sz=2}
Run #1: ActivityRecord{274a48f u0 com.example.demo103/.MainBindActivity t278}
Run #0: ActivityRecord{e70354a u0 com.example.demo103/.MainGeetestActivity t278}
mResumedActivity: ActivityRecord{e99eb46 u0 com.example.learn_001_activity/.SixthActivity t383}
可以看出前台任务栈的taskAffinity值为com.example.learn_001_activity(另外2个是其他没关的程序的),它里面只有1个Activity
//SixthActivity中复写的onNewIntent方法:SixthActivity.onNewIntent()
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d(TAG, "onNewIntent, time = " + intent.getLongExtra("time", 0));
}
//Logcat中输出的结果
03-14 09:21:53.261 1798-1798/com.example.learn_001_activity D/SixthActivity: onPause
03-14 09:21:53.261 1798-1798/com.example.learn_001_activity D/SixthActivity: onNewIntent, time = 1521019313259
03-14 09:21:53.262 1798-1798/com.example.learn_001_activity D/SixthActivity: onResume
03-14 09:21:53.835 1798-1798/com.example.learn_001_activity D/SixthActivity: onPause
03-14 09:21:53.835 1798-1798/com.example.learn_001_activity D/SixthActivity: onNewIntent, time = 1521019313832
03-14 09:21:53.835 1798-1798/com.example.learn_001_activity D/SixthActivity: onResume
03-14 09:21:54.433 1798-1798/com.example.learn_001_activity D/SixthActivity: onPause
03-14 09:21:54.433 1798-1798/com.example.learn_001_activity D/SixthActivity: onNewIntent, time = 1521019314430
03-14 09:21:54.433 1798-1798/com.example.learn_001_activity D/SixthActivity: onResume
可以看出,Activity的确没有重新创建,只是暂停了一下,然后调用了onNewIntent,接着调用onResume就又继续了。
- (2)去掉android:launchMode="singleTask"后的SixthActivity,再执行上述操作:
Running activities (most recent first):
TaskRecord{5c0bdbe #384 A=com.example.learn_001_activity U=0 StackId=1 sz=5}
Run #7: ActivityRecord{48e5526 u0 com.example.learn_001_activity/.SixthActivity t384}
Run #6: ActivityRecord{f76fb50 u0 com.example.learn_001_activity/.SixthActivity t384}
Run #5: ActivityRecord{1afc095 u0 com.example.learn_001_activity/.SixthActivity t384}
Run #4: ActivityRecord{3363da1 u0 com.example.learn_001_activity/.SixthActivity t384}
Run #3: ActivityRecord{50cd266 u0 com.example.learn_001_activity/.MainActivity t384}
TaskRecord{5ac339a #299 A=com.android.gallery3d U=0 StackId=1 sz=1}
Run #2: ActivityRecord{4df2ed7 u0 com.android.gallery3d/.app.GalleryActivity t299}
TaskRecord{52714cb #278 A=com.example.demo103 U=0 StackId=1 sz=2}
Run #1: ActivityRecord{274a48f u0 com.example.demo103/.MainBindActivity t278}
Run #0: ActivityRecord{e70354a u0 com.example.demo103/.MainGeetestActivity t278}
可以看出前台任务栈的taskAffinity值为com.example.learn_001_activity(另外2个是其他没关的程序的),它里面有4个Activity
Running activities (most recent first):
TaskRecord{7e50489 #106 I=com.google.android.apps.nexuslauncher/.NexusLauncherActivity U=0 StackId=0 sz=1}
Run #1: ActivityRecord{b8d40e4 u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t106}
TaskRecord{9fbe4a8 #111 A=com.android.systemui U=0 StackId=0 sz=1}
Run #0: ActivityRecord{f63f216 u0 com.android.systemui/.recents.RecentsActivity t111}
还有这两个应该就是后台任务栈了,其taskAffinity值为com.google.android.apps.nexuslauncher和com.android.systemui
至于singleTask模式的Activity切换到栈顶会使在它之上的栈内的Activity出栈,这里就不演示了。
2.2 Activity的flags
- FLAG_ACTIVITY_NEW_TASK :指定"singleTask"启动模式,其效果和在XML中指定该启动模式相同。
- FLAG_ACTIVITY_SINGLE_TOP :指定"singleTask"启动模式,其效果和在XML中指定该启动模式相同。
- FLAG_ACTIVITY_CLEAR_TOP :具有此标记位的Activity,当它启动时,在同一个任务栈中所有位于它上面的Activity都要出栈,这个模式一般需要和FLAG_ACTIVITY_NEW_TASK 配合使用,在这种情况下,被启动的Activity的实例如果已经存在,那么系统就会调用它的onNewIntent。如果被启动的Activity采用standard模式启动,那么它连同连同它之上的Activity都要出栈,系统会创建新的Activity实例并放入栈顶。singleTask启动模式默认具有此标记位的效果。
- FLAG_ACTIVITY_EXCLUDE_FEOM_RECENTS :具有该标记的Activity不会出现在历史Activity的列表中,。等同于XML中指定Activity的属性android:excludeFromRecents="true"
2.3 IntentFilter的匹配规则
启动Activity分为2种:显式调用(明确指定被启动对象的组件信息:包括包名和类名)和隐式调用(不需要明确指定被启动对象的组件信息),如果二者共存则以显示调用为主。
关于隐式调用,即需要Intent能够匹配目标组件的IntentFilter中所设置的过滤信息,如果不匹配将无法启动目标Activity。IntentFilter中的过滤信息有action、category、data。
//过滤规则的示例:
<activity
android:name=".SeventhActivity"
android:launchMode="singleTask"
android:taskAffinity="com.example.learn_001_activity">
<intent-filter>
<action android:name="com.example.learn_001_activity.c"/>
<action android:name="com.example.learn_001_activity.d"/>
<category android:name="com.example.category.c"/>
<category android:name="com.example.category.d"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
一个Activity中可以有多个intent-filter(匹配列表,其中的过滤信息有action、category、data(可以有多个)),为了匹配过滤列表,一个Intent需要同时匹配列表中的action、category、data信息,才能成功启动目标Activity。
(1) action 的匹配规则:
action是一个字符串,系统预定义了一些action,同时我们可以在应用中定义自己的action。action的匹配规则是Intent中的action必须和过滤规则中的action匹配(区分大小写,字符串值完全一样)。一个过滤规则(即intent-filter、匹配列表)可以有多个action,那么只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功。针对上面例子的过滤规则,只要我们的Intent中action值为"com.example.learn_001_activity.c"或者"com.example.learn_001_activity.d"就能成功匹配,注意,Intent中如果没有指定action,那么匹配失败。(2) category 的匹配规则:
category是一个字符串,系统预定义了一些category,同时我们可以在应用中定义自己的category。与action不同的是,category要求Intent可以没有category(系统在startActivity或startActivityForResult的时候默认Ietent加上"android.intent.category.DEFAULT"),但是如果你一旦有了category,不管有几个,每个都要能过和过滤规则中的任何一个category相同。针对上面例子的过滤规则,只要我们写下面的Intent:intent.addcategory("com.example.learn_001_activity.c"或intent.addcategory("com.example.learn_001_activity.d")就能成功匹配。(3) data 的匹配规则:
data的匹配规则和action类似,如果过滤规则中定义了data,那么Intent中必须也要定义可匹配的data。
//data的语法:
<data
android:scheme="string"
android:host="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:mimeType="string"/>
data由两部分组成:mimeType和URI。mimeType指媒体类型,比如image/jepg、audio/mpeg4-generic和video/*等,可以表示图片、文本、视频等不同的媒体格式,而URI中包含的数据就比较多了,下面是URI的结构:
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
//下面两个例子:
content://com.example.project:200/folder/subfolder/etc
http://www/baidu.com:80/search/info
至于每个数据的含义如下:
1:Scheme:URI的模式,比如http、file、content等,如果URI中没有指定scheme,那么整个URI的其他参数无效,这也意味着URI是无效的。
2:Host:URI的主机名,比如www.baidu.com,如果host未指定,那么整个URI中的其他参数无效,也意味着URI是无效的。
3:Port:URI的端口号,比如80,仅当URI中制定了scheme和host参数的时候port参数才是有意义的。
4:path、pathPattern和pathPrefix;这3个参数表述路径信息。path表示完整的路径信息;pathPattern也表示完整的路径信息,但是它里面可以包含通配符“ * ”,“ * ”表示0个或多个任意字符,注意,由于正则表达式的规范,如果想表示真实的字符串,“ * ”要写成“ \\* ”,“ \ ”要写成“ \\\\ ”;pathPrefix表示路径的前缀信息。
前面说到,data的匹配规则和action类似,它要求Intent中必须含有data数据,并且data数据能够完全匹配过滤规则中的某一个data。下面分情况说明:
(1) 如下过滤规则:
<intent-filter>
<data android:mimeType="image/*"/>
.....
</intent-filter>
这种规则制定了媒体类型为所有类型的图片,那么Intent中的mimeType属性必须为“image/”才能匹配,这种情况下虽然过滤规则中没有指定URI,但是却有默认值:content和file。也就是说,虽然没有指定URI,但是Intent中的URI部分的schema必须为content或file才能匹配*。为了匹配(1)中规则,可以写出如下示例(如果要为Intent指定完整的data,必须调用setDataAndType方法,不能先调用setData再调用setType,因为这两个方法会彼此清除对方的值。):
intent.setDataAndType(Uri.parse("file://abc"), "image/png");
(2)如下过滤规则:
<intent-filter>
<data android:mimeType="video/mpeg" android:scheme="http" .../>
<data android:mimeType="audio/mpeg" android:scheme="http" .../>
.....
</intent-filter>
这种规则指定了两组data规则,且每个data都指定了完整的属性值,既有URI又有mimeType。为了匹配(1)中规则,可以写出如下示例:
intent.setDataAndType(Uri.parse("http://abc"), "video/mpeg");
或者
intent.setDataAndType(Uri.parse("http://abc"), "audio/mpeg");
对于一开始给出的intent-filter的示例,现在我们给出完全匹配它的Intent:
Intent intent = new Intent("com.example.learn_001_activity.c");
intent.addCategory("com.example.category.c");
intent.setDataAndType(Uri.parse("file://abc"), "text/plain"); //手机里面要有名字是abc的file文件...
startActivity(intent);
上面说到,URI的schema是由默认值的(content和file),如果把上面的intent.setDataAndType(Uri.parse("file://abc"), "text/plain");改成intent.setDataAndType(Uri.parse("http://abc"), "text/plain");,打开Activity的时候就会报错,提示无法找到Activity。另外Intent-filter的匹配规则对于Service和BroadcastReceiver也是同样的道理,不过系统对于Service的建议是尽量使用显示调用方式来启动服务。
最后,当我们想通过隐式方式启动一个Activity的时候,可以做一下判断,看是否有Activity能够匹配我们的隐式Intent,判断方法有2种:采用PackageManager的resolveActivity方法或者Intent的resolveActivity方法,如果找不到匹配的Activity就会返回null,通过判断返回值就可以规避上述错误了。另外,PackageManager还提供了queryIntentActivities方法:返回所有成功匹配的Activity信息。
//PackageManager类中:
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.iintent.category.DEFAULT"/>这个category的Activity。使用这个标记位的意义在于,只要上述两个方法不返回null,那么startActivity一定可以成功,如果不用这个标记位,就可以吧intent-filter中category不含DEFAULT的那些Activity匹配出来,从而导致startActivity可能失败。因为不含有DEFAULT这个category的Activity是否无法接收隐式Intent的。
在action和category中有一类action和category比较重要:
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
这二者的共同作用是来标明这是一个入口Activity并且会出现在系统的应用列表中。另外,针对Service和BroadcastReceiver,PackageManager同样提供了类似的犯法去获取成功匹配的组件信息。