Activity Advanced: Reshaping Underlying Cognition of Task Stacks, Launch Modes, and taskAffinity
1. Mechanism Internals: A Task is Not an Array Stack, But a System Container Tree Node
Before discussing any scheduling rules, one must see through the actual physical structures residing in memory. The so-called "Task Stack" was represented in early AMS (Android 9.0 and prior) through a hierarchy from ActivityStackSupervisor, ActivityStack, TaskRecord, down to ActivityRecord. However, as the OS evolved to eradicate black/frozen screens caused by synchronization latency between AMS and WMS (Window Manager Service), Android fundamentally merged Activity lifecycle management with the screen rendering tree at the lowest levels. This resulted in a unified WindowContainer tree structure for expression:
RootWindowContainer
└── DisplayContent (Represents a true physical screen, or a virtual display)
└── TaskDisplayArea (The visual display boundary)
└── Task (This is what we commonly refer to as the Task Stack)
├── Task (Since Android 11, Tasks can nest to support split-screen)
└── ActivityRecord (The leaf physical node, representing a physically existent Activity instance)
In this model:
ActivityRecord
This is the true, singular system mirror representing the MyActivity class instance you wrote in code. A single Activity object instantiated via new in the application process invariably corresponds to an ActivityRecord instance object on the ATMS side. Within this node resides not only the Activity's Intent and current lifecycle state, but it directly holds the appToken. WMS leverages this token to establish a binding relationship with the WindowState (window UI buffer) on the screen. This is the fundamental physical reason why killing an Activity at the application layer without proper WMS unbinding results in memory leaks.
Task
Do not conceptualize this as a List<Activity>. Deep in the Task.java source code, it is an advanced rendering container unit possessing display scheduling capabilities. A Task is the foundational physical slice unit in Android handling application layer switching, state preservation, and rendering within Recents (the recent tasks overview). When you swipe away a card in the multi-tasking interface, you are slaughtering a Task container and its entire enclosed ActivityRecord tree.
2. ATMS Source Code Autopsy: What Trade-offs Does LaunchMode Actually Make?
When your application invokes startActivity(), this cross-process request ultimately penetrates the system_server process via Binder, is received by ATMS, and handed over to ActivityStarter.java for immensely complex legality validations. This involves permission checks and black/gray market interception. Its core action is deciding based on your declared LaunchMode: Do I reuse an existing node? Do I destroy existing nodes? Do I mount to a new container?
standard: The Default but Violent Cloning Mechanism
The source code logic is the most straightforward: absolutely zero reuse query logic. ATMS will directly assemble a new ActivityRecord and blindly addChild it to the caller's resident Task, following the current caller's context.
Why this way? It pushes the construction cost back onto the application layer. The system assumes that for this standard page, you do not care about burning extra heap memory or window buffers to isolate these entirely independent UI nodes.
singleTop: The Short-Lived Stack-Top Interceptor
When a routing request carrying the singleTop mode arrives, the core routing logic in ActivityStarter reads the trailing element of the target Task's mChildren (i.e., the topmost ActivityRecord) and compares its ComponentName. If it hits, ATMS will entirely bypass creation and push behaviors, instead triggering deliverNewIntent() on the target ActivityRecord for dispatch. Ultimately, your code executes the onNewIntent callback, accomplishing UI refresh within the lifecycle reentry to avoid reconstruction overhead.
Engineering Pitfall Prevention: Inside
onNewIntent, the defaultgetIntent()internal pointer remains tethered to the old Intent from the instance's initial creation. This is deliberate framework design (preserving the ability to trace the original awakening condition). If you rely on new parameters to refresh business logic, you must mindlessly appendsetIntent(intent);on the very first line to overwrite the Record's internal pointer!
singleTask: The Global Scheduler with Destructive Semantics
singleTask does not simply mean "keep only one instance in this stack." Its behavior is systemic and highly destructive:
- Global Addressing (Find Task):
ActivityStarterdoes not restrict its search to the currentTaskor even the current app's process. It traverses upward along the current display area to find if anyTasksatisfies its desiredtaskAffinity, and then searches within thatTaskto see if anActivityRecordnode with the identicalComponentNamewas ever mounted. - Intra-Stack Truncation (Clear Top): The moment it discovers the target
ActivityRecorddeep within an existingTasktree, to float it to the surface, the system does not pull the bottom out and insert it at the top (which would violate the linear time constraints of stack push/pop rules). ATMS acts ruthlessly: It executes theperformClearTaskStarting()method, traversing allActivityRecords resting above the target node, and directly triggers their destruction. This is equivalent to chopping off the branches of that node. - "Creation" is merely a Forced Escape (Conditional Trigger): Many tutorials rigidly equate "
singleTask+ differenttaskAffinity" to "inevitably creates a new task stack." This is flawed at the logical bedrock. When you launch with this configuration, ATMS primarily executes a full-stack search to see if an old Task tree matching thattaskAffinityslumbers in the background. Only when the entire OS completely fails to find a matching old tree does it execute the creation action (Create a new Task). If it happens to find a matching legacy tree, it executes a ruthless "grapple," violently yanking that old tree to the system foreground, and directly mounts or reuses your Activity within it (absolutely no new creation).
Design Motivation: This forced pruning behavior is frequently deployed to protect heavy-duty root nodes like an App's home page. From the perspective of system memory management and architectural stability, root nodes (like the WeChat home page) maintaining massive global observer listeners and heavy business aggregation logic expose extreme vulnerabilities to OOM and state synchronization failures if arbitrary top-stack cloning is allowed. Pruning the top branches is a guaranteed safe landing mechanism.
singleInstance: The Pure, Untainted Exclusive Domain and Edge-Case Deductions
Far more extreme than singleTask, it rigidly enforces bi-directional uniqueness constraints for the ActivityRecord and its Task container. The system must establish a solitary Task that mounts only this specific ActivityRecord.
Once situated within this "absolute isolation" container, if you initiate another routing request from within it, highly deceptive underlying scheduling mutations trigger:
-
Homogeneous Repulsion (A launches A itself): Due to bi-directional uniqueness, ATMS instantly hits that existing solitary
Taskduring global addressing. It will never splinter a second parallel stack due to the relaunch. Instead, it directly paths through the intra-stack reuse mechanism, calling the existing A instance'sonNewIntent(). At this moment, the Task stack count remains unchanged, and the Activity instance remains solely at 1. -
Heterogeneous Rejection (A launches B - standard mode): This immediately triggers the
singleInstanceanti-contamination security baseline—in this VIP isolation ward, no outsiders are permitted. When you executestartActivity(B)inside A, the underlyingActivityStarterdetects that the current source Task is exclusively locked. It will forcefully stamp this implicit invocation with theFLAG_ACTIVITY_NEW_TASKengine flag. It then unsheathes B'staskAffinityprobe to address the outside world:- Even if B's affinity is entirely identical to A's, the system will deliberately bypass A's VIP stack during Task traversal.
- The system searches for other standard Tasks matching the affinity (e.g., the default Task of the app's main process retreating to the background). If found, B is directly mounted there, and the main Task is instantly yanked to the foreground, obscuring A.
- If not found? It directly clones an entirely new tree-shaped container Task specifically to house B.
-
Archipelago of Isolation (Launching an entirely different singleInstance C): Because every instance declaring this attribute swears sovereign independence. Thus, even if you sequentially launch 5 different class names of
singleInstanceActivities, the OS'sWindowContainerwill unhesitatingly bifurcate laterally into 5 completely parallel, distinct Task trees.
Business Design Projection: Why does the OS need to maintain this mechanism laced with severe "immune rejection reactions"? This is the final defensive line against rendering-level hijacking and UI injection attacks (Tapjacking). For interfaces like "System-level incoming call pop-ups" or "High-security finger-vein verification pages," third-party ad SDKs or hidden WebView hooks must absolutely be prevented from utilizing the opportunity within the same stack structure to quietly overlay malicious masks on top to deceive the user. It is a "biohazard suit" possessing military-grade vacuum physical isolation.
3. taskAffinity Source Code Routing Protocol: An Addressing Probe, Not a Static Placard
If we consider Tasks as shipping containers, many tutorials claim taskAffinity dictates which container the Activity belongs to. This is a massive inversion of causality.
In the ATMS source code definition, taskAffinity merely represents a preferred String string carried by the Activity (defaulting to the main project's package name application.packageName injected during build). It is NOT the ID of the container, but rather an "Affinity Router probe" utilized when matching a new home.
This probe only truly takes effect when Cross-Task Routing (New Task Routing) is triggered.
Generally, the following conditions activate the cross-task computation engine, causing taskAffinity to be queried:
- Your Manifest's
launchModeis declared assingleTask/singleInstance. - You utilize the dynamic routing override flag
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)during a runtime invocation.
When executing FLAG_ACTIVITY_NEW_TASK, as the system executes the internal getReusableTask() traversing all Task containers system-wide for a match, it extracts the affinity string of the current Task's head node for a brute-force Equals check. A match injects into that Task; a miss forges a new Task. If there are no flags triggering the logic to recreate the container, even if we configure a special taskAffinity="com.google.isolated" in the manifest, it will silently resign to its fate, methodically populated into the Task of the current caller, failing to exert any probe preference.
Mutual Exclusion and Combination of Underlying Mechanisms: The singular correct approach when utilizing
taskAffinityto separate distinct independent business lines (e.g., isolating a specialized playback interface) is: Simultaneously configure a specifictaskAffinityANDFLAG_ACTIVITY_NEW_TASK(or the corresponding manifest launch mode). Both are indispensable. This is an ingenious decoupling layer designed to balance not destroying static configuration parsing efficiency while retaining the flexibility of runtime dynamic routing addressing.
4. Intent Flags: Runtime Override Commanders Surpassing Manifest Configurations
The system permits us to hardcode configurations in the AndroidManifest, but in complex microservices, plugin architectures, and combinatorial penetrations between differing modules, static control rights fall vastly short of the advanced programmer's requirement for "contextual state awareness" resets. Therefore, the provided runtime Intent Flags essentially serve as system scheduling override commands possessing superior judgment priority:
-
FLAG_ACTIVITY_NEW_TASK: Its core existence is not limited to the aforementioned "triggering cross-Task lookup mechanisms." From an OS perspective, this is the security baseline preventing system crashes. For example, when aServicebackground daemon wakes a UI page, or when you execute an operation using an extremeApplication Context. Because this purely computational memory downstream flow resides in zero visual rendering buffer containers, it fundamentally possesses no knowledge of whichTasktree to mount to. Without this Flag, the system will outright discard the request, leading to extremely easily caught system-level exceptions (AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag). -
FLAG_ACTIVITY_CLEAR_TOP: Utilized to implement soft-landing cleanup operations on the business layer. When combined with the standardsingleTop(i.e., mixingaddFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)), its objective is: Locate that node instance within the current chain, violently sever its subsequent developmental branches, retreat to the node itself, and accomplish a reunion through theonNewIntentupdate, rather than directly destroying everything and rebuilding. (If not paired withSINGLE_TOP, usingCLEAR_TOPalone will, by default, kill the node itself and then rebuild and push a new instantiated Record. This is a highly perilous implicit detail recognized by very few!).
5. Dangerous Engineering Failure Boundary Models: The Untouchable Back-Stack Trap
As a hardcore developer possessing industrial depth, one must penetrate all the source-returning dead links this tiered architectural design can manufacture in cross-system environments, avoiding self-made ghostly chaos like "pressing the back button magically skipped the previous App" that shatters the user experience.
[Failure Boundary 1]: Process Pseudo-Disconnection Caused by Back-Stack Path Penetration
When you launch an activity page of App B carrying the FLAG_ACTIVITY_NEW_TASK flag from within App A (common when a third-party app launches a browser or an Alipay page), ATMS calculates that B's taskAffinity is not identical to A's. Consequently, in Recents and the underlying containers, ATMS directly strips them to form two entirely independent containers.
However, this does not mean the user's perception is truly independent. The system possesses a hidden behavior: ActivityStack stack switching chaining. For a smooth experience, when the later-launched B-Task stack empties upon back-navigation, it automatically resumes the foreground rendering display of the previously out-of-focus A-Task. But this ONLY occurs during standard back-navigation flows. If, at this moment in the B-Task, some non-standard cleanup is triggered, or due to severe memory suppression B's host service forcefully retries, A's stack is highly likely to fail to promptly recall to the foreground to become mResumedActivity during the focus layer jump, becoming suspended or even hibernated. This is the origin of supernatural events like third-party SDK payment pop-ups causing the app to drop offline to the background before returning.
[Failure Boundary 2]: Background Autostart Interception (Android 10+ Background Start Restrictions)
Having comprehended the pre-control logic of ActivityRecord, we should know that launching is not an unconditional service provided to you by the system. Specifically in ATMS, to restrain mindless resident services from popping rogue inductive advertisement overlays, starting from Android 10, the underlying ActivityStarter entry point appended rigid system state validations. That is, regardless of how your component demands a jump, if the underlying verification determines the calling process's UidRecord is not mounted to the absolute foreground or visible state, it is directly rejected or transcribed into a Notification (unless applying for the special floating permission SYSTEM_ALERT_WINDOW or triggered forward by a physical user button/notification bar PendingIntent instruction).
[Failure Boundary 3]: taskAffinity Forgery and the StrandHogg Task Hijacking Defense Line
Many geeks ponder a dangerous vulnerability: Since the system relies on matching the taskAffinity string to find the Target Task, what if I write a malicious App, forcefully declare my Activity's taskAffinity in the Manifest as com.android.settings (System Settings) or com.xxx.bank (a specific bank app), and then trigger it from the background? Will it stealthily be injected to the top of the real system application's stack?
The answer is: In earlier Android versions, YES! This was the StrandHogg (Task Stack Hijacking) vulnerability that once shook the security world.
Hackers utilized this identity string forgery to mount their phishing Activity to the top of a well-known App's Task. When the user clicked the real bank App icon from the desktop, the underlying ATMS discovered the Task already existed, and thus directly brought it to the foreground—the result being the user saw the phishing input box the hacker had preemptively pressed on top (leveraging the natural trust of the Task, the user was defenseless).
To counter this, starting from Android 11 (API 30), ATMS introduced extremely strict cross-Uid (cross-process owner) protection validations. Modern Android, when executing the string Equals matching of taskAffinity, mandates underlying UidRecord authentication. If the mounting Activity does not possess the same signature or shared Uid (android:sharedUserId) as the target Task, ATMS coldly rejects this "cuckoo in the nest" invasion behavior, forcefully blocking cross-application black-box mounting, thoroughly crushing the attack path of utilizing taskAffinity forgery for stack hijacking.
Where does it go after being rejected? Since it is locked out of the target stack, ATMS in the source code directly evaluates this match as "Reusable Task Not Found (Task Match Failed)". The subsequent destination depends on your launch parameters:
- New Stack Isolation: If your launch carried
FLAG_ACTIVITY_NEW_TASK(or configuredsingleTask/singleInstance), since it cannot merge into the target, ATMS initiates the fallback plan—forcefully creating a completely new Task tree within the system entirely under the control of your malicious App's Uid. Although in memory it carries the Affinity string of the system name, in physical entity and the multi-tasking list (Recents cards), it is utterly flattened and forever separated from the victim container you covet, fundamentally incapable of completing an overlapping cover attack. - Imprisoned in Place: If it is a standard launch (without the New Task flag, merely hoping to exploit the
allowTaskReparentingattribute), it won't even secure a newly created tree. After the evaluation fails, it automatically falls back and is unceremoniously pressed firmly into the caller's task stack of your own malicious App.
6. allowTaskReparenting: The Dynamic "Returning to Ancestry" Mechanism Across Container Trees
Since we have delved into the boundaries of taskAffinity, we absolutely cannot bypass the most ghostly switch in AndroidManifest: android:allowTaskReparenting="true".
If launchMode constitutes the traffic laws at departure, then allowTaskReparenting is the spacetime jump exploit during transit. It permits a living ActivityRecord instance, mid-lifecycle, to be forcefully uprooted by ATMS from one Task tree and transplanted onto another.
How is it Triggered?
Assume a page in App A is configured with allowTaskReparenting="true" and carries its own taskAffinity="com.A.app". Its "jump" mechanism adheres to the following physical laws:
<!-- App A (e.g., a Browser)'s AndroidManifest.xml -->
<application
android:taskAffinity="com.a.app"
... >
<!-- Standard Home Page -->
<activity
android:name=".HomeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Web Browsing Page: Declared capable of returning to ancestry -->
<activity
android:name=".WebViewActivity"
android:taskAffinity="com.a.app"
android:allowTaskReparenting="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
</intent-filter>
</activity>
</application>
When App B (e.g., Zhihu) pulls up this page, no special Flags are required—a standard implicit startActivity suffices to trigger the subsequent returning-to-ancestry logic:
// Clicking an external link inside Zhihu, standard implicit startActivity, NO NEW_TASK
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com/article"))
startActivity(intent)
// A.WebViewActivity is pushed to the top of Zhihu's Task stack. Pressing back now returns to Zhihu.
// BUT, if the user switches out and actively taps A's icon, this page will "go home" and hang at the top of A's stack.
- Lodging Under Another's Roof (Initial State): App B implicitly pulls up this page of App A from within its own Task, without adding any Flags. Because it lacked
NEW_TASK, Page A is treated as a standard child node, parasiting the top of B's Task tree. At this moment, pressing back pops A and reveals B. This perfectly aligns with user continuity perception. - Returning to Ancestry (Jump State): The user does not press back, but presses the Home button to return to the desktop, and subsequently clicks App A's launch icon on the desktop.
- ATMS Grafting (Source Code Level): When the system prepares to pull A's main Task from the background to the foreground, ATMS triggers a global scan to locate all isolated nodes declaring
allowTaskReparentingwith an matchingaffinityofcom.A.app. It discovers that the Page A sitting atop B's stack perfectly matches! Driven by "kinship summoning," the underlyingWindowContainerexecutes physical redirection—directly forcefully erasing the parent pointer of thatActivityRecordfrom B-Task, and re-addChild-ing it to the top of A-Task's tree. When the screen re-illuminates, the user still sees the page just pulled up by B, except if you press back now, underneath is no longer App B, but App A's home page.
Why this way? Why did the lower levels design such a bait-and-switch behavior?
The core of this mechanism is a compromise and advanced encapsulation tailored for componentization of service experiences.
The most representative business projection: Browser core pages. Imagine you are using "Zhihu (App B)" and click an external link. Zhihu uses an Intent to pull up your "Chrome System Browser (App A)'s" web browsing Activity. For immersion, this webpage defaults to overlaying the top of Zhihu's stack (the user feels they are still browsing Zhihu). But if the user presses the Home button to switch out, and actively taps the Chrome icon. If the system lacked the Reparent mechanism, Chrome could only open a new blank homepage, and that article page you just opened from Zhihu, possibly right at the climax, would be permanently locked and forgotten in the invisible task stack at the bottom of Zhihu!
To thoroughly stitch together the experiential tear between "cross-application container borrowing" and "component origin belonging," Google implanted this dynamic stack-shifting schema extremely early in the low levels: When you awaken the container's sovereignty via a shortcut (tapping Chrome), all components previously floating outside "working for others" automatically retreat to defend the mother ship's main base.
This is also why in modern security frameworks (as detailed in Failure Boundary 3 earlier), this "returning to ancestry" ability permitting overriding merges has been strictly confined to the shared underlying Uid or explicitly trusted invocation source systems, preventing it from degenerating into a Trojan horse utilized by rogue apps to hijack users.
All superficially complex jump parameters (Mode, Affinity, Reparenting, Intent Flags), when peeled back to reach their underlying structural orchestration, efficiency, and container contamination safety compromise computation frameworks, allow this system to self-originate its source code control loops directly within your mind. This, truly, is the fundamental internal martial art of writing bug-free code.