Activity四种启动模式 图文详解:standard, singleTop, singleTask 以及 singleInstance

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0520/2897.html


启动模式

启动模式是什么

有这样的场景:

  • 当我们使用App的时候,呈现出一个Activity,按下返回键(不考虑重写返回键事件),常常就回退到上一个打开的Activity或者退出App。
//重写返回按键事件
public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
            // 这里重写返回键
            return true;
        }
        return false;
    }
  • 几个应用之间,例如我的App要使用拍照功能,我需要调用系统的相机App,这分明就是两个不同的应用程序,分别运行在不同的进程,但是当我调用完成相机后,按下返回键可以返回我的App
//调用相机
private void openCamera() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        uri = PhotoUtil.createImageFile();
        if (takePictureIntent.resolveActivity(mContext.getPackageManager()) != null) {// 相机被卸载时不会崩溃
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAMERA);
        }
    }
还有例如在我们的App打开浏览器、微博之类的应用,然后跳转到浏览器,使用完成浏览器的功能,不断按下返回键,可以回到我们的应用。而不是像点击桌面上浏览器图标启动浏览器那样,不断返回键,最后回到的是桌面。
  • 生成重复页面。
    以前遇到过这样的Bug:
    1. 消息推送,通知栏弹出Notification,点击Notification跳转到指定Activity,但是如果我现在页面就停留在那个指定的Activity,会再次打开我当前的Activity,这样返回的时候回退的页面和当前页面一样,感官上就会很奇怪。
    2. 登录的时候,登录成功跳转到主页,按下两次登录按钮,生成了两个主页。一些有启动延迟的页面(往往是动画,网络造成)也会有这样的情况。

为什么要研究启动模式

  1. 有时候我们的App需要生成给其他App调用的Activity,例如浏览器应用,照相机应用
  2. 解决生成重复页面等等Bug
  3. 任务栈过深的时候,避免一直按返回键也退不回想要的页面

任务栈

任务栈Task,是一种用来放置Activity实例的容器,他是以栈的形式进行盛放,也就是所谓的先进后出,主要有2个基本操作:压栈和出栈,其所存放的Activity是不支持重新排序的,只能根据压栈和出栈操作更改Activity的顺序。

启动一个Application的时候,系统会为它默认创建一个对应的Task,用来放置根Activity。默认启动Activity会放在同一个Task中,新启动的Activity会被压入启动它的那个Activity的栈中,并且显示它。当用户按下回退键时,这个Activity就会被弹出栈,按下Home键回到桌面,再启动另一个应用,这时候之前那个Task就被移到后台,成为后台任务栈,而刚启动的那个Task就被调到前台,成为前台任务栈,手机页面显示的就是前台任务栈中的栈顶元素。

***********************************应用举例××××××××××××××××××××

standard  : 标准普通 传统模式。

singleTop:消息推送/ 全局搜索框。

singleTask : 跨应用调用单任务,不要出现重复页面,同时可以避免标准普通模式的调用栈过深。

singleInstance:全局单一实例。如电话来电页面。

********************************××××××××××××××××××××

Activity是安卓上最聪明的设计之一,优秀的内存管理让多任务完美运行在最流行的操作系统之上。并不是让Activity在屏幕上启动就完事了,其启动方式也是需要关注的。这个话题的内容很多,其中很重要的就是启动模式(launchMode)。这也是我们这篇博客要讨论的内容。

因为不同的Activity有不同的目的。有些被设计成每发送一个intent都单独一个Activity工作,比如邮件客户端中撰写邮件的Activity,而有些则被设计成单例的,比如邮件收件箱的Activity。

这就是为什么指明一个Activity是否需要新建还是使用现有Activity是很有必要的,否则可能导致糟糕的用户体验。多亏了安卓的核心工程师,让launchMode可以帮助你专门应对这种情况。

设置一个launchMode

一般地,我们可以直接在AndroidManifest.xml <activity>标签的一个属性中设置launchMode,如下:

 
 
  1. <activity
  2.             android:name=".SingleTaskActivity"
  3.             android:label="singleTask launchMode"
  4.             android:launchMode="singleTask">

有4种类型的launchMode,我们一个一个的看。

standard

这是默认的模式。

这种模式下,当Intent发送的时候,Activity总是被创建一个新的出来单独工作。想象一下,如果有发送10个撰写邮件的Intent,那么将有10个不同的Activity启动。


Lollipop之前设备上的表现


这种Activity将被创建并置于栈顶,和发送intent的Activity处于同一个任务中。注:一般来讲,安卓第三个虚拟键所列出的那些就是任务。

standardtopstandard


下面的图片显示了向标准启动模式的Activity分享照片时的情况。虽然分别来自不同的应用,但仍然它会和发送intent的Activity处于同一个任务中。

注:从图中可以看出分享图片的是Gallery应用。


standardgallery2


同时你会看到此时任务管理器是这样的(有一点怪异)。



gallerystandard



如果我们切换到另外一个应用然后再切回到Gallery,你会发现standard launchMode启动的Activity仍然在Gallery任务的上面,导致在操作Gallery之前,我们必须首先结束这个额外的Activity。


Lollipop设备上的表现


如果Activity都是来自同一个应用,其表现和Lollipop之前的设备一样,在任务的顶端。


standardstandardl


但是如果intent来自其他应用,将创建一个新的任务,同时新创建的Activity会被作为一个根Activity,如下:


standardgalleryl

注:图片中的Task#2和Task#3分别表示两个任务,序号大的比序号小的后启动。


下面是任务管理器中的样子:


gallerystandardl1


发生这种情况的原因是Lollipop中任务管理系统做了修改,让它看起来更合理了。因为它们在不同的任务中,你可以直接切回Gallery,你还可以触发另一个Intent,创建新的与之前相同的任务。


gallerystandardl2


撰写邮件的Activity或者发布社交网络状态的Activity都是采用这种Activity的例子。如果你希望Activity单独服务于一个Intent,就可以考虑standard启动模式。

singleTop

接下来就是singleTop模式。它的表现几乎和standard模式一模一样,一个singleTop Activity 的实例可以无限多,唯一的区别是如果在栈顶已经有一个相同类型的Activity实例,Intent不会再创建一个Activity,而是通过onNewIntent()被发送到现有的Activity。


singletop


在singleTop模式下我们需要同时在onCreate() 和 onNewIntent()中处理发来的intent,以满足不同情况。

这种启动模式的用例之一就是搜索功能。假设我们创建了一个搜索框,点击搜索的时候将导航到一个显示搜索结果列表的SearchActivity中,为了更好的用户体验,这个搜索框一般也会被放到SearchActivity中,这样用户想要再次搜索就不需要按返回键。

想像一下,如果每次显示搜索结果的时候我们都启动一个新的activity,10次搜索10个activity,那样当我们想返回最初的那个activity的时候需要按10次返回。

所以我们应该这样,如果栈顶已经有一个SearchActivity,我们将Intent发送给现有的activity,让它来更新搜索结果。这样就只会有一个在栈顶的SearchActivity,只需点一次back就可以回到之前的activity。

不管怎样,singleTop和它的调用者处在一个任务中。如果你想要让intent发送给另一个任务中处于栈顶的Activity,是不行的。

而当Intent来自于另外一个应用的时候,新的Activity的启动方式和standard模式是一致的(pre-Lollipop:处于调用者任务的栈顶,Lollipop:会创建一个新的任务)。

singleTask

这种模式和standard以及singleTop有很大不同。singleTask模式的Activity只允许在系统中有一个实例。如果系统中已经有了一个实例,持有这个实例的任务将移动到顶部,同时intent将被通过onNewIntent()发送。如果没有,则会创建一个新的Activity并置放在合适的任务中。


在同一个应用中的情况


如果系统中还没有singleTask的Activity,会新创建一个,并放在同一任务的栈顶。


singleTask1


但是如果已经存在,singleTask Activity上面的所有Activity将以合适的方式自动销毁,让我们想要显示的Activity处于栈顶。同时Intent也会通过onNewIntent()方法发送到这个singleTask Activity。


singleTaskD


在用户体验方面,可能不是很合理,但是它就是这样设计的...

你可能注意到了 官方文档 中提到的一个问题:

 系统会创建一个新的任务,并将这个Activity实例化为新任务的根部(root)。

但从实验结果来看,并不是这么回事。singleTask Activity仍然在任务的Activity栈顶,我们可以从dumpsys activity 命令显示上看出来:

 
 
  1. Task id #239
  2.   TaskRecord{428efe30 #239 A=com.thecheesefactory.lab.launchmode U=0 sz=2}
  3.   Intent
  4.  { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]
  5.  flg=0x10000000 
  6. cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
  7.     Hist #1: ActivityRecord{429a88d0 u0 com.thecheesefactory.lab.launchmode/.SingleTaskActivity t239}
  8.       Intent { cmp=com.thecheesefactory.lab.launchmode/.SingleTaskActivity }
  9.       ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}
  10.     Hist #0: ActivityRecord{425fec98 u0 com.thecheesefactory.lab.launchmode/.StandardActivity t239}
  11.       Intent
  12.  { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]
  13.  flg=0x10000000 
  14. cmp=com.thecheesefactory.lab.launchmode/.StandardActivity }
  15.       ProcessRecord{42243130 18965:com.thecheesefactory.lab.launchmode/u0a123}


如果你希望singleTask Activity表现的和文档中描述的一致,你需要为singleTask Activity设置taskAffinity属性。

 
 
  1. <activity
  2.             android:name=".SingleTaskActivity"
  3.             android:label="singleTask launchMode"
  4.             android:launchMode="singleTask"
  5.             android:taskAffinity="">


这里是启动SingleTaskActivity的的结果(使用了taskAffinity之后)。


singleTaskTaskAffinity

screenshot17


是否使用taskAffinity取决于你自己。


和其他应用一起工作的情况


一旦intent是从另外的应用发送过来,并且系统中也没有任何Activity的实例,则会创建一个新的任务,并且新的Activity被作为根Activity创建。


singleTaskAnotherApp1

singletaskfromapp2


除非拥有这个singleTask Activity 的应用已经存在,那样的话,新建的Activity会置于这个任务的上面(而不是新建一个任务)。


singleTaskAnotherApp2

In case that there is an Activity instance existed in any Task, the whole Task would be moved to top and every single Activity placed above the singleTask Activity will be destroyed with lifecycle. If back button is pressed, user has to travel through the Activities in the stack before going back to the caller Task.

假设已经有了一个Activity的实例,不管它是在哪个任务中(包括上面的那种情况,在用于这个Activity的应用中),整个任务将被移到顶端,而singleTask  Activity上面的所有 Activity 都将被销毁, 用户需要按back键遍历玩栈中的Activity才能回到调用者任务。


singleTaskAnotherApp3


这种模式的应用案例有。邮件客户端的收件箱或者社交网络的时间轴。这些Activity一般不会设计成拥有多个实例,singleTask可以满足。但是在使用这种模式的时候必须要明智,因为有些Activity会在用户不知情的情况下被销毁。

singleInstance

这个模式非常接近于singleTask,系统中只允许一个Activity的实例存在。区别在于持有这个Activity的任务中只能有一个Activity:即这个单例本身。If another Activity is called from this kind of Activity, a new Task would be automatically created to place that new Activity. Likewise, if singleInstance Activity is called, new Task would be created to place the Activity.

不过结果却很怪异,从dumpsys提供的信息来看,似乎系统中有两个任务但任务管理器中只显示一个,即最后被移到顶部的那个。导致虽然后台有一个任务在运行,我们却无法切换回去,这一点也不科学。

下面是当singleInstance Activity被调用的同时栈中已经有一些Activity的情况下所发生的事情:

singleInstance


本来有两个任务,但是任务管理器中却只显示一个任务:


singleInstances

Since this Task could has only one Activity, we couldn't switch back to Task #1 anymore. Only way to do so is to relaunch the application from launcher but it appears that the singleInstance Task would be hidden in the background instead.

因为这个任务只有一个Activity,我们再也无法切回到任务#1了。唯一的办法是重新在launcher中启动这个应用。 but之后的没有翻译,因为我也不明白作者的意思。

不过这个问题也有解决方案,就像我们在singleTask Acvity中做的,只要为singleInstance Activity设置taskAffinity属性就可以了。

 
 
  1. <activity
  2.             android:name=".SingleInstanceActivity"
  3.             android:label="singleInstance launchMode"
  4.             android:launchMode="singleInstance"
  5.             android:taskAffinity="">

现在科学多了。

screenshot18


这种模式很少被使用。实际使用的案例如Launcher的Activity或者100%确定只有一个Activity的应用。总之除非完全有必要,不然我不建议使用这种模式。

Intent Flags

除了在AndroidManifest.xml中直接设置launch mode,我们还可以通过叫做 Intent Flags的东西设置更多的行为,比如:

 
 
  1. Intent intent = new Intent(StandardActivity.this, StandardActivity.class);
  2. intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
  3. startActivity(intent);

这段代码将会启动一个singleTop启动模式的的StandardActivity






三 实例验证singleTask启动模式


上面将activity的四种启动模式就基本介绍完了。为了加深对启动模式的了解,下面会通过一个简单的例子进行验证。由以上的介绍可知,standard和singleTop这两种启动模式行为比较简单,所以在下面的例子中,会对singleTask和singleInstance着重介绍。


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


以下为验证示例AndroidTaskTest。这个实例中有三个Activity,分别为:MainActivity,SecondActivity和ThirdActivity。以下为这个示例的manifest文件。

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     package="com.jg.zhang.androidtasktest"  
  4.     android:versionCode="1"  
  5.     android:versionName="1.0" >  
  6.   
  7.     <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />  
  8.   
  9.     <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">  
  10.         <activity  android:label="@string/app_name"  
  11.             android:name="com.jg.zhang.androidtasktest.MainActivity">  
  12.             <intent-filter>  
  13.                 <action android:name="android.intent.action.MAIN" />  
  14.                 <category android:name="android.intent.category.LAUNCHER" />  
  15.             </intent-filter>  
  16.         </activity>  
  17.           
  18.         <!--android:taskAffinity="com.jg.zhang.androidtasktest.second"   
  19.             android:alwaysRetainTaskState="true"  
  20.             android:allowBackup="true" -->  
  21.               
  22.          <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"  
  23.              android:launchMode="singleTask">  
  24.             <intent-filter >  
  25.                 <action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>  
  26.                 <category android:name="android.intent.category.DEFAULT"/>  
  27.             </intent-filter>  
  28.         </activity>  
  29.           
  30.          <activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"  
  31.             android:label="@string/app_name" >  
  32.         </activity>  
  33.     </application>  
  34.       
  35. </manifest>  

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

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



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

MainActivity
  1. public class MainActivity extends Activity {  
  2.   
  3.     private static final String ACTIVITY_NAME = "MainActivity";  
  4.     private static final String LOG_TAG = "xxxx";  
  5.   
  6.     @Override  
  7.     protected void onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         setContentView(R.layout.activity_main);  
  10.           
  11.         findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {  
  12.             @Override  
  13.             public void onClick(View v) {  
  14.                 Intent intent = new Intent(MainActivity.this, SecondActivity.class);  
  15.   
  16.                 startActivity(intent);  
  17.             }  
  18.         });  
  19.           
  20.         int taskId = getTaskId();  
  21.         Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);  
  22.     }  



SecondActivity
  1. public class SecondActivity extends Activity {  
  2.     private static final String ACTIVITY_NAME = "SecondActivity";  
  3.     private static final String LOG_TAG = "xxxx";  
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_second);  
  8.           
  9.         findViewById(R.id.button2).setOnClickListener(new OnClickListener() {  
  10.             @Override  
  11.             public void onClick(View v) {  
  12.                     Intent intent = new Intent(SecondActivity.this, ThirdActivity.class);  
  13.                     startActivity(intent);  
  14.             }  
  15.         });  
  16.           
  17.         int taskId = getTaskId();  
  18.         Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);  
  19.           
  20.     }  

ThirdActivity
  1. public class ThirdActivity extends Activity {  
  2.       
  3.     private static final String ACTIVITY_NAME = "ThirdActivity";  
  4.     private static final String LOG_TAG = "xxxx";  
  5.     @Override  
  6.     protected void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.activity_third);  
  9.         int taskId = getTaskId();  
  10.         Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);  
  11.     }  

以上三个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属性,如下所示:

  1. <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"  
  2.     android:launchMode="singleTask"  
  3.     android:taskAffinity="com.jg.zhang.androidtasktest.second">  
  4.    <intent-filter >  
  5.        <action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>  
  6.        <category android:name="android.intent.category.DEFAULT"/>  
  7.    </intent-filter>  
  8. lt;/activity>  

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


在命令行中执行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运行在同一个任务中。这种现象的具体解释可以参考 解开Android应用程序组件Activity的"singleTask"之谜

在这里便引出了manifest文件中<activity>的一个重要属性,taskAffinity。在官方文档中可以得到关于taskAffinity的以下信息
  1. taskAffinity表示当前activity具有亲和力的一个任务(翻译不是很准确,原句为The task that the activity has an affinity for.),大致可以这样理解,这个 taskAffinity表示一个任务,这个任务就是当前activity所在的任务。
  2. 在概念上,具有相同的affinity的activity(即设置了相同taskAffinity属性的activity)属于同一个任务。
  3.  一个任务的affinity决定于这个任务的根activity(root activity)的taskAffinity。
  4.  这个属性决定两件事:当activity被re-parent时,它可以被re-paren哪个任务中;当activity以FLAG_ACTIVITY_NEW_TASK标志启动时,它会被启动到哪个任务中。(这个比较    难以理解,请结合<activity>中的属性allowTaskReparenting和Intent中的标志       FLAG_ACTIVITY_NEW_TASK加以理解)
  5.  默认情况下,一个应用中的所有activity具有相同的taskAffinity,即应用程序的包名。我们可以通过设置不同的taskAffinity属性给应用中的activity分组,也可以把不同的       应用中的activity的taskAffinity设置成相同的值。
  6.  为一个activity的taskAffinity设置一个空字符串,表明这个activity不属于任何task。

这就可以解释上面示例中的现象了,由第5条可知,MainActivity和SecondActivity具有不同的taskAffinity,MainActivity的taskAffinity为com.jg.zhang.androidtasktest,SecondActivity的taskAffinity为com.jg.zhang.androidtasktest.second,根据上面第4条,taskAffinity可以影响当activity以FLAG_ACTIVITY_NEW_TASK标志启动时,它会被启动到哪个任务中。这句话的意思是,当新启动的activity(SecondActivity)是以FLAG_ACTIVITY_NEW_TASK标志启动时(可以认为FLAG_ACTIVITY_NEW_TASK和singleTask作用相同,当启动模式为singleTask时,framework会将它的启动标志设为FLAG_ACTIVITY_NEW_TASK),framework会检索是否已经存在了一个affinity为com.jg.zhang.androidtasktest.second的任务(即一个TaskRecord对象)

  • 如果存在这样的一个任务,则检查在这个任务中是否已经有了一个SecondActivity的实例,
    • 如果已经存在一个SecondActivity的实例,则会重用这个任务和任务中的SecondActivity实例,将这个任务调到前台,清除位于SecondActivity上面的所有Activity,显示SecondActivity,并调用SecondActivity的onNewIntent();
    • 如果不存在一个SecondActivity的实例,会在这个任务中创建SecondActivity的实例,并调用onCreate()方法
  • 如果不存在这样的一个任务,会创建一个新的affinity为com.jg.zhang.androidtasktest.second的任务,并且将SecondActivity启动到这个新的任务中


上面讨论的是设置taskAffinity属性的情况,如果SecondActivity只设置启动模式为singleTask,而不设置taskAffinity,即三个Activity的taskAffinity相同,都为应用的包名,那么SecondActivity是不会开启一个新任务的,framework中的判定过程如下:

  1. 在MainActivity启动SecondActivity时,发现启动模式为singleTask,那么设定他的启动标志为FLAG_ACTIVITY_NEW_TASK
  2.  然后获得SecondActivity的taskAffinity,即为包名com.jg.zhang.androidtasktest
  3. 检查是否已经存在一个affinity为com.jg.zhang.androidtasktest的任务,这个任务是存在的,就是MainActivity所在的任务,这个任务是在启动MainActivity时开启的
  4.  既然已经存在这个任务,就检索在这个任务中是否存在一个SecondActivity的实例,发现不存在
  5.  在这个已有的任务中启动一个SecondActivity的实例

为了作一个清楚的比较,列出SecondActivity启动模式设为singleTask,并且taskAffinity设为com.jg.zhang.androidtasktest.second时的启动过程

  1. 在MainActivity启动SecondActivity时,发现启动模式为singleTask,那么设定他的启动标志为FLAG_ACTIVITY_NEW_TASK
  2. 然后获得SecondActivity的taskAffinity,即com.jg.zhang.androidtasktest.second
  3. 检查是否已经存在一个affinity为com.jg.zhang.androidtasktest.second的任务,这个任务是不存在的
  4. 创建一个新的affinity为com.jg.zhang.androidtasktest.second的任务,并且将SecondActivity启动到这个新的任务中

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



实例验证将两个不同app中的不同的singleTask模式的Activity的taskAffinity设成相同


官方文档中提到,可以把不同的 应用中的activity的taskAffinity设置成相同的值,这样的话这两个activity虽然不在同一应用中,却会在运行时分配到同一任务中,下面对此进行验证,在这里,会使用上面的示例AndroidTaskTest,并创建一个新的示例AndroidTaskTest1。AndroidTaskTest1由两个activity组成,分别为MianActivity和OtherActivity,在MianActivity中点击按钮会启动OtherActivity,该程序的界面和上一个类似,代码也类似,再此仅列出清单文件。

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     package="com.jg.zhang.androidtasktest1"  
  4.     android:versionCode="1"  android:versionName="1.0" >  
  5.   
  6.     <uses-sdk android:minSdkVersion="9"  android:targetSdkVersion="17" />  
  7.   
  8.     <application  
  9.         android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name"  
  10.         android:theme="@style/AppTheme" >  
  11.         <activity  
  12.             android:name="com.jg.zhang.androidtasktest1.MainActivity"  
  13.             android:label="com.jg.zhang.androidtasktest1.MainActivity" >  
  14.             <intent-filter>  
  15.                 <action android:name="android.intent.action.MAIN" />  
  16.                 <category android:name="android.intent.category.LAUNCHER" />  
  17.             </intent-filter>  
  18.         </activity>  
  19.           
  20.         <activity  
  21.             android:name="com.jg.zhang.androidtasktest1.OtherActivity"  
  22.             android:label="com.jg.zhang.androidtasktest1.OtherActivity"  
  23.             android:taskAffinity="com.jg.zhang.androidtasktest.second"  
  24.             android:launchMode="singleTask">  
  25.         </activity>  
  26.     </application>  
  27.   
  28. </manifest>  

可以看到OtherActivity的启动模式被设置为singleTask,并且taskAffinity属性被设置为com.jg.zhang.androidtasktest.second,这和AndroidTaskTest应用中的SecondActivity相同。现在将这两个应用安装在设备上。执行以下操作:

启动AndroidTaskTest应用,在它的MianActivity中点击按钮开启SecondActivity,由上面的介绍可知secondActivity是运行在一个新任务中的,这个任务就是com.jg.zhang.androidtasktest.second。

然后按Home键回到Launcher,启动AndroidTaskTest1,在启动AndroidTaskTest1的入口Activity(MianActivity)时,会自动启动新的任务,那么现在一共有三个任务,AndroidTaskTest的MianActivity和SecondActivity分别占用一个任务,AndroidTaskTest1的MianActivity也占用一个任务。

在AndroidTaskTest1的MianActivity中点击按钮启动OtherActivity,那么这个OtherActivity是在哪个任务中呢?

下面执行adb shell dumpsys activity命令,发现有以下输出:

TaskRecord{412370c0 #4 A com.jg.zhang.androidtasktest.second
Intent { cmp=com.jg.zhang.androidtasktest/.SecondActivity }
Hist #4: ActivityRecord{412f5ba0 com.jg.zhang.androidtasktest1/.OtherActivity}
Intent { flg=0x400000 cmp=com.jg.zhang.androidtasktest1/.OtherActivity }
ProcessRecord{412adb28 479:com.jg.zhang.androidtasktest1/ 10044}
Hist #3: ActivityRecord{4125c880 com.jg.zhang.androidtasktest/.SecondActivity}
Intent { cmp=com.jg.zhang.androidtasktest/.SecondActivity }
ProcessRecord{41218e48 463:com.jg.zhang.androidtasktest/ 10043}

TaskRecord{412f0f60 #5 A com.jg.zhang.androidtasktest1
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest1/.MainActivity }
Hist #2: ActivityRecord{413045a8 com.jg.zhang.androidtasktest1/.MainActivity}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest1/.MainActivity }
ProcessRecord{412adb28 479:com.jg.zhang.androidtasktest1/ 10044}
   
TaskRecord{412c5928 #3 A com.jg.zhang.androidtasktest}
 Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest/.MainActivity }
Hist #0: ActivityRecord{41250850 com.jg.zhang.androidtasktest/.MainActivity}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest/.MainActivity }
ProcessRecord{41218e48 463:com.jg.zhang.androidtasktest/ 10043}

在执行上述操作时,打印出的Log为:


所以由此可见,AndroidTaskTest的SecondActivity和AndroidTaskTest1的OtherActivity是在同一任务中的。由上面adb shell dumpsys activity命令的输出结果(蓝色字体)还可以看出,AndroidTaskTest和AndroidTaskTest1这两个应用程序会开启两个进程,他们的所有组件分别运行在独立的进程中,其中AndroidTaskTest所在进程的进程号为10043,AndroidTaskTest1所在进程的进程号为10044。com.jg.zhang.androidtasktest.second任务中的两个activity属于不同的应用,并且运行在不同的进程中,这也说明了一个问题: 任务(Task)不仅可以跨应用(Application),还可以跨进程(Process)


实例验证singleTask的另一意义:在同一个任务中具有唯一性


谷歌官方文档中提到,singleTask模式的activity总会在一个新的任务中开启。上面已经验证了这种说法不确切,singleTask模式只意味着“可以在一个新的任务中开启”,至于是不是真的会在新任务中开启,在framework中还有其他条件的限制。由上面的介绍可知,这个条件为:是否已经存在了一个由他的taskAffinity属性指定的任务。这一点具有迷惑性,我们在看到singleTask这个单词的时候,会直观的想到它的本意:single in task。即,在同一个任务中,只会有一个该activity的实例。现在让我们进行验证:

为了验证这种情况,需要修改一下上面用到的AndroidTaskTest示例。增加一个FourthActivity,并且MianActivity,SecondActivity,ThirdActivity和FourthActivity这四个activity都不设置taskAffinity属性,并且将SecondActivity启动模式设为singleTask,这样这四个activity会在同一个任务中开启。他们的开启流程是这样的:MianActivity开启SecondActivity,SecondActivity开启ThirdActivity,ThirdActivity开启FourthActivity,FourthActivity开启SecondActivity。代码和软件界面就不列出了,只列出清单文件

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     package="com.jg.zhang.androidtasktest"  
  4.     android:versionCode="1"  
  5.     android:versionName="1.0" >  
  6.   
  7.     <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />  
  8.   
  9.     <application android:allowBackup="true"  
  10.         android:icon="@drawable/ic_launcher" android:label="androidtasktest">  
  11.           
  12.         <activity  android:name="com.jg.zhang.androidtasktest.MainActivity">  
  13.             <intent-filter>  
  14.                 <action android:name="android.intent.action.MAIN" />  
  15.                 <category android:name="android.intent.category.LAUNCHER" />  
  16.             </intent-filter>  
  17.         </activity>  
  18.               
  19.          <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"  
  20.              android:launchMode="singleTask"/>  
  21.           
  22.          <activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"/>  
  23.            
  24.          <activity android:name="com.jg.zhang.androidtasktest.FourthActivity"/>  
  25.            
  26.     </application>  
  27.       
  28. </manifest>  

现在从MianActivity一直启动到FourthActivity,打印出的系统Log为:


由此可见这四个activity都是在同一个任务中的。再次执行adb shell dumpsys activity命令加以验证:

TaskRecord{412e9458 #6 A com.jg.zhang.androidtasktest}
Run #4: ActivityRecord{412e12e8 com.jg.zhang.androidtasktest/.FourthActivity}
Run #3: ActivityRecord{412a9e30 com.jg.zhang.androidtasktest/.ThirdActivity}
Run #2: ActivityRecord{412a4dd8 com.jg.zhang.androidtasktest/.SecondActivity}
Run #1: ActivityRecord{4122fae0 com.jg.zhang.androidtasktest/.MainActivity}

同样可以说明目前这四个activity都运行在affinity为com.jg.zhang.androidtasktest的任务中,即栈中的状态为MainActivity -->  SecondActivity --> ThirdActivity --> FourthActivity。

下面执行在FourthActivity中点击按钮启动SecondActivity的操作,注意,SecondActivity的启动模式为singleTask,那么现在栈中的情况如何呢?再次执行adb shell dumpsys activity命令,有以下输出:

TaskRecord{412e9458 #6 A com.jg.zhang.androidtasktest}
Run #2: ActivityRecord{412a4dd8 com.jg.zhang.androidtasktest/.SecondActivity}
Run #1: ActivityRecord{4122fae0 com.jg.zhang.androidtasktest/.MainActivity}

这时栈中的状态为MainActivity -->  SecondActivity。确实确保了在任务中是唯一的,并且清除了同一任务中它上面的所有Activity。 那么这个SecondActivity的实例是重用的上次已有的实例还是重新启动了一个实例呢?可以观察系统Log, 发现系统Log没有改变,还是上面的四条Log。打印Log的语句是在各个Activity中的onCreate方法中执行的,没有打印出新的Log,说明SecondActivity的onCreate的方法没有重新执行,也就是说是重用的上次已经启动的实例,而不是销毁重建。

经过上面的验证,可以得出如下的结论: 在启动一个singleTask的Activity实例时,如果系统中已经存在这样一个实例,就会将这个实例调度到任务栈的栈顶,并清除它当前所在任务中位于它上面的所有的activity。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值