Activity启动模式

39 阅读11分钟

启动模式概览

启动模式声明方式行为特点典型场景
standard(标准模式)默认或android:launchMode="standard"每次都创建新实例普通页面跳转,如列表→详情
singleTop(栈顶复用)android:launchMode="singleTop"如果已经在栈顶,则复用并调用onNewIntent()避免重复创建,如搜索
singleTask(栈内唯一)android:launchMode="singleTask"在任务栈中保持唯一,会清除上方的Activity应用主页、登录页
singleInstance(单实例)android:launchMode="singleInstance"独占一个任务栈,且栈内只有它自己,确保全局只有一个实例系统Launcher、来电页面、视频播放
singleInstancePerTask(单实例每任务)android:launchMode="singleInstancePerTask"允许同一个 Activity 在不同任务栈中有自己的实例,但在同一栈中保持唯一多窗口或多实例场景,如文档编辑

任务栈是是一个后进先出的Activity集合,用于维护用户的导航历史。每一个Activity都有一个taskAffinity属性,默认是应用包名,它决定了Activity倾向于在哪个任务栈中。

启动模式详解

standard(标准模式)

标准模式是Activity的默认启动模式。在这种模式下,每次启动Activity都会创建一个新的实例,并将其压入当前任务栈的栈顶。

当在应用中调用startActivity()启动一个standard模式的Activity时,系统会执行一下内容,与Activity启动流程最后一步一致。

// 在ActivityThread中处理standard模式的启动
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 1. 通过类加载器创建Activity实例
    Activity activity = mInstrumentation.newActivity(
        cl, component.getClassName(), r.intent);
    
    // 2. 调用Activity的构造函数
    // 3. 创建Context实例
    // 4. 调用attach()方法绑定上下文
    activity.attach(appContext, this, getInstrumentation(), r.token,
                   r.ident, app, r.intent, r.activityInfo, title,
                   r.parent, r.embeddedID, r.lastNonConfigurationInstances,
                   config, r.referrer, r.voiceInteractor, window);
    
    // 5. 调用onCreate()生命周期方法
    mInstrumentation.callActivityOnCreate(activity, r.state);
    
    return activity;
}

假如我们有三个Activity,A(standard) → B(standard) → C(standard)

初始状态:
任务栈:[A]

操作1:从A启动B
结果:创建B的新实例
任务栈:[A, B]

操作2:从B启动C
结果:创建C的新实例
任务栈:[A, B, C]

操作3:从C再次启动B
结果:创建B的另一个新实例
任务栈:[A, B, C, B]  ← 注意:这是B的第二个实例

操作4:按返回键
结果:销毁栈顶的B实例
任务栈:[A, B, C]

继续按返回键:C → BA
最终回到空栈
  • 设计意图:提供最直观的页面导航,保持每个实例的独立性,支持多实例并发存在。
  • 适用场景:大多数普通的内容展示页面
  • 不适用场景:应用主页、登录页、全局设置页(容易创建多实例,应保持唯一性)

singleTop(栈顶复用)

singleTop模式的核心特性是避免在栈顶重复创建相同Activity的实例。如果目标Activity已经在任务栈的栈顶,则复用该实例并调用其onNewIntent()方法;否则,创建新实例。

AMS在启动singleTop Activity时的决策逻辑:

// ActivityStarter.java - 判断是否复用栈顶实例
private boolean startActivityUnchecked(...) {
    // 检查是否是singleTop模式
    if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP) {
        // 获取当前任务栈的栈顶Activity
        ActivityRecord top = mSourceRecord.task.getTopActivity();
        
        if (top != null && top.realActivity.equals(r.realActivity)) {
            // 栈顶已有相同Activity,触发deliverNewIntent
            mTargetStack.lastPausedActivity = top;
            
            // 调用onNewIntent()而不是创建新实例
            top.deliverNewIntentLocked(callingUid, r.intent);
            
            // 调整任务栈顺序
            mTargetStack.moveTaskToFrontLocked(top.task, noAnimation, options, 
                                              r.appTimeTracker, "singleTop");
            return true;
        }
    }
    // 否则继续正常启动流程
    return startActivityLocked(r, newTask, doResume, keepCurTransition, options);
}

栈顶已存在相同Activity

初始状态:
任务栈:[A, B(singleTop)]

操作:从B启动B
结果:复用B的现有实例,调用B.onNewIntent()
任务栈:[A, B]  ← 栈深度不变
日志:B.onNewIntent()被调用,B.onResume()被调用

栈顶不是目标Activity

初始状态:
任务栈:[A(singleTop), B]

操作:从B启动A
结果:创建A的新实例
任务栈:[A, B, A]  ← 新实例入栈
日志:A.onCreate()被调用

复杂导航链

初始状态:
任务栈:[A, B(singleTop), C]

操作1:从C启动B
结果:创建B的新实例(因为B不在栈顶)
任务栈:[A, B, C, B]  ← 第二个B实例

操作2:从栈顶B再次启动B
结果:复用栈顶B实例
任务栈:[A, B, C, B]  ← 栈深度不变

当singleTop Activity被复用时,系统不会调用onCreate(),而是调用onNewIntent(),需要手动调用setIntent()更新Activity的当前Intent,onNewIntent()调用后,onResume()会被调用。

  • 设计意图:优化用户体验,避免重复页面闪烁;节省系统资源,减少不必要的实例创建;支持实时更新已存在的界面,例如后台运行的Activity需要根据特定事件更新界面。
  • 适用场景:搜索页面,通知处理页面,实时数据展示页面。
  • 局限性:只检查栈顶,不检查栈中其他位置,如果目标不在栈顶,仍会创建新实例

singleTask(栈内唯一)

singleTask模式确保Activity在整个任务栈中保持唯一性。当启动一个singleTask Activity时,系统会:

  1. 查找匹配的任务栈(基于taskAffinity)
  2. 如果找到栈中已有该Activity的实例,则清除其上的所有其他Activity
  3. 将找到的实例移到栈顶并调用onNewIntent()
  4. 如果没找到,则在合适的任务栈中创建新实例

taskAffinity是决定Activity属于哪个任务栈的关键属性:

<!-- 默认情况:使用应用包名 -->
<activity android:name=".MainActivity"
    android:taskAffinity="com.example.myapp" />

<!-- 显式指定不同的affinity -->
<activity android:name=".SettingsActivity"
    android:launchMode="singleTask"
    android:taskAffinity="com.example.myapp.settings" />

<!-- 完全不同的affinity -->
<activity android:name=".ShareActivity"
    android:launchMode="singleTask"
    android:taskAffinity="com.example.share" />

taskAffinity规则

  • 默认值:应用包名
  • 相同affinity的Activity倾向于在同一个任务栈
  • 不同affinity的singleTask Activity会进入不同的任务栈
  • 从桌面启动的应用默认创建新的任务栈

AMS处理singleTask启动的完整流程:

// ActivityStarter.java - singleTask的复杂处理逻辑
private int setTaskFromIntentRecord(ActivityRecord r) {
    // 1. 检查是否应该创建新任务栈
    boolean newTask = false;
    boolean bringToFront = false;
    
    // 2. 根据launchMode和Flags决定
    if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0 &&
        (r.intent.getFlags() & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0) {
        newTask = true;
    }
    
    // 3. 如果是singleTask,查找匹配的任务栈
    TaskRecord task = null;
    if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) {
        task = findTaskLocked(r);
    }
    
    // 4. 如果找到任务栈
    if (task != null) {
        // 检查栈中是否已有该Activity
        ActivityRecord existing = task.getActivity(r.intent, r.info);
        if (existing != null) {
            // 清除existing之上的所有Activity
            while (task.getTopActivity() != existing) {
                ActivityRecord top = task.getTopActivity();
                if (top != null) {
                    top.finishIfPossible("singleTask");
                }
            }
            
            // 移到栈顶并传递新Intent
            task.moveActivityToFront(existing);
            existing.deliverNewIntentLocked(callingUid, r.intent);
            bringToFront = true;
        }
    }
    
    // 5. 没找到则创建新任务栈
    if (task == null && newTask) {
        task = new TaskRecord(mSupervisor.getNextTaskId(),
                             r.info, r.intent, null, null);
        mService.addTask(task, true);
    }
    
    return task != null ? task.taskId : -1;
}

清除上方Activity

初始状态:
任务栈:[A, B, C(singleTask), D, E]

操作:从E启动C(singleTask)
结果:找到C的实例,清除D和E,C回到栈顶
任务栈:[A, B, C]
日志:C.onNewIntent()被调用,D和E被销毁

不同affinity的singleTask

应用结构:
- MainActivity (standard, affinity=默认)
- LoginActivity (singleTask, affinity=默认)
- SettingsActivity (singleTask, affinity="com.example.settings")

操作流程:
1. 从桌面启动应用
   任务栈1:[MainActivity]

2. Main启动Login
   任务栈1:[MainActivity, LoginActivity]

3. Login启动Settings (不同affinity)
   创建新任务栈2:[SettingsActivity]
   现在有两个任务栈,Settings所在栈在前台

4. 从Settings返回
   销毁Settings,回到任务栈1的Login

从其他应用启动singleTask

假设有应用A和应用BB有Activity X(singleTask)

操作流程:
1. 在应用A中启动X
2. 系统会创建新任务栈(因为不同应用)
3. 任务栈A[A的当前Activity]
   任务栈B[X]
   
4. 在X中启动A的某个Activity
5. 新Activity会进入任务栈A
  • 设计意图:为应用提供明确的"主页"概念;防止关键页面出现多个实例;管理复杂的导航层次结构
  • 适用场景:应用主界面,登录/认证页,全局设置页,深度链接处理页 (处理后返回主流程)。

singleInstance(单实例)

singleInstance是最严格的启动模式,它要求Activity独占一个任务栈,并且该栈中只有它一个Activity。系统范围内只能存在该Activity的一个实例。

// ActivityStarter.java - singleInstance的特殊处理
private int setTaskFromIntentRecord(ActivityRecord r) {
    if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
        // 查找是否已有singleInstance任务栈
        TaskRecord existingTask = findTaskLocked(r);
        
        if (existingTask != null) {
            // 已有实例,直接切换到前台
            mTargetStack = existingTask.stack;
            
            ActivityRecord existing = existingTask.getTopActivity();
            if (existing != null) {
                existing.deliverNewIntentLocked(callingUid, r.intent);
            }
            
            // 确保任务栈在前台
            mTargetStack.moveToFront("singleInstance");
            return existingTask.taskId;
        }
        
        // 创建新的任务栈,并设置singleInstance标志
        TaskRecord task = new TaskRecord(mSupervisor.getNextTaskId(),
                                        r.info, r.intent, null, null);
        task.setTaskToReturnTo(HOME_ACTIVITY_TYPE);
        mService.addTask(task, true);
        
        // 设置任务栈为singleInstance模式
        task.mSingleInstanceMode = true;
        
        return task.taskId;
    }
    return -1;
}

从其他Activity启动singleInstance

应用结构:
- MainActivity (standard)
- CallActivity (singleInstance)
- ChatActivity (standard)

操作流程:
1. 启动Main
   任务栈1:[Main]

2. Main启动Call
   创建新任务栈2:[Call]
   栈2在前台,栈1在后台

3. Call启动Chat
   创建新任务栈3:[Chat]  ← 不会进入栈2!
   或者进入栈1:[Main, Chat](如果允许)

4. 从Chat返回
   回到Call所在的栈2

singleInstance Activity启动其他singleInstance

应用结构:
- VideoCallActivity (singleInstance)
- AudioCallActivity (singleInstance)

操作流程:
1. 启动VideoCall
   任务栈1[VideoCall]

2. VideoCall启动AudioCall
   创建新任务栈2[AudioCall]
   每个singleInstance都有自己独立的任务栈

3. 此时有两个任务栈
   栈1[VideoCall]2[AudioCall]
  • 设计意图:实现全局唯一的重要服务界面,防止与其他界面交互干扰,确保关键界面完全独立。
  • 适用场景:来电/视频通话界面,系统Launcher,全局浮窗/画中画控制器。
  • 注意项:谨慎使用singleInstance,它会破坏正常的导航栈,确保用户有明确的退出方式;考虑与其他应用的交互;在Android 10+上注意后台启动限制。

singleInstancePerTask(单实例每任务模式- Android 12+)

Android 12引入了singleInstancePerTask来解决传统启动模式在大屏幕设备(平板、折叠屏)上的局限性。随着多窗口、分屏模式的普及,传统模式无法很好地处理同一个Activity在多个任务栈中的实例问题。 singleInstancePerTask结合了singleTask和singleInstance的特点:

  1. 在同一个任务栈中保持唯一(类似singleTask)
  2. 允许在不同任务栈中存在多个实例(类似standard)
  3. 不强制独占任务栈,允许其他Activity进入
// Android 12+ 中的新处理逻辑
private int setTaskFromIntentRecord(ActivityRecord r) {
    if (r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK) {
        // 查找当前任务栈中是否已有该Activity
        ActivityRecord existing = null;
        if (mSourceRecord != null) {
            existing = mSourceRecord.task.getActivity(r.intent, r.info);
        }
        
        if (existing != null) {
            // 清除当前任务栈中该实例之上的所有Activity
            mSourceRecord.task.finishActivitiesAboveLocked(existing);
            
            // 传递新Intent
            existing.deliverNewIntentLocked(callingUid, r.intent);
            
            // 移到栈顶
            mSourceRecord.task.moveActivityToFront(existing);
            return existing.task.taskId;
        }
        
        // 当前栈中没有,创建新实例
        // 注意:不寻找其他任务栈
        return -1; // 表示创建新实例
    }
    return -1;
}

多窗口模式下的文档编辑

设备:平板,分屏模式
应用:文档编辑器

窗口1(左侧):
任务栈1[DocList, DocEditor(singleInstancePerTask)]

窗口2(右侧):
任务栈2[DocList, DocEditor(singleInstancePerTask)]

每个窗口有独立的DocEditor实例
但每个任务栈内DocEditor是唯一的

折叠屏的不同状态

// 在折叠屏设备上处理不同状态
class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 监听折叠状态变化
        val foldFeature = findViewById<FoldFeatureView>(R.id.fold_feature)
        foldFeature?.setOnFoldStateChangedListener { isFolded ->
            if (isFolded) {
                // 折叠状态:单窗口
                handleFoldedState()
            } else {
                // 展开状态:可多窗口
                handleExpandedState()
            }
        }
    }
    
    private fun openDetailInNewTask(itemId: String) {
        val intent = Intent(this, DetailActivity::class.java).apply {
            putExtra("item_id", itemId)
            // 在新任务栈中启动
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or
                    Intent.FLAG_ACTIVITY_MULTIPLE_TASK
        }
        startActivity(intent)
    }
}

Intent Flags与启动模式的交互

Intent Flags是Android系统中用于精确控制Activity启动行为的标记集合,它们作为Intent对象的附加指令,影响Activity在任务栈中的管理方式,如是否创建新实例、如何处理现有栈状态或调整导航路径。

Flag效果与启动模式的冲突处理
FLAG_ACTIVITY_NEW_TASK在新任务栈中启动会覆盖standard模式
FLAG_ACTIVITY_SINGLE_TOP等同于singleTop与Manifest的singleTop效果相同
FLAG_ACTIVITY_CLEAR_TOP清除目标Activity之上的所有Activity常与singleTask配合
FLAG_ACTIVITY_CLEAR_TASK清除整个任务栈必须与NEW_TASK一起使用

使用示例:

Intent intent = new Intent(context, MyActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);

重要原则:Intent Flags的优先级高于Manifest中声明的launchMode。

// 在ActivityStarter中处理Flags和launchMode的优先级
private int computeLaunchingTaskFlags() {
    int launchFlags = mStartActivity.intent.getFlags();
    
    // Flags可以覆盖launchMode
    if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
        if ((launchFlags & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
            // 总是创建新任务栈
            return FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK;
        } else {
            // 可能复用现有任务栈
            return FLAG_ACTIVITY_NEW_TASK;
        }
    }
    
    // 否则使用Manifest中的launchMode
    switch (mStartActivity.launchMode) {
        case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
            return FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK;
        case ActivityInfo.LAUNCH_SINGLE_TASK:
            return FLAG_ACTIVITY_NEW_TASK;
        // ... 其他模式
    }
}

启动模式选择指南

场景需求推荐模式替代方案说明
普通页面跳转standard-默认行为
避免重复打开同一页面singleTopFLAG_ACTIVITY_SINGLE_TOP适用于搜索、通知页
应用主页/登录页singleTaskFLAG_ACTIVITY_CLEAR_TOP保证唯一性
全局唯一界面singleInstance-来电、Launcher
多窗口支持singleInstancePerTasksingleTask + 多taskAffinityAndroid 12+
深度链接处理singleTask-清除历史栈

Q&A

Q1:singleTask和FLAG_ACTIVITY_CLEAR_TOP有什么区别?

A

  • singleTask:系统会寻找匹配的任务栈,如果找到该Activity实例,则清除其上的所有Activity并调用onNewIntent();如果没找到,则创建新实例。
  • FLAG_ACTIVITY_CLEAR_TOP:清除目标Activity所在任务栈中它上面的所有Activity。如果目标Activity是standard模式,会销毁并重新创建;如果是singleTop/singleTask,会调用onNewIntent()

Q2:如何实现应用内多任务?

A

可以使用singleInstancePerTask(Android 12+)或结合singleTask+ 不同taskAffinity来实现。例如文档编辑器应用,每个文档在独立的任务栈中编辑。

Q3:从通知栏启动,如何回到正确的页面?

A

需要根据业务逻辑设计合适的启动模式。如果是回到已有实例:

val intent = Intent(context, TargetActivity::class.java).apply {
    flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
            Intent.FLAG_ACTIVITY_SINGLE_TOP
    // 或者使用singleTask模式
}