《第一行代码》读书笔记-第二章04 活动的启动模式

task

task是在程序运行时,只针对activity的概念。说白了,task是一组相互关联的activity的集合,它是存在于framework层的一个概念,控制界面的跳转和返回。这个task存在于一个称为back stack的数据结构中,也就是说,framework是以栈的形式管理用户开启的activity。这个栈的基本行为是,当用户在多个activity之间跳转时,执行压栈操作,当用户按返回键时,执行出栈操作。

举例来说,如果应用程序中存在A,B,C三个activity,当用户在Launcher或Home Screen点击应用程序图标时,启动主Activity A,接着A开启B,B开启C,这时栈中有三个Activity,并且这三个Activity默认在同一个任务(task)中,当用户按返回时,弹出C,栈中只剩A和B,再按返回键,弹出B,栈中只剩A,再继续按返回键,弹出A,任务被移除。

task是可以跨应用的,这正是task存在的一个重要原因。有的Activity,虽然不在同一个app中,但为了保持用户操作的连贯性,把他们放在同一个任务中。例如,在我们的应用中的一个Activity A中点击发送邮件,会启动邮件程序的一个Activity B来发送邮件,这两个activity是存在于不同app中的,但是被系统放在一个任务中,这样当发送完邮件后,用户按back键返回,可以返回到原来的Activity A中,这样就确保了用户体验。

process

process一般翻译成进程,进程是操作系统内核中的一个概念,表示直接受内核调度的执行单位。在应用程序的角度看,我们用java编写的应用程序,运行在dalvik虚拟机中,可以认为一个运行中的dalvik虚拟机实例占有一个进程,所以,在默认情况下,一个应用程序的所有组件运行在同一个进程中。但是这种情况也有例外,即,应用程序中的不同组件可以运行在不同的进程中。只需要在manifest中用process属性指定组件所运行的进程的名字。

<activity android:name=".MyActivity" android:label="@string/app_nam"
    android:process=":remote">
</activity>

这样的话这个activity会运行在一个独立的进程中。

Activity四种启动模式详解

启动模式一共有四种,分别是standardsingleTopsingleTasksingleInstance , 可以在AndroidManifest.xml 中通过给<activity> 标签指定android:launchMode 属性来选择启动模式。

standard

standard 是活动默认的启动模式。对于使用standard 模式的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。如:在A Activity中启动A,会新建一个A 实例,在返回栈中存在两个A。

singleTop

在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。并且会调用该实例的 onNewIntent()方法将Intent对象传递到这个实例中。

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    Toast.makeText(FirstActivity.this, "调用了onNewIntent()方法" + intent.getStringExtra("name"), Toast.LENGTH_SHORT).show();
}

如果以singleTop模式启动的activity的一个实例已经存在与任务桟中,但是不在桟顶,那么它的行为和standard模式相同,也会创建多个实例。

singleTask

注意:此处讨论的均是以startActivity(intent)的方式启动意图,如果以startActivityForResult(intent, 0)的方式启动,在AndroidManifest.xml配置taskAffinity属性无效,不会开启一个新的task。测试代码同下,日志打印出相同的ID。

谷歌的官方文档上称,如果一个activity的启动模式为singleTask,那么系统总会在一个新任务的最底部(root)启动这个activity,并且被这个activity启动的其他activity会和该activity同时存在于这个新任务中。如果系统中已经存在这样的一个activity则会重用这个实例,并且调用他的onNewIntent()方法。并把在这
个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。即,这样的一个activity在系统中只会存在一个实例。

其实官方文档中的这种说法并不准确,启动模式为singleTask的activity并不会总是开启一个新的任务。详情请参考 解开Android应用程序组件Activity的"singleTask"之谜

验证启动singleTask模式的activity时是否会创建新的任务

转自:Android中Activity四种启动模式和taskAffinity属性详解
以下为验证示例AndroidTaskTest。这个实例中有三个Activity,分别为:MainActivity,SecondActivity和ThirdActivity。以下为这个示例的manifest文件。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jg.zhang.androidtasktest"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />

    <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
        <activity  android:label="@string/app_name"
            android:name="com.jg.zhang.androidtasktest.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!--android:taskAffinity="com.jg.zhang.androidtasktest.second"
            android:alwaysRetainTaskState="true"
            android:allowBackup="true" -->

         <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"
             android:launchMode="singleTask">
            <intent-filter >
                <action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>

         <activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"
            android:label="@string/app_name" >
        </activity>
    </application>

</manifest>

由此可见,MainActivity和ThirdActivity都是标准的启动模式,而SecondActivity的启动模式为singleTask

三个Activity的界面,很简单,在MainActivity中点击按钮启动SecondActivity,在SecondActivity中点击按钮启动ThirdActivity。

以下为这三个activity的主要代码:

MainActivity

public class MainActivity extends Activity {

    private static final String ACTIVITY_NAME = "MainActivity";
    private static final String LOG_TAG = "xxxx";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);

                startActivity(intent);
            }
        });

        int taskId = getTaskId();
        Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);
    }

SecondActivity

public class SecondActivity extends Activity {
    private static final String ACTIVITY_NAME = "SecondActivity";
    private static final String LOG_TAG = "xxxx";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        findViewById(R.id.button2).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                    Intent intent = new Intent(SecondActivity.this, ThirdActivity.class);
                    startActivity(intent);
            }
        });

        int taskId = getTaskId();
        Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);

    }

ThirdActivity

public class ThirdActivity extends Activity {

    private static final String ACTIVITY_NAME = "ThirdActivity";
    private static final String LOG_TAG = "xxxx";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        int taskId = getTaskId();
        Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);
    }

以上三个activity只列出了onCreate()方法中的内容,实现的逻辑为在MainActivity中点击按钮启动SecondActivity,在SecondActivity中点击按钮启动ThirdActivity。并且在onCreate方法中会以log的形式打印出当前activity所属的任务(Task)的Id。

现在执行以下操作,运行该示例,并且点击MainActivity界面中的按钮,开启SecondActivity。在该示例中SecondActivity的启动模式为singleTask。按照官方文档的说法,SecondActivity会在一个新的任务中开启。但是查看打印出的log,发现MainActivity和SecondActivity所在的任务的Id相同

在命令行中执行以下命令 adb shell dumpsys activity , 有以下输出:

TaskRecord{412ded08 #8 A com.jg.zhang.androidtasktest}
    Run #2: ActivityRecord{412c91e8 com.jg.zhang.androidtasktest/.SecondActivity}
    Run #1: ActivityRecord{412c08a0 com.jg.zhang.androidtasktest/.MainActivity}

所以,和官方文档表述的不同,MainActivity和SecondActivity是启动在同一个任务中的。其实,把启动模式设置为singleTask,framework在启动该activity时只会把它标示为可在一个新任务中启动,至于是否在一个新任务中启动,还要受其他条件的限制。现在在SecondActivity增加一个taskAffinity属性,如下所示:

<activity android:name="com.jg.zhang.androidtasktest.SecondActivity"
    android:launchMode="singleTask"
    android:taskAffinity="com.jg.zhang.androidtasktest.second">
   <intent-filter >
       <action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>
       <category android:name="android.intent.category.DEFAULT"/>
   </intent-filter>
</activity>

重新运行该示例,执行相同的操作,即:点击MainActivity界面中的按钮,开启SecondActivity,并且点击SecondActivity中的按钮,启动ThirdActivity,log中输出的内容为:

xxxx    MainActivity所在的任务id为:5
xxxx    SecondActivity所在的任务id为:6
xxxx    ThirdActivity所在的任务id为:6

在命令行中执行adb shell dumpsys activity命令,有以下输出:

TaskRecord{411e6a88 #6 A com.jg.zhang.androidtasktest.second}
    Run #3: ActivityRecord{411c8ea0 com.jg.zhang.androidtasktest/.ThirdActivity}
    Run #2: ActivityRecord{412bc870 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{412ece18 #5 A com.jg.zhang.androidtasktest}
    Run #1: ActivityRecord{412924c0 com.jg.zhang.androidtasktest/.MainActivity}

由此可见,MainActivity和SecondActivity运行在不同的任务中了,并且被SecondActivity启动的ThirdActivity和SecondActivity运行在同一个任务中。

taskAffinity

在这里便引出了manifest文件中的一个重要属性,taskAffinity。在官方文档中可以得到关于taskAffinity的以下信息

  1. taskAffinity表示当前activity具有亲和力的一个任务,大致可以这样理解,这个 taskAffinity表示一个任务,这个任务就是当前activity所在的任务。
  2. 在概念上,具设置了相同taskAffinity属性的activity,属于同一个任务。
  3. 一个任务的affinity决定于这个任务的根activity(root activity)的taskAffinity。(如:上面,被SecondActivity启动的ThirdActivity和SecondActivity运行在同一个任务中)
  4. 这个属性决定两件事:①当activity被re-parent时,它可以被re-paren哪个任务中;②当activity以FLAG_ACTIVITY_NEW_TASK标志启动时,它会被启动到哪个任务中。
  5. 默认情况下,一个应用中的所有activity具有相同的taskAffinity,即应用程序的包名。我们可以通过设置不同的taskAffinity属性给应用中的activity分组,也可以把不同的 应用中的activity的taskAffinity设置成相同的值。
  6. 为一个activity的taskAffinity设置一个空字符串,表明这个activity不属于任何task。

在MainActivity启动SecondActivity时,发现启动模式为singleTask,会设定他的启动标志FLAG_ACTIVITY_NEW_TASK

其实framework中对任务和activity的调度是很复杂的,尤其是把启动模式设为singleTask或者以FLAG_ACTIVITY_NEW_TASK标志启动时。所以,在使用singleTaskFLAG_ACTIVITY_NEW_TASK时,要仔细测试应用程序。这也是官方文档上的建议,make sure to test the usability of the activity during launch。

不同的应用中的activity的taskAffinity设置成相同的值

可以把不同的应用中的activity的taskAffinity设置成相同的值,这样的话这两个activity虽然不在同一应用中,却会在运行时分配到同一任务中

这样如果同时开启两个应用,应用中有的Activity设置相同的taskAffinity。启动一个应用跳转到该Activity,按Home键退出,再启动另一个应用跳转到相应的Activity,点击Back键,将会退回到上一个应用中去。

总结

设置了"singleTask"启动模式的Activity的特点:

  1. 设置了"singleTask"启动模式的Activity,它在启动的时候,会先在系统中查找属性值affinity等于它的属性值taskAffinity的任务存在;如果存在这样的任务,它就会在这个任务中启动,否则就会在新任务中启动。因此,如果我们想要设置了"singleTask"启动模式的Activity在新的任务中启动,就要为它设置一个独立的taskAffinity属性值。

  2. 如果设置了"singleTask"启动模式的Activity不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的Activity实例,如果存在,就会把位于这个Activity实例上面的Activity全部结束掉,即最终这个Activity实例会位于任务的堆栈顶端中。

singleInstance

总是在新的任务中开启,并且这个新的任务中有且只有这一个实例,也就是说被该实例启动的其他activity会自动运行于另一个任务中。当再次启动该activity的实例时,会重用已存在的任务和实例。并且会调用这个实例的onNewIntent()方法,将Intent实例传递到该实例中。和singleTask相同,同一时刻在系统中只会存在一个这样的Activity实例。

想象以下场景,假设我们的程序中有一个活动是允许其他程序调用的,如果我们想实现其他程序和我们的程序可以共享这个活动的实例,应该如何实现呢?使用前面三种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个活动在不同的返回栈中入栈时必然是创建了新的实例。而使用singleInstance 模式就可以解决这个问题,在这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都共用的同一个返回栈,也就解决了共享活动实例的问题。

修改上面的例子,

android:name=".SecondActivity"
android:launchMode="singleInstance"

在 MainActivity 界面点击按钮进入到 SecondActivity , 然后在SecondActivity 界面点击按钮进入到 ThirdActivity 。

xxxx    MainActivity所在的任务id为:10
xxxx    SecondActivity所在的任务id为:11
xxxx    ThirdActivity所在的任务id为:10

参考

  1. Android中Activity四种启动模式和taskAffinity属性详解
  2. 解开Android应用程序组件Activity的"singleTask"之谜

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《第一行代码》读书笔记-第二章04 活动的启动模式

文章字数:3.4k

本文作者:Bin

发布时间:2016-05-14, 13:25:01

最后更新:2019-08-06, 00:49:54

原始链接:http://coolview.github.io/2016/05/14/Android/%E3%80%8A%E7%AC%AC%E4%B8%80%E8%A1%8C%E4%BB%A3%E7%A0%81%E3%80%8B%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0-%E7%AC%AC%E4%BA%8C%E7%AB%A004-%E6%B4%BB%E5%8A%A8%E7%9A%84%E5%90%AF%E5%8A%A8%E6%A8%A1%E5%BC%8F/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录