读书笔记-艺术探索-Activity的启动模式

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同样提供了类似的犯法去获取成功匹配的组件信息。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容