解开Android应用程序组件Activity的"singleTask"之谜

发表于 5年以前  | 总阅读数:2730 次

在Android应用程序中,可以配置Activity以四种方式来启动,其中最令人迷惑的就是"singleTask"这种方式了,官方文档称以这种方式启动的Activity总是属于一个任务的根Activity。果真如此吗?本文将为你解开Activity的"singleTask"之谜。

在解开这个谜之前,我们先来简单了解一下在Android应用程序中,任务(Task)是个什么样的概念。我们知道,Activity是Android应用程序的基础组件之一,在应用程序运行时,每一个Activity代表一个用户操作。用户为了完成某个功能而执行的一系列操作就形成了一个Activity序列,这个序列在Android应用程序中就称之为任务,它是从用户体验的角度出发,把一组相关的Activity组织在一起而抽象出来的概念。

对初学者来说,在开发Android应用程序时,对任务的概念可能不是那么的直观,一般我们只关注如何实现应用程序中的每一个Activity。事实上,Android系统中的任务更多的是体现是应用程序运行的时候,因此,它相对于Activity来说是动态存在的,这就是为什么我们在开发时对任务这个概念不是那么直观的原因。不过,我们在开发Android应用程序时,还是可以配置Activity的任务属性的,即告诉系统,它是要在新的任务中启动呢,还是在已有的任务中启动,亦或是其它的Activity能不能与它共享同一个任务,具体配置请参考官方文档:

http://developer.android.com/guide/topics/fundamentals/tasks-and-back-stack.html

它是这样介绍以"singleTask"方式启动的Activity的:

The system creates a new task and instantiates the activity at the root of the new task. However, if an instance of the activity already exists in a separate task, the system routes the intent to the existing instance through a call to its onNewIntent() method, rather than creating a new instance. Only one instance of the activity can exist at a time.

它明确说明,以"singleTask"方式启动的Activity,全局只有唯一个实例存在,因此,当我们第一次启动这个Activity时,系统便会创建一个新的任务,并且初始化一个这样的Activity的实例,放在新任务的底部,如果下次再启动这个Activity时,系统发现已经存在这样的Activity实例,就会调用这个Activity实例的onNewIntent成员函数,从而把它激活起来。从这句话就可以推断出,以"singleTask"方式启动的Activity总是属于一个任务的根Activity。

但是文档接着举例子说明,当用户按下键盘上的Back键时,如果此时在前台中运行的任务堆栈顶端是一个"singleTask"的Activity,系统会回到当前任务的下一个Activity中去,而不是回到前一个Activity中去,如下图所示:

真是坑爹啊!有木有!前面刚说"singleTask"会在新的任务中运行,并且位于任务堆栈的底部,这里在Task B中,一个赤裸裸的带着"singleTask"标签的箭头无情地指向Task B堆栈顶端的Activity Y,刚转身就翻脸不认人了呢!

狮屎胜于熊便,我们来做一个实验吧,看看到底在启动这个"singleTask"的Activity的时候,它是位于新任务堆栈的底部呢,还是在已有任务的顶部。

首先在Android源代码工程中创建一个Android应用程序工程,名字就称为Task吧。关于如何获得Android源代码工程,请参考在Ubuntu上下载、编译和安装Android最新源代码一文;关于如何在Android源代码工程中创建应用程序工程,请参考在Ubuntu上为Android系统内置Java应用程序测试Application Frameworks层的硬件服务一文。这个应用程序工程定义了一个名为shy.luo.task的package,这个例子的源代码主要就是实现在这里了。下面,将会逐一介绍这个package里面的文件。

应用程序的默认Activity定义在src/shy/luo/task/MainActivity.java文件中:

package shy.luo.task;   

    import android.app.Activity;  
    import android.content.Intent;  
    import android.os.Bundle;  
    import android.util.Log;  
    import android.view.View;  
    import android.view.View.OnClickListener;  
    import android.widget.Button;  

    public class MainActivity extends Activity  implements OnClickListener {  
        private final static String LOG_TAG = "shy.luo.task.MainActivity";  

        private Button startButton = null;  

        @Override  
        public void onCreate(Bundle savedInstanceState) {  
            super.onCreate(savedInstanceState);  
            setContentView(R.layout.main);  

            startButton = (Button)findViewById(R.id.button_start);  
            startButton.setOnClickListener(this);  

            Log.i(LOG_TAG, "Main Activity Created.");  
        }  

        @Override  
        public void onClick(View v) {  
            if(v.equals(startButton)) {  
                Intent intent = new Intent("shy.luo.task.subactivity");  
                startActivity(intent);  
            }  
        }  
    }  

它的实现很简单,当点击它上面的一个按钮的时候,就会启动另外一个名字为"shy.luo.task.subactivity"的Actvity。 名字为"shy.luo.task.subactivity"的Actvity实现在src/shy/luo/task/SubActivity.java文件中:

package shy.luo.task;  

    import android.app.Activity;  
    import android.os.Bundle;  
    import android.util.Log;  
    import android.view.View;  
    import android.view.View.OnClickListener;  
    import android.widget.Button;  

    public class SubActivity extends Activity implements OnClickListener {  
        private final static String LOG_TAG = "shy.luo.task.SubActivity";  

        private Button finishButton = null;  

        @Override  
        public void onCreate(Bundle savedInstanceState) {  
            super.onCreate(savedInstanceState);  
            setContentView(R.layout.sub);  

            finishButton = (Button)findViewById(R.id.button_finish);  
            finishButton.setOnClickListener(this);  

            Log.i(LOG_TAG, "Sub Activity Created.");  
        }  

        @Override  
        public void onClick(View v) {  
            if(v.equals(finishButton)) {  
                finish();  
            }  
        }  
    }  

它的实现也很简单,当点击上面的一个铵钮的时候,就结束自己,回到前面一个Activity中去。

再来看一下应用程序的配置文件AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>  
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
        package="shy.luo.task"  
        android:versionCode="1"  
        android:versionName="1.0">  
        <application android:icon="@drawable/icon" android:label="@string/app_name">  
            <activity android:name=".MainActivity"  
                      android:label="@string/app_name">  
                <intent-filter>  
                    <action android:name="android.intent.action.MAIN" />  
                    <category android:name="android.intent.category.LAUNCHER" />  
                </intent-filter>  
            </activity>  
            <activity android:name=".SubActivity"  
                      android:label="@string/sub_activity"
                      android:launchMode="singleTask">  
                <intent-filter>  
                    <action android:name="shy.luo.task.subactivity"/>  
                    <category android:name="android.intent.category.DEFAULT"/>  
                </intent-filter>  
            </activity>  
        </application>  
    </manifest>  

注意,这里的SubActivity的launchMode属性配置为"singleTask"。 再来看界面配置文件,它们定义在res/layout目录中,main.xml文件对应MainActivity的界面:

<?xml version="1.0" encoding="utf-8"?>  
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        android:orientation="vertical"  
        android:layout_width="fill_parent"  
        android:layout_height="fill_parent"   
        android:gravity="center">  
            <Button   
                android:id="@+id/button_start"  
                android:layout_width="wrap_content"  
                android:layout_height="wrap_content"  
                android:gravity="center"  
                android:text="@string/start" >  
            </Button>  
    </LinearLayout>  

而sub.xml对应SubActivity的界面:

<?xml version="1.0" encoding="utf-8"?>  
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        android:orientation="vertical"  
        android:layout_width="fill_parent"  
        android:layout_height="fill_parent"   
        android:gravity="center">  
            <Button   
                android:id="@+id/button_finish"  
                android:layout_width="wrap_content"  
                android:layout_height="wrap_content"  
                android:gravity="center"  
                android:text="@string/finish" >  
            </Button>  
    </LinearLayout>  

字符串文件位于res/values/strings.xml文件中:

<?xml version="1.0" encoding="utf-8"?>  
    <resources>  
        <string name="app_name">Task</string>  
        <string name="sub_activity">Sub Activity</string>  
        <string name="start">Start singleTask activity</string>  
        <string name="finish">Finish activity</string>  
    </resources>  

最后,我们还要在工程目录下放置一个编译脚本文件Android.mk:

LOCAL_PATH:= $(call my-dir)  
    include $(CLEAR_VARS)  

    LOCAL_MODULE_TAGS := optional  

    LOCAL_SRC_FILES := $(call all-subdir-java-files)  

    LOCAL_PACKAGE_NAME := Task  

    include $(BUILD_PACKAGE)  

这样,原材料就准备好了,接下来就要编译了。有关如何单独编译Android源代码工程的模块,以及如何打包system.img,请参考如何单独编译Android源代码中的模块一文。 执行以下命令进行编译和打包:

USER-NAME@MACHINE-NAME:~/Android$ mmm packages/experimental/Task    
    USER-NAME@MACHINE-NAME:~/Android$ make snod   

这样,打包好的Android系统镜像文件system.img就包含我们前面创建的Task应用程序了。 再接下来,就是运行模拟器来运行我们的例子了。关于如何在Android源代码工程中运行模拟器,请参考在Ubuntu上下载、编译和安装Android最新源代码一文。 执行以下命令启动模拟器:

USER-NAME@MACHINE-NAME:~/Android$ emulator

模拟器启动起,就可以App Launcher中找到Task应用程序图标,接着把它启动起来:

点击中间的按钮,就会以"singleTask"的方式来启动SubActivity:

现在,我们如何来确认SubActivity是不是在新的任务中启动并且位于这个新任务的堆栈底部呢?Android源代码工程为我们准备了adb工具,可以查看模拟器上系统运行的状况,执行下面的命令查看;

USER-NAME@MACHINE-NAME:~/Android$ adb shell dumpsys activity

这个命令输出的内容比较多,这里我们只关心TaskRecord部分:

Running activities (most recent first):
        TaskRecord{4070d8f8 #3 A shy.luo.task}
          Run #2: HistoryRecord{406a13f8 shy.luo.task/.SubActivity}
          Run #1: HistoryRecord{406a0e00 shy.luo.task/.MainActivity}
        TaskRecord{4067a510 #2 A com.android.launcher}
          Run #0: HistoryRecord{40677518 com.android.launcher/com.android.launcher2.Launcher}

果然,SubActivity和MainActivity都是运行在TaskRecord#3中,并且SubActivity在MainActivity的上面。这是怎么回事呢?碰到这种情况,Linus Torvalds告诫我们:Read the fucking source code;去年张麻子又说:枪在手,跟我走;我们没有枪,但是有source code,因此,我要说:跟着代码走。

前面我们在两篇文章Android应用程序启动过程源代码分析Android应用程序内部启动Activity过程(startActivity)的源代码分析时,分别在Step 9和Step 8中分析了Activity在启动过程中与任务相关的函数ActivityStack.startActivityUncheckedLocked函数中,它定义在frameworks/base/services/java/com/android/server/am/ActivityStack.java文件中:

public class ActivityStack {

        ......

        final int startActivityUncheckedLocked(ActivityRecord r,
                ActivityRecord sourceRecord, Uri[] grantedUriPermissions,
                int grantedMode, boolean onlyIfNeeded, boolean doResume) {
            final Intent intent = r.intent;
            final int callingUid = r.launchedFromUid;

            int launchFlags = intent.getFlags();

            ......

            ActivityRecord notTop = (launchFlags&Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
               != 0 ? r : null;

            ......

            if (sourceRecord == null) {
                ......
            } else if (sourceRecord.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
                ......
            } else if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE
               || r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) {
                // The activity being started is a single instance...  it always
                // gets launched into its own task.
                launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;
            }

            ......

            boolean addingToTask = false;
            if (((launchFlags&Intent.FLAG_ACTIVITY_NEW_TASK) != 0 &&
               (launchFlags&Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0)
               || r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK
               || r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
                   // If bring to front is requested, and no result is requested, and
                   // we can find a task that was started with this same
                   // component, then instead of launching bring that one to the front.
                   if (r.resultTo == null) {
                       // See if there is a task to bring to the front.  If this is
                       // a SINGLE_INSTANCE activity, there can be one and only one
                       // instance of it in the history, and it is always in its own
                       // unique task, so we do a special search.
                       ActivityRecord taskTop = r.launchMode != ActivityInfo.LAUNCH_SINGLE_INSTANCE
                           ? findTaskLocked(intent, r.info)
                           : findActivityLocked(intent, r.info);
                       if (taskTop != null) {

                           ......

                           if ((launchFlags&Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0
                               || r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK
                               || r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
                                   // In this situation we want to remove all activities
                                   // from the task up to the one being started.  In most
                                   // cases this means we are resetting the task to its
                                   // initial state.
                                   ActivityRecord top = performClearTaskLocked(
                                       taskTop.task.taskId, r, launchFlags, true);
                                   if (top != null) {
                                       ......
                                   } else {
                                       // A special case: we need to
                                       // start the activity because it is not currently
                                       // running, and the caller has asked to clear the
                                       // current task to have this activity at the top.
                                       addingToTask = true;
                                       // Now pretend like this activity is being started
                                       // by the top of its task, so it is put in the
                                       // right place.
                                       sourceRecord = taskTop;
                                   }
                           } else if (r.realActivity.equals(taskTop.task.realActivity)) {
                               ......
                           } else if ((launchFlags&Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) == 0) {
                               ......
                           } else if (!taskTop.task.rootWasReset) {
                               ......
                           }

                           ......
                       }
                   }
            }

            ......

            if (r.packageName != null) {
               // If the activity being launched is the same as the one currently
               // at the top, then we need to check if it should only be launched
               // once.
               ActivityRecord top = topRunningNonDelayedActivityLocked(notTop);
               if (top != null && r.resultTo == null) {
                   if (top.realActivity.equals(r.realActivity)) {
                       if (top.app != null && top.app.thread != null) {
                           ......
                       }
                   }
               }

            } else {
               ......
            }

            boolean newTask = false;

            // Should this be considered a new task?
            if (r.resultTo == null && !addingToTask
               && (launchFlags&Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
                 // todo: should do better management of integers.
                             mService.mCurTask++;
                             if (mService.mCurTask <= 0) {
                                  mService.mCurTask = 1;
                             }
                             r.task = new TaskRecord(mService.mCurTask, r.info, intent,
                                 (r.info.flags&ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH) != 0);
                             if (DEBUG_TASKS) Slog.v(TAG, "Starting new activity " + r
                                + " in new task " + r.task);
                             newTask = true;
                             if (mMainStack) {
                                  mService.addRecentTaskLocked(r.task);
                             }
            } else if (sourceRecord != null) {
               if (!addingToTask &&
                   (launchFlags&Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) {
                    ......
               } else if (!addingToTask &&
                   (launchFlags&Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) != 0) {
                    ......
               }
               // An existing activity is starting this new activity, so we want
               // to keep the new one in the same task as the one that is starting
               // it.
               r.task = sourceRecord.task;

               ......

            } else {
               ......
            }

            ......

            startActivityLocked(r, newTask, doResume);
            return START_SUCCESS;
        }

        ......

    }

首先是获得用来启动Activity的Intent的Flags,并且保存在launchFlags变量中,这里,launcFlags的Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP位没有置位,因此,notTop为null。

接下来的这个if语句:

    if (sourceRecord == null) {
        ......
        } else if (sourceRecord.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
        ......
        } else if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE
              || r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) {
        // The activity being started is a single instance...  it always
        // gets launched into its own task.
        launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK;
        }

这里变量r的类型为ActivityRecord,它表示即将在启动的Activity,在这个例子中,即为SubActivity,因此,这里的r.launchMode等于ActivityInfo.LAUNCH_SINGLE_TASK,于是,无条件将launchFlags的Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP位置为1,表示这个SubActivity要在新的任务中启动,但是别急,还要看看其它条件是否满足,如果条件都满足,才可以在新的任务中启动这个SubActivity。 接下将addingToTask变量初始化为false,这个变量也将决定是否要将SubActivity在新的任务中启动,从名字我们就可以看出,默认不增加到原有的任务中启动,即要在新的任务中启动。这里的r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK条成立,条件r.resultTo == null也成立,它表这个Activity不需要将结果返回给启动它的Activity。于是会进入接下来的if语句中,执行:

    ActivityRecord taskTop = r.launchMode != ActivityInfo.LAUNCH_SINGLE_INSTANCE
            ? findTaskLocked(intent, r.info)
            : findActivityLocked(intent, r.info)

这里的条件r.launchMode != ActivityInfo.LAUNCH_SINGLE_INSTANCE成立,于是执行findTaskLocked函数,这个函数也是定义在frameworks/base/services/java/com/android/server/am/ActivityStack.java文件中:

public class ActivityStack {

        ......

        /**
        * Returns the top activity in any existing task matching the given
        * Intent.  Returns null if no such task is found.
        */
        private ActivityRecord findTaskLocked(Intent intent, ActivityInfo info) {
            ComponentName cls = intent.getComponent();
            if (info.targetActivity != null) {
                cls = new ComponentName(info.packageName, info.targetActivity);
            }

            TaskRecord cp = null;

            final int N = mHistory.size();
            for (int i=(N-1); i>=0; i--) {
                ActivityRecord r = (ActivityRecord)mHistory.get(i);
                if (!r.finishing && r.task != cp
                    && r.launchMode != ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
                        cp = r.task;
                        //Slog.i(TAG, "Comparing existing cls=" + r.task.intent.getComponent().flattenToShortString()
                        //        + "/aff=" + r.task.affinity + " to new cls="
                        //        + intent.getComponent().flattenToShortString() + "/aff=" + taskAffinity);
                        if (r.task.affinity != null) {
                            if (r.task.affinity.equals(info.taskAffinity)) {
                                //Slog.i(TAG, "Found matching affinity!");
                                return r;
                            }
                        } else if (r.task.intent != null
                            && r.task.intent.getComponent().equals(cls)) {
                                //Slog.i(TAG, "Found matching class!");
                                //dump();
                                //Slog.i(TAG, "For Intent " + intent + " bringing to top: " + r.intent);
                                return r;
                        } else if (r.task.affinityIntent != null
                            && r.task.affinityIntent.getComponent().equals(cls)) {
                                //Slog.i(TAG, "Found matching class!");
                                //dump();
                                //Slog.i(TAG, "For Intent " + intent + " bringing to top: " + r.intent);
                                return r;
                        }
                }
            }

            return null;
        }

        ......

    }

这个函数无非就是根据即将要启动的SubActivity的taskAffinity属性值在系统中查找这样的一个Task:Task的affinity属性值与即将要启动的Activity的taskAffinity属性值一致。如果存在,就返回这个Task堆栈顶端的Activity回去。在上面的AndroidManifest.xml文件中,没有配置MainActivity和SubActivity的taskAffinity属性,于是它们的taskAffinity属性值就默认为父标签application的taskAffinity属性值,这里,标签application的taskAffinity也没有配置,于是它们就默认为包名,即"shy.luo.task"。由于在启动SubActivity之前,MainActivity已经启动,MainActivity启动的时候,会在一个新的任务里面启动,而这个新的任务的affinity属性就等于它的第一个Activity的taskAffinity属性值。于是,这个函数会动回表示MainActivity的ActivityRecord回去.

回到前面的startActivityUncheckedLocked函数中,这里的taskTop就表示MainActivity,它不为null,于是继续往前执行。由于条件r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK成立,于是执行下面语句:

    ActivityRecord top = performClearTaskLocked(
        taskTop.task.taskId, r, launchFlags, true);

函数performClearTaskLocked也是定义在frameworks/base/services/java/com/android/server/am/ActivityStack.java文件中:

public class ActivityStack {

        ......

        /**
        * Perform clear operation as requested by
        * {@link Intent#FLAG_ACTIVITY_CLEAR_TOP}: search from the top of the
        * stack to the given task, then look for
        * an instance of that activity in the stack and, if found, finish all
        * activities on top of it and return the instance.
        *
        * @param newR Description of the new activity being started.
        * @return Returns the old activity that should be continue to be used,
        * or null if none was found.
        */
        private final ActivityRecord performClearTaskLocked(int taskId,
        ActivityRecord newR, int launchFlags, boolean doClear) {
            int i = mHistory.size();

            // First find the requested task.
            while (i > 0) {
                i--;
                ActivityRecord r = (ActivityRecord)mHistory.get(i);
                if (r.task.taskId == taskId) {
                    i++;
                    break;
                }
            }

            // Now clear it.
            while (i > 0) {
                i--;
                ActivityRecord r = (ActivityRecord)mHistory.get(i);
                if (r.finishing) {
                    continue;
                }
                if (r.task.taskId != taskId) {
                    return null;
                }
                if (r.realActivity.equals(newR.realActivity)) {
                    // Here it is!  Now finish everything in front...
                    ActivityRecord ret = r;
                    if (doClear) {
                        while (i < (mHistory.size()-1)) {
                            i++;
                            r = (ActivityRecord)mHistory.get(i);
                            if (r.finishing) {
                                continue;
                            }
                            if (finishActivityLocked(r, i, Activity.RESULT_CANCELED,
                                null, "clear")) {
                                    i--;
                            }
                        }
                    }

                    // Finally, if this is a normal launch mode (that is, not
                    // expecting onNewIntent()), then we will finish the current
                    // instance of the activity so a new fresh one can be started.
                    if (ret.launchMode == ActivityInfo.LAUNCH_MULTIPLE
                        && (launchFlags&Intent.FLAG_ACTIVITY_SINGLE_TOP) == 0) {
                            if (!ret.finishing) {
                                int index = indexOfTokenLocked(ret);
                                if (index >= 0) {
                                    finishActivityLocked(ret, index, Activity.RESULT_CANCELED,
                                        null, "clear");
                                }
                                return null;
                            }
                    }

                    return ret;
                }
            }

            return null;
        }

        ......

    }

这个函数中作用无非就是找到ID等于参数taskId的任务,然后在这个任务中查找是否已经存在即将要启动的Activity的实例,如果存在,就会把这个Actvity实例上面直到任务堆栈顶端的Activity通过调用finishActivityLocked函数将它们结束掉。在这个例子中,就是要在属性值affinity等于"shy.luo.task"的任务中看看是否存在SubActivity类型的实例,如果有,就把它上面的Activity都结束掉。这里,属性值affinity等于"shy.luo.task"的任务只有一个MainActivity,而且它不是SubActivity的实例,所以这个函数就返回null了。

回到前面的startActivityUncheckedLocked函数中,这里的变量top就为null了,于是执行下面的else语句:

    if (top != null) {
        ......
        } else {
        // A special case: we need to
        // start the activity because it is not currently
        // running, and the caller has asked to clear the
        // current task to have this activity at the top.
        addingToTask = true;
        // Now pretend like this activity is being started
        // by the top of its task, so it is put in the
        // right place.
        sourceRecord = taskTop;
        }

于是,变量addingToTask值就为true了,同时将变量sourceRecord的值设置为taskTop,即前面调用findTaskLocked函数的返回值,这里,它就是表示MainActivity了。

继续往下看,下面这个if语句:

    if (r.packageName != null) {
        // If the activity being launched is the same as the one currently
            // at the top, then we need to check if it should only be launched
        // once.
        ActivityRecord top = topRunningNonDelayedActivityLocked(notTop);
        if (top != null && r.resultTo == null) {
            if (top.realActivity.equals(r.realActivity)) {
                    if (top.app != null && top.app.thread != null) {
                    ......
                }
            }
        }

        } else {
        ......
        }

它是例行性地检查当前任务顶端的Activity,是否是即将启动的Activity的实例,如果是否的话,在某些情况下,它什么也不做,就结束这个函数调用了。这里,当前任务顶端的Activity为MainActivity,它不是SubActivity实例,于是继续往下执行:

    boolean newTask = false;

        // Should this be considered a new task?
        if (r.resultTo == null && !addingToTask
        && (launchFlags&Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
        ......

        } else if (sourceRecord != null) {
        if (!addingToTask &&
            (launchFlags&Intent.FLAG_ACTIVITY_CLEAR_TOP) != 0) {
            ......
        } else if (!addingToTask &&
                (launchFlags&Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) != 0) {
            ......
        }
        // An existing activity is starting this new activity, so we want
        // to keep the new one in the same task as the one that is starting
        // it.
        r.task = sourceRecord.task;

        ......

        } else {
            ......
        }

这里首先将newTask变量初始化为false,表示不要在新的任务中启动这个SubActivity。由于前面的已经把addingToTask设置为true,因此,这里会执行中间的else if语句,即这里会把r.task设置为sourceRecord.task,即把SubActivity放在MainActivity所在的任务中启动。

最后,就是调用startActivityLocked函数继续进行启动Activity的操作了。后面的操作这里就不跟下去了,有兴趣的读者可以参考两篇文章Android应用程序启动过程源代码分析Android应用程序内部启动Activity过程(startActivity)的源代码分析

到这里,思路就理清了,虽然SubActivity的launchMode被设置为"singleTask"模式,但是它并不像官方文档描述的一样:The system creates a new task and instantiates the activity at the root of the new task,而是在跟它有相同taskAffinity的任务中启动,并且位于这个任务的堆栈顶端,于是,前面那个图中,就会出现一个带着"singleTask"标签的箭头指向一个任务堆栈顶端的Activity Y了。 那么,我们有没有办法让一个"singleTask"的Activity在新的任务中启动呢?答案是肯定的。从上面的代码分析中,只要我们能够进入函数startActivityUncheckedLocked的这个if语句中:

    if (r.resultTo == null && !addingToTask
              && (launchFlags&Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
          // todo: should do better management of integers.
              mService.mCurTask++;
              if (mService.mCurTask <= 0) {
                   mService.mCurTask = 1;
              }
              r.task = new TaskRecord(mService.mCurTask, r.info, intent,
                         (r.info.flags&ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH) != 0);
              if (DEBUG_TASKS) Slog.v(TAG, "Starting new activity " + r
                         + " in new task " + r.task);
               newTask = true;
               if (mMainStack) {
                     mService.addRecentTaskLocked(r.task);
               }
        }

那么,这个即将要启动的Activity就会在新的任务中启动了。进入这个if语句需要满足三个条件,r.resultTo为null,launchFlags的Intent.FLAG_ACTIVITY_NEW_TASK位为1,并且addingToTask值为false。从上面的分析中可以看到,当即将要启动的Activity的launchMode为"singleTask",并且调用startActivity时不要求返回要启动的Activity的执行结果时,前面两个条件可以满足,要满足第三个条件,只要当前系统不存在affinity属性值等于即将要启动的Activity的taskAffinity属性值的任务就可以了。

我们可以稍微修改一下上面的AndroidManifest.xml配置文件来做一下这个实验:

<?xml version="1.0" encoding="utf-8"?>  
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
        package="shy.luo.task"  
        android:versionCode="1"  
        android:versionName="1.0">  
        <application android:icon="@drawable/icon" android:label="@string/app_name">  
            <activity android:name=".MainActivity"  
                      android:label="@string/app_name"
                      android:taskAffinity="shy.luo.task.main.activity">  
                <intent-filter>  
                    <action android:name="android.intent.action.MAIN" />  
                    <category android:name="android.intent.category.LAUNCHER" />  
                </intent-filter>  
            </activity>  
            <activity android:name=".SubActivity"  
                      android:label="@string/sub_activity"
                      android:launchMode="singleTask"
                      android:taskAffinity="shy.luo.task.sub.activity">  
                <intent-filter>  
                    <action android:name="shy.luo.task.subactivity"/>  
                    <category android:name="android.intent.category.DEFAULT"/>  
                </intent-filter>  
            </activity>  
        </application>  
    </manifest>  

注意,这里我们设置MainActivity的taskAffinity属性值为"shy.luo.task.main.activity",设置SubActivity的taskAffinity属性值为"shy.luo.task.sub.activity"。重新编译一下程序,在模拟器上把这个应用程序再次跑起来,用"adb shell dumpsys activity"命令再来查看一下系统运行的的任务,就会看到:

Running activities (most recent first):
        TaskRecord{4069c020 #4 A shy.luo.task.sub.activity}
          Run #2: HistoryRecord{40725040 shy.luo.task/.SubActivity}
        TaskRecord{40695220 #3 A shy.luo.task.main.activity}
          Run #1: HistoryRecord{406b26b8 shy.luo.task/.MainActivity}
        TaskRecord{40599c90 #2 A com.android.launcher}
          Run #0: HistoryRecord{40646628 com.android.launcher/com.android.launcher2.Launcher}

这里就可以看到,SubActivity和MainActivity就分别运行在不同的任务中了。

至此,我们总结一下,设置了"singleTask"启动模式的Activity的特点:

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

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

看来,要解开Activity的"singleTask"之谜,还是要自力更生啊,不过,如果我们仔细阅读官方文档,在http://developer.android.com/guide/topics/manifest/activity-element.html中,有这样的描述:

As shown in the table above, standard is the default mode and is appropriate for most types of activities. SingleTop is also a common and useful launch mode for many types of activities. The other modes -- singleTask and singleInstance --are not appropriate for most applications, since they result in an interaction model that is likely to be unfamiliar to users and is very different from most other applications. Regardless of the launch mode that you choose, make sure to test the usability of the activity during launch and when navigating back to it from other activities and tasks using the BACK key.

这样看,官方文档也没有坑我们呢,它告诫我们:make sure to test the usability of the activity during launch

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 目录