四大组件——Activity(2)

293 阅读11分钟

一.Activity的启动模式

要设置Activity的启动模式,只需要在manifest中设置android:launchMode属性即可,分别有:standardsingleTopsingleTasksingleInstance

当启动一个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里的onCreateonDestroy里分别调用入栈和出栈的方法,并且直接打印输出当前栈里包含有哪些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所在的栈中。

SecondActivitystandard模式启动时,点击MainActivity按钮启动SecondActivity,并且点击SecondActivity启动自身。

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

2.singleTop

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

SecondActivitysingleTop模式启动时,点击MainActivity按钮启动SecondActivity,并且再多次点击SecondActivity启动自身。

可以看到,当SecondActivity已经处于栈顶后,没有创建新的实例入栈,而是回调onNewIntent方法

修改此时SecondActivity的点击事件,使其跳转到ThirdActivity,ThirdActivitystandard模式启动,并点击里面的按钮跳转到MainActivity,然后再点击MainActivity按钮跳转到SecondActivity。就是MainActivity -> SecondActivity -> ThirdActivity -> MainActivity -> SecondActivity以这样的顺序。

可以看到,还是重复创建了SecondActivity的实例,并且没有回调onNewIntent方法。以上都是在同一个任务栈中测试

3.singleTask

栈内复用模式。是一种单实例的模式,如果栈中存在了Activity的实例,当再次以这种模式启动这个Activity后,不会再次创建新的实例,会回调onNewIntent方法;如果栈中不存在Activity的实例,会创建新的实例压入栈中。

SecondActivitysingleTask模式启动时,还是以上一个singleTop的例子,以MainActivity -> SecondActivity -> ThirdActivity -> MainActivity -> SecondActivity这样的顺序,来看看。

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

可以看出,此时并没有重新创建SecondActivity的实例,并且回调了onNewIntent方法,注意到了SecondActivity上面的MainActivityh和ThirdActivity不存在于栈中了,这是因为singleTask模式具有clearTop的效果,让SecondActivity之上的Activity全部出栈。需要注意singleTasksingleTop的区别,singleTop只是在栈顶的时候不会重复创建实例,如果不在栈顶,还是会创建新的实例入栈,而singleTask是不管是否在栈顶,只要存在于栈内之中,就不会创建新的实例,并且让之上的Activity全部出栈。以上都是在同一个任务栈中测试

4.singleInstance

单实例模式。它是singleTask的加强型,除了具有singleTask所有特性外,那就是以此种模式启动的Activity,只能单独的位于一个任务栈中。比如SecondActivity是以这种模式启动,系统会为它创建一个新的任务栈,并把SecondActivity压入栈中,SecondActivity会独自存在于这个任务栈中,并且由于栈内复用的模式,后续的重复启动,都不会创建新的实例。

SecondActivitysingleInstance模式启动时,指定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这样的顺序来依次启动,SecondActivitysingleInstance启动,并设置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过滤信息包含actioncategorydata。下面是一个在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,需要同时匹配过滤类别中的actioncategorydata信息,否则匹配失败就无法启动。一个过滤列表中actioncategorydata可以有多个,所有的actioncategorydata分别构成了不同的类别,同一类别中的信息共同约束当前类别的匹配过程。一个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,上面就是自定义的actionaction是区分大小写的所以com.test.acom.test.A是2个不同的actionaction的匹配规则是Intent中的action要和过滤规则中的action值完全一样。过滤规则可以有多个action,那么只要Intent中的action能够和规则中的任何一个action相同即匹配成功。所以针对上面第一个过滤规则,我们只需要设置Intent中的action值为com.test.myAction或者com.test.target.myAction都能匹配成功。注意,如果Intent中没有设置action,那么匹配失败。

2.category的匹配规则

categoryaction一样,也是一个字符串,系统也定义了一些category,我们也可以自定义自己的categorycategoryaction的匹配规则不同,它要求如果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部分组成:URImimeTypemimeType是值媒体类型,比如text/htmlapplication/x-www-form-urlencodedvideo/mpeg等可以表示图片、文本、视频等不同的媒体格式。UIR包含的数据较多,格式为:<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]。举个简单的栗子:https://www.google.comftp://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的默认值是contentfile,也就是说,虽然没有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》开发艺术探索