一.Activity的启动模式
要设置Activity的启动模式,只需要在manifest中设置android:launchMode属性即可,分别有:standard、singleTop、singleTask、singleInstance。
当启动一个Activity时,系统会把此Activity的实例添加到一个以栈为数据结构的任务栈容器中去,栈的表现形式为后进先出。当启动一个应用程序后,系统会创建一个task,来放置根Activity。默认情况下一个Activity启动另一个Activity后,这两个Activity是在同一个task中的,后者被压入前者的所在的任务栈。如果在manifest中设置了android:taskAffinity属性,参数值就是这个Activity所需任务栈的名称。默认的,在没有指定任何android:taskAffinity属性时,应用程序所有Activity任务栈的名称就是包名,一般这个属性和singleTask配合或者与android:allowTaskReparenting属性配合使用使用才会有效果,对于android:allowTaskReparenting属性用的实在是太少了,就不再说明,自行百度。下面用代码来看看具体的效果。
我用自定义的一个ActivityStackManager类来对具体的Activity进行管理,方便打印查看。代码其实很简单,用Stack类来对Activity进行栈式的管理,并提供出栈、入栈、打印的功能。
class ActivityStackManager private constructor(){
private var activityStack = Stack<Activity>()
companion object{
val instance = MySingle.mySingle
}
private object MySingle{
val mySingle = ActivityStackManager()
}
fun pushActivity(activity: Activity){
activityStack.push(activity)
}
fun popActivity() : Activity = activityStack.pop()
fun printStack(){
for (ac in activityStack){
println("当前栈中有:" + ac.localClassName)
}
println("--------------------------------")
}
}
在
BaseActivity里的onCreate和onDestroy里分别调用入栈和出栈的方法,并且直接打印输出当前栈里包含有哪些Activity,代码也很简单。
abstract class BaseActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityStackManager.instance.pushActivity(this)
printActivityStack()
}
override fun onDestroy() {
super.onDestroy()
ActivityStackManager.instance.popActivity()
printActivityStack()
}
private fun printActivityStack() {
ActivityStackManager.instance.printStack()
}
}
下面是点击
MainActivity跳转到SecondActivity,布局文件都很简单,里面只有一个按钮,就不贴代码了。
class MainActivity : BaseActivity() {
private var button: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.button1)
button?.setOnClickListener {
val intent = Intent(this,SecondActivity::class.java)
startActivity(intent)
}
}
}
让
SecondActivity启动它自身,并重写了onNewIntent方法
class SecondActivity : BaseActivity() {
private var button : Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
button = findViewById(R.id.secondBtn)
button?.setOnClickListener {
val intent = Intent(this,SecondActivity::class.java)
startActivity(intent)
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
println("SecondActivity----onNewIntent()方法被调用")
}
}
1.standard模式
Android默认的Activity启动模式,也可以显式的在Manifest中设置此启动模式。每次启动一个Activity都会创建一个新的实例,不管这个实例是否已经存在。并且谁启动了这个Activity,这个Activity就运行在启动它的那个Activity所在的栈中。
当
SecondActivity以standard模式启动时,点击MainActivity按钮启动SecondActivity,并且点击SecondActivity启动自身。

当多点击几次重复启动SecondActivity后可以看到栈中有重复的实例,此时并不会回调onNewIntent方法。

2.singleTop
栈顶复用模式。在这种模式下,如果当前Activity已经在栈顶后,再次启动自身的话,并不会创建新的实例,而是会回调onNewIntent方法;如果当前Activity不在栈顶或者不存在实例时,就会新创建一个实例压入栈中。
当
SecondActivity以singleTop模式启动时,点击MainActivity按钮启动SecondActivity,并且再多次点击SecondActivity启动自身。

可以看到,当SecondActivity已经处于栈顶后,没有创建新的实例入栈,而是回调onNewIntent方法
修改此时
SecondActivity的点击事件,使其跳转到ThirdActivity,ThirdActivity以standard模式启动,并点击里面的按钮跳转到MainActivity,然后再点击MainActivity按钮跳转到SecondActivity。就是MainActivity -> SecondActivity -> ThirdActivity -> MainActivity -> SecondActivity以这样的顺序。

可以看到,还是重复创建了SecondActivity的实例,并且没有回调onNewIntent方法。以上都是在同一个任务栈中测试。
3.singleTask
栈内复用模式。是一种单实例的模式,如果栈中存在了Activity的实例,当再次以这种模式启动这个Activity后,不会再次创建新的实例,会回调onNewIntent方法;如果栈中不存在Activity的实例,会创建新的实例压入栈中。
当
SecondActivity以singleTask模式启动时,还是以上一个singleTop的例子,以MainActivity -> SecondActivity -> ThirdActivity -> MainActivity -> SecondActivity这样的顺序,来看看。

上面的输出是从
ThirdActivity -> MainActivity的所有输出,此时点击MainActivity按钮启动SecondActivity后,会得到以下输出

可以看出,此时并没有重新创建
SecondActivity的实例,并且回调了onNewIntent方法,注意到了SecondActivity上面的MainActivityh和ThirdActivity不存在于栈中了,这是因为singleTask模式具有clearTop的效果,让SecondActivity之上的Activity全部出栈。需要注意singleTask和singleTop的区别,singleTop只是在栈顶的时候不会重复创建实例,如果不在栈顶,还是会创建新的实例入栈,而singleTask是不管是否在栈顶,只要存在于栈内之中,就不会创建新的实例,并且让之上的Activity全部出栈。以上都是在同一个任务栈中测试。
4.singleInstance
单实例模式。它是singleTask的加强型,除了具有singleTask所有特性外,那就是以此种模式启动的Activity,只能单独的位于一个任务栈中。比如SecondActivity是以这种模式启动,系统会为它创建一个新的任务栈,并把SecondActivity压入栈中,SecondActivity会独自存在于这个任务栈中,并且由于栈内复用的模式,后续的重复启动,都不会创建新的实例。
当
SecondActivity以singleInstance模式启动时,指定android:taskAffinity=":hello",前面用冒号表示使用当前包名,任务栈的全称是com.test.launchmode:hello,现在用MainActivity启动SecondActivity,并SecondActivity启动自身。 用adb shell dumpsys activity命令查看任务栈:

从图中可以看到,存在2哥任务栈,一个是以当前包名作为任务栈com.test.launchmode,里面包含一个MainActivity的实例;一个是com.test.launchmode:hello任务栈,里面包含一个SecondActivity的实例。
当还是以MainActivity -> SecondActivity -> ThirdActivity -> MainActivity -> SecondActivity这样的顺序来依次启动,SecondActivity以singleInstance启动,并设置android:taskAffinity=":hello任务栈名称后,得到以下输出:

可以看到根据栈内复用模式,在com.test.launchmode:hello任务栈中,只有一个SecondActivity的实例,而以包名com.test.launchmode任务栈中,存在有MainActivity和ThirdActivity的实例。
二.Activity的Flag
虽然Activity中的Flag有很多,介绍几个比较常见的,其余的可以自行去网上查看相关文档。
Intent.FLAG_ACTIVITY_SINGLE_TOP
这个Flag的作用是为Activity指定singleTop为启动模式,效果和在manifest中指定android:launchMode属性相同。
Intent.FLAG_ACTIVITY_NEW_TASK
这个Flag的作用是为Activity指定singleTask为启动模式,效果和在manifest中指定android:launchMode属性相同。
Intent.FLAG_ACTIVITY_CLEAR_TOP
这个Flag的作用是当他被启动时,如果此Activity的实例已经存在于任务栈中,那么所有在它上面的Activity都要出栈。这个Flag一般和Intent.FLAG_ACTIVITY_NEW_TASK配合使用。
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
这个Flag的作用是此Activity不会出现在历史Activity列表中,效果和在manifest中指定android:excludeFromRecents="true"属性相同。
三.IntentFilter的匹配规则
启动一个Activity分为隐式启动和显式启动,显式启动就是需要明确的指定被调用组件的相关信息,比如包名和类名,而隐式启动则不需要明确指定。原则上一个Intent不应该既是隐式也是显式调用,如果同时存在的话,以显式调用为主。显式调用不做说明,主要看一下隐式调用。隐式调用需要Intent匹配目标组件中的IntentFilter过滤信息,如果不匹配则无法启动相关组件。IntentFilter过滤信息包含action、category、data。下面是一个在manifest中设置的过滤信息:
<activity android:name="com.test.launchmode.TargetActivity">
<intent-filter>
<action android:name="com.test.myAction"/>
<action android:name="com.test.target.myAction"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="com.test.myCategory"/>
<category android:name="com.test.target.myCategory"/>
<data android:mimeType="image/png"/>
</intent-filter>
</activity>
要隐式启动一个Activity,需要同时匹配过滤类别中的action、category、data信息,否则匹配失败就无法启动。一个过滤列表中action、category、data可以有多个,所有的action、category、data分别构成了不同的类别,同一类别中的信息共同约束当前类别的匹配过程。一个Intent只有同时匹配action类别、category类别、data类别,才算完全匹配,然后才能启动目标Activity。另外,一个Activity可以有多个intent-filter,一个Intent只要匹配任何一组intent-filter即可成功启动目标Activity。
<activity android:name="com.test.launchmode.TargetActivity">
<intent-filter>
<action android:name="com.test.myAction"/>
<action android:name="com.test.target.myAction"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="com.test.myCategory"/>
<category android:name="com.test.target.myCategory"/>
<data android:mimeType="image/png"/>
</intent-filter>
<intent-filter>
<action android:name="com.test.a"/>
<action android:name="com.test.b"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="com.test.c"/>
<category android:name="com.test.d"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
1.action的匹配规则
action是一个字符串,系统已经定义了一些action,我们也可以自定义自己的action,上面就是自定义的action。action是区分大小写的所以com.test.a和com.test.A是2个不同的action,action的匹配规则是Intent中的action要和过滤规则中的action值完全一样。过滤规则可以有多个action,那么只要Intent中的action能够和规则中的任何一个action相同即匹配成功。所以针对上面第一个过滤规则,我们只需要设置Intent中的action值为com.test.myAction或者com.test.target.myAction都能匹配成功。注意,如果Intent中没有设置action,那么匹配失败。
2.category的匹配规则
category和action一样,也是一个字符串,系统也定义了一些category,我们也可以自定义自己的category。category和action的匹配规则不同,它要求如果Intent中含有category,那么所有的category必须和过滤规则的中其中一个category相同,通俗讲就是,Intent中的category,必须是规则中的category的子集。当然,也可以不设置Intent中的category,这时仍然能够匹配成功,为什么?这是因为系统在调用startActivity或者startActivityForResult的时候,默认会为Intent加上android:name="android.intent.category.DEFAULT",所以能够匹配成功。如果为了能够接收隐式调用,则必须在intent-filter中指定android:name="android.intent.category.DEFAULT"这个category。
3.data的匹配规则
data的匹配规则和action类似,如果过滤规则定义了data,那么Intent也必须要设置可匹配的data。先来了解以下data的结构。
<data android:scheme=""
android:host=""
android:port=""
android:path=""
android:pathPattern=""
android:mimeType="" />
data主要有2部分组成:URI和mimeType。mimeType是值媒体类型,比如text/html、application/x-www-form-urlencoded、video/mpeg等可以表示图片、文本、视频等不同的媒体格式。UIR包含的数据较多,格式为:<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]。举个简单的栗子:https://www.google.com、ftp://127.0.0.1:8080/resource/main.html等等。下面分别说明各个代表的含义。
scheme URI的模式,例如http、file、content等,如果未指定URI的scheme,整个URI是无效的。
host URI的主机名,例如www.baidu.com,如果未指定URI的host,整个URI是无效的。
port URI的端口号,例如8080,只有当URI中指定了scheme和host,port参数才有意义。
path/pathPattern/pathPrefix 表示URI的路径信息,其中path表示完整的路径信息,pathPattern也表示完整的路径信息,但是它可以包含通配符“*”,pathPrefix表示路径的前缀信息。
前面说到,data的匹配规则和action类型,它也要求Intent中必须包含data数据,并且data数据可以完全匹配规则中的某个data,通俗的讲就是Intent中的data必须是规则中data的子集。考虑下面的过滤规则:
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<data android:mimeType="video/mpeg"/>
</intent-filter>
这种规则指定了媒体类型为视频格式,那么Intent中的mimeType属性必须为"video/mpeg才能匹配,虽然过滤规则中没有指定URI,但是有默认值,URI的默认值是content和file,也就是说,虽然没有URI,但是Intent中额URI部分的scheme必须为content或者file才能匹配。还有一点需要注意的是,如果要为Intent指定完整的data,需要要调用intent.setDataAndType方法,不能先调用setData再调用setType,因为这两个方法会彼此清除对方发值。


对于上面写的过滤规则,我们只需要写出intent.setDataAndType(Uri.parse("file://abc"),"video/mpeg")就可匹配。
对于刚开始写的2组过滤规则,我们只需要能匹配其中一组,即可成功启动目标Activity,写出如下匹配内容:
try{
val intent = Intent()
intent.action = MY_ACTION
intent.addCategory(MY_CATEGORY)
intent.setDataAndType(Uri.parse("content://abc"),"image/png")
intent.putExtra("XXX","hello target")
startActivity(intent)
println("找到目标Activity,启动成功")
}catch(e:Exception){
println("没有找到目标Activity,启动失败")
}
具体代码可参考demo:github.com/leewell5717…
activity_launch_mode工程代码
四、参考
《Android》开发艺术探索