尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android启动模式全解密:singleTop、singleTask、singleInstance底层机制与实战避坑指南

Android启动模式全解密:singleTop、singleTask、singleInstance底层机制与实战避坑指南 写启动模式这块很多文章要么只贴官方定义要么干脆甩一张表格让你背。但等你真遇到问题——比如“为什么singleTask从通知栏点进去Activity走了onCreate而不是onNewIntent”“为什么singleInstance的Task里还能看到别的页面”“为什么设置了singleTop还是重复创建了实例”你就会发现文档那几句话根本不够用。这篇文章我不想搞成字典式科普而是想从“系统到底怎么决策”这个底层角度把singleTop、singleTask、singleInstance这三个模式彻底讲透每个结论都会告诉你背后的机制以及实际项目里最常用的落地方案。1. 先统一认知Task、返回栈和启动模式的关系在聊这三种模式之前必须先理解一个底层概念Task。Android里的Task翻译过来是“任务”但很多开发把它理解成“栈”这其实不准确。一个Task是一组Activity的集合系统按照“后进先出”的规则来管理这组Activity这个管理结构就是返回栈Back Stack。Task是集合的概念返回栈是物理存储结构之所以要区分是因为后面讲singleTask和singleInstance时系统判断依据是Task而不是栈顶。用户从桌面上点击一个App图标系统首先会查找这个App的“根Activity”所对应的Task。找不到就创建新Task然后把根Activity放进返回栈找到了就直接在这个Task上继续叠新的Activity。默认情况下新启动的Activity总会放进当前Task的栈顶这就是standard模式——每次启动都new一个新实例这个实例被压入启动方所在的Task。很多时候你会发现即使你退出了App再次点开图标界面能恢复到上次退出时的样子就是因为Task和Activity实例依然存在。Task在Android里有一个描述字段叫做taskAffinity默认值是包名这个字段决定了“一个Activity希望归属于哪个Task”。很多坑真的就是从taskAffinity里冒出来的后面我会单独细讲。明白了Task的存在启动模式就好理解了。启动模式其实回答了一个问题当你想启动一个Activity时是要走“常规创建流程”还是先检查一下有没有可复用的实例检查的条件是什么如果复用要不要把栈里的东西清掉这三个问题的不同回答就对应了standard、singleTop、singleTask、singleInstance四种模式。singleTop、singleTask、singleInstance这三个都属于“复用”模式但它们的复用条件、复用范围、对Task的干预程度天差地别。很多老开发有时候都会搞混singleTask和singleInstance更别提新手了。所以下面我把它们一个个拆开直接从系统源码级别的决策逻辑来讲。2. 三种单例模式逐个拆解系统到底做了什么决定2.1 singleTop只看栈顶能复用就复用不复用就新建singleTop的官方定义说得文绉绉“如果要启动的Activity已经在任务栈的栈顶则直接复用并回调onNewIntent否则创建新实例。”翻译成人话就是——系统在启动一个singleTop的Activity时先看一眼“当前Task的栈顶是不是它自己”。是就走onNewIntent复用现有实例。不是就老老实实走onCreate创建新实例。需要注意“栈顶是不是它自己”这个判断只针对“当前Task”。也就是说如果当前操作发生在Task A你需要启动的singleTop Activity在Task B的栈顶那系统根本不会觉得“反正它在别的栈顶复用它吧”它还是会新建一个实例放进当前Task。这个结论在写多Task应用时特别容易踩坑。实际开发里我见过不少人把singleTop用错了场景。最典型的是“防止重复点击导致页面重复创建”。这个场景用singleTop确实有效但前提是用户点击时页面确实已经处于栈顶。比如你在Fragment里放了一个按钮快速点了两次第二次点击时当前Activity还在栈顶singleTop能拦截这次的重复创建。但如果这个Activity上面已经压了一个半透明的弹窗Activity或者跳转是异步的在点击第二次的时候目标Activity已经不是栈顶那singleTop就失效了照样创建两个实例。所以singleTop的适用面很窄只适合“进入该页面后接下来用户操作仍停留在这个页面可能再次触发跳转到同一个页面”的场景。比如聊天页面里点一个聊天对象跳转到同一个聊天页详情页里反复查看同一条信息的按钮或者通知栏里重复点击同一条通知。我做过一个IM类的App聊天页就把launchMode配成了singleTop。用户正在跟A聊天再来一条A的新消息点击通知栏跳转到A的聊天页如果此时聊天页就在栈顶就不重复创建直接刷新消息。这个场景里singleTop是非常完美的因为它不会像singleTask那样顺手把栈顶的其他东西清掉保留用户之前的浏览状态。但singleTop有个让人头疼的设计它没有“清除栈顶其他Activity”的能力。假设用户从MainActivity跳到DetailActivity此时Detail是栈顶然后用户又触发了一个跳转目标是一个也配置了singleTop的SearchActivity那Search不会被复用因为当前栈顶是Detail而是新创建实例压栈。最后返回时Detail、Main都还在返回栈会变得臃肿。这类场景后面讲singleTask时会提到它才是真正的“结构性清理者”。2.2 singleTask把栈内重复的实例清零让目标Activity成为栈顶singleTask的官方定义是“如果存在该Activity所在的Task则在该Task中复用它并清除它之上的所有Activity”。这个描述很容易让人忽略一个细节系统查找的是“该Activity所在的Task”并不一定要求这个Activity在栈顶。只要它在当前Task中的存在不管你栈顶现在是其他页面系统都会把这个Activity上面的所有页面全部弹出销毁然后让这个Activity回到栈顶。这里的关键词是“当前Task”。如果你深入读源码会发现系统查找singleTask目标实例时依据的是Activity的taskAffinity去匹配Task。默认情况下taskAffinity等于应用包名所以绝大多数App里同一个应用的所有Activity都在一个Task里看起来效果就是“这个Activity在全局唯一的任务栈里只有一份”。可一旦给Activity单独设置了taskAffinity结果就大不一样了。举个例子有个页面A配置了singleTask同时taskAffinity设成了“com.example.another”。此时如果这个App已经运行你从其他页面启动A系统会检查是否存在taskAffinity等于com.example.another的Task找不到就创建一个新Task并把A放进去。一旦存在则把该Task里的A复用并清掉它上面的页面。看到没有singleTask的“唯一”并不是全局唯一而是在“同一个taskAffinity的Task”里唯一。这个点很多面试题都会考也是实际开发里最容易出Bug的地方。再说另一个非常容易误解的点singleTask启动后Activity到底会不会走onNewIntent。经常有人调试时发现从A(singleTask所在页面)跳到B再从B启动A结果A确实没重复创建走了onNewIntent但页面重新显示时B没了。没错这就是singleTask的“清除栈顶”副作用。系统复用A后会把A之上的B、C、D全部销毁因此从B“返回”时不会回到B而是直接回到A之前的页面或者桌面。很多App的主页都喜欢用singleTask目的是用“清栈”来保证无论用户从哪个角落回到主页返回栈里只剩主页一个Activity。比如退到后台再点桌面图标像微博、微信这类App点返回键退出后再打开你看到的就是主页而不是之前浏览的某个二级页靠的就是这种清空逻辑。这里手把手算一下假如你的页面结构是Main(singleTask) → Detail → Profile此时栈从上到下是Profile、Detail、Main。你在Profile里通过通知栏启动Main系统做的事情是在Task中查找Main找到复用实例清掉Main以上的Detail和ProfileMain走到栈顶触发onNewIntent最终栈从上到下只有Main。这个逻辑非常粗暴但也非常高效。它保证了主页永远不重复、永远在栈底其他页面一旦从外部入口跳回主页就直接干净利落。实际开发中我一般会提醒自己使用singleTask时至少都要在onNewIntent里重新读取Intent数据并刷新页面。因为复用时不会走onCreate而很多人的页面上次退出时已经被推到后台数据可能有变化如果不主动刷新就会展示旧数据。这个是singleTask最常见的使用坑之一。2.3 singleInstance独一无二的Activity独占一个独立TasksingleInstance在四种模式里是最“孤僻”的。它规定这个Activity所在的Task里只允许存在这一个Activity不允许再压入任何其他Activity。同时这个Activity在全局范围内只会存在一个实例。比如你有一个singleInstance模式的地图页MapActivity。第一次启动它的时候系统会单独创建一个新Task把这个MapActivity放进去。之后无论从哪个页面启动MapActivity系统都只会复用同一个实例。如果此时MapActivity正在前台那直接走它的onNewIntent如果它在后台系统会把整个MapActivity所在的Task整体切换到前台。最让人迷惑的是如果从MapActivity里启动另一个Activity比如点击地图上的商家卡片跳转到商家详情页DetailActivitystandard系统不会把DetailActivity压到MapActivity的Task里而是会把DetailActivity放在启动它的那个“来源Task”中。如果MapActivity是从默认Task里启动的也就是普通App主Task详情页就会自动放到默认Task里。用户按返回键时看到的是FromMapDetail的返回行为然后回到的来源场景通常是发起跳转前的位置。用一句不太恰当但是特别容易记住的话来总结singleInstance就像是一个VIP包间只能一个人在里面而且这个人永远不允许别人进来。从它这里发起的跳转都只能在包间外面进行。正因为singleInstance的这种“独占性”它会对返回栈产生强烈冲击。用户从singleInstance页面退出后并不能回到它所在Task原有的页面——因为这个Task里根本没有别的页面。用户按下返回键或调用finish这个Task就彻底消失系统会切到用户之前停留过的其他Task。如果当前系统里只有一个Task用户就只能退到桌面这会让用户感觉“怎么一下就退出App了”。由于这种体验容易让用户困惑我强烈不建议把业务普通页面设成singleInstance尤其是那种需要频繁跳转或作为中转的页面。它的应用场景其实很窄一般是那种“从任何入口进入都应该弹出同一个页面而且这个页面还不适合和业务栈混在一起”的场景比如全局的悬浮球操作页、系统级拨号界面、语音助手界面。2.4 复用后的onNewIntent才是真正要小心的战场无论singleTop还是singleTask还是singleInstance只要命中了复用条件Activity的生命周期都会走一个特殊流程onPause → onNewIntent → onResume而不会走onCreate。但这里有一个极易被忽略的致命细节如果你在onNewIntent里面不主动调用setIntent(intent)那后面所有通过getIntent()取的都还是旧Intent。很多“支付成功页面数据不刷新”“从通知栏跳转后拿不到最新消息Id”的Bug都是这么来的。我踩过一次很深的坑。当时做的是一个看房App房源详情页设了singleTask通过分享链接跳转到详情页时复用了已有的Activity只能走onNewIntent刷新页面。我当时只在onNewIntent里getIntent().getStringExtra(houseId)结果发现拿到的永远是第一次进入时的数据。原因就是我忽略了getIntent()返回的其实是Activity内部维护的那个mIntent字段而复用时系统不会自动把你新传来的Intent赋给这个字段必须自己在onNewIntent里执行setIntent(intent)再重新取数据。所以我的模板固定成这样你们可以直接抄Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); // 这行不写后面所有getIntent都是旧数据 String id intent.getStringExtra(id); // 根据新id刷新页面数据 refresh(id); }这步做完才算真正用上了复用模式。3. 实战选型把三种模式放到具体业务里来选3.1 配置启动模式之前先想清楚这五个问题每次我在代码里写launchMode之前都会先问自己这样几个问题这个页面是否希望全局最多只有一个实例还是只在栈顶避免重复这个页面被“重复进入”时是否希望把之前的页面全部清掉这个页面返回时用户应该回到哪里这个页面是否会与外部模块通知栏、H5、第三方SDK频繁交互是否依赖FLAG_ACTIVITY_NEW_TASK或FLAG_ACTIVITY_CLEAR_TOP动态切换行为这些问题想清楚了选型基本就出来了。如果只是想“防止重复点击创建重复页面”优先考虑singleTop如果是“主页统一入口 清栈”用singleTask如果是“系统级悬浮页面 不受业务栈影响”才考虑singleInstance。大多数人把singleTask和singleInstance当成集美图里那种“随便选一个效果都一样”的选项实际差别大得很。另外还有一个冷门但关键的知识点launchMode和Intent Flags同时存在时以Flags为准。也就是说只要你在Intent里主动设置了FLAG_ACTIVITY_NEW_TASK或FLAG_ACTIVITY_CLEAR_TOPmanifest里的launchMode就形同虚设。很多方案为了省事喜欢在代码里动态修改结果把manifest配置的模式全打乱了。最好二选一不要混用。3.2 三个高频场景的方案设计第一个高频场景是App主页。几乎每个App都希望“点返回键退出再点图标进来看到的是主页而不是二级页”。我的做法是MainActivity配置singleTask其他跳转到MainActivity的代码统一走一个工具方法Intent只带必要的刷新标记。这样无论从通知栏、H5、扫描二维码还是其他第三方调起进主页系统都会把二级页全部清掉非常干净。第二个高频场景是聊天/IM页面。前面说过聊天页我推荐singleTop而不是singleTask。设想一下用户正在和A聊天突然来了一条B的消息你希望跳转到B的聊天页但如果聊天页是singleTask它会先把当前A聊天页和其他页面全部清掉这很不合理。用singleTop可以保证同样聊天页只有一个实例但只有在页面处于栈顶时才复用不会误伤其他页面。很多IM项目把聊天页设成singleTask用户从A聊天页跳B聊天页时A的聊天记录直接消失被用户吐槽“切个聊天对象上一个对话框就没了”其实这个锅完全不该singleTask背是模式用错了。第三个高频场景是支付/登录回调页面。这类页面的特点是外部H5或SDK回调拉起App内某个Activity希望直接切换到已有页面并刷新状态。我一般会配合Intent的动态flag而不是直接改manifest。比如回调时使用intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);这两个Flag组合起来的效果非常接近singleTask如果存在同一个Activity就复用它并清掉它上面的所有页面。好处是不需要为这个Activity单独设置launchMode不影响它其他入口的启动行为。对于支付回调这类“临门一脚”的场景动态Flags反而更可控。3.3 别忽略了taskAffinity这个隐藏变量前面说的都是默认taskAffinity下的行为。一旦你手动设置了taskAffinity启动模式的表现就会变得很拧巴。最常见的问题是设置了singleTask的Activity为什么从另一个模块跳过来时它每次都创建了新实例原因基本就是它的taskAffinity和跳转方的taskAffinity不在同一个Task里系统找不到目标Task只能新建一个。比如主页MainActivity设置了singleTask但taskAffinity是com.xxx.mainTask详情页所在默认Task是com.xxx包名从详情页跳MainActivity时系统会先找taskAffinity为com.xxx.mainTask的任务栈。如果这个栈不存在因为MainActivity还没启动过就新建一个Task后续的页面跳转也都会跟随这个新Task跑。结果可能出现两个入口各自维护MainActivity实例的乱象。所以我的铁律是一旦某个Activity用了singleTask就不要再给它单独设置taskAffinity除非你极其清楚自己要干什么。绝大多数情况下默认值就够了额外设置只会增加排查成本。再补充一个taskAffinity的常见设计场景跨进程或跨App嵌入页面。比如宿主App里要启动一个独立任务的收银台页面希望这个收银台不占用主任务的返回栈会把taskAffinity设成特殊值并配singleTask或singleInstance。但这类需求一般发生在SDK或组件化架构里普通应用开发真的很少需要主动去改taskAffinity。4. 验证与调试用命令把Task和实例的状态看得明明白白4.1 实战工具dumpsys就是调试启动模式的放大镜纸上谈兵再多不如实际看一眼系统里的返回栈。Android提供了一条非常有用的调试命令dumpsys activity activities。它能把当前系统里所有Task以及每个Task里的Activity实例、launchMode、affinity全部列出来。我在验证启动模式时基本流程是这样的先把App跑起来进入目标页面adb shell dumpsys activity activities搜索自己App的包名重点看task id、affinity、Activity状态复现“重复启动”操作再跑一次dumpsys对比前后差异这条命令打出来的信息量很大要注意几个关键字段TaskRecord代表一个Task后面括号里就是taskIdaffinity确认这个Task归属哪个affinityHist代表返回栈中的Activity条目越靠后的越接近栈顶stateRESUMED表示当前在前台比如你怀疑singleTask没生效用这个命令看一眼就能判断是不是Task里真的有多个同类型Activity还是taskAffinity对不上导致系统根本没找到。另外提一个版本限制Android 10API 29开始系统对dumpsys的权限做了收缩普通应用只能看到自己的Task信息看不到别的App。好在我们调试自己的App时看到自己这侧的信息完全够用。如果你想看系统级Task或第三方应用的就需要有shell权限或者用框架层工具普通开发一般不需要。4.2 最常用的验证小实验怎么证明三种模式的区别我给你们设计一个半小时就能做完的验证实验正好能把三种模式全部覆盖。实验环境就一个MainActivity和一个TestActivity分别设置不同launchMode。MainActivity设singleTaskTestActivity设singleTop从Main启动Test再从Test启动Test注意此时Test在栈顶观察Log你会发现第二次启动Test时只走了onNewIntent没有走onCreate再从Test启动Main观察返回栈变化你会发现Main以上的Test被清掉了最后把MainActivity提前finish再从外部拉起观察new实例的出现很多人做实验时会看Log来判断是否创建了新实例这没问题。但我更建议配合dumpsys一起看因为Log只能告诉你走了哪些生命周期无法告诉你这个实例在哪个Task、哪个位置。调试启动模式时用dumpsys来确认Task结构能省去大量“我觉得它应该在复用”但实际没有的困惑。4.3 问题速查把日常踩到的坑整理成一张表这里我把自己在项目里遇到、以及在答疑贴里看到的案例整理成一张速查表遇到类似问题可以直接对着找答案。现象最常见原因解决办法singleTop设置了但还是重复创建目标Activity不在栈顶或启动方和目标的Task不同改成singleTask或确认栈顶页面singleTask设置了但每次都是新实例taskAffinity不一致或目标Task不存在导致新建统一taskAffinity或去掉自定义affinityonNewIntent拿不到新数据没有调用setIntent(intent)在onNewIntent开头先setIntent从singleInstance页面跳转后返回直接退到桌面singleInstance独占TaskTask销毁后无页面可回换singleTask或重新设计返回流程主页被多次压栈点返回又回到同一个主页使用了standard或singleTop没有清栈能力首页改用singleTask从通知栏跳页面返回时回到了奇怪的位置没有合理设置FlagsActivity归属了错误Task用FLAG_ACTIVITY_NEW_TASK明确指定Task退出SingleTask再启动依然恢复旧栈系统可能只是切到后台并未真正销毁Task需要确认task的remove行为或手动清理这张表不是让我背的是我每次接到启动模式相关Bug时都会先对一遍的检查清单直接排查效率能高不少。5. 经验和本命建议这些坑我替你们先踩了最后聊点实战体会。启动模式这个东西理论很简单真正用起来到处是隐性条件。我自己带新人的时候最常说的一句话是不要试图记答案要试图记“系统判断的步骤”。因为面试官换一个问法情况就完全不同。比如常见的面试题“A(singleTask) → B → C → 再启动A到底会不会走onNewIntent”。如果你只是背了“singleTask会复用并清除”答案是对的。但如果再问一句“如果B设了taskAffinity等于另一个TaskC再启动AB会被清掉吗”这个时候你会发现光记结论完全不够用你得知道系统根据affinity找到的究竟是哪个TaskC当前所在Task里有没有A的实例。我的建议是动手做一个最小Demo把上面几个实验全部跑一遍用dumpsys逐步观察这个过程会帮你把知识点真正焊在脑子里。我自己是在第N次处理“为什么通知栏跳转又创建了一个新页面”的Bug时才彻底把Task、affinity和launchMode的关系吃透之后再看这类问题就再也不慌了。还有一个小细节值得养成习惯如果你给某个Activity配置了singleTask或singleInstance强烈建议在代码里加一段测试用的Log把taskAffinity、taskId、Activity hashcode打出来。这样线上出问题时即使看不到dumpsys也能通过日志中点阵式片段判断是不是同一实例在运行。我通常会在onCreate和onNewIntent各加一条Log注明isNewTask排查效率翻倍。启动模式这个知识点看起来就四个词但往深了挖能牵连出Task、affinity、生命周期、IntentFlags一整条线索。把这个线索理清你写跳转逻辑、处理外部调起、做组件化拆分时都会少踩很多隐形坑。这篇就当我在工位上跟你唠了半上午希望对你有用。
返回列表