协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
mobile-deep-linking【开发与工程】Use this skill when the user says 'deep linking', 'universal link', 'app link', 'URL scheme', 'deferred deep link', 'routing deep link', 'link handler', 'deep navigation'. Implement deep linking across iOS and Android using universal links, custom URL schemes, and deferred deep linking. Do NOT use for: web URL routing or push notification handling.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Guide for implementing deep linking across iOS and Android using universal links, custom URL schemes, and deferred deep linking.
Phrases: "deep linking", "universal link", "app link", "URL scheme", "deferred deep link", "routing deep link", "link handler", "deep navigation"
Deep link configuration: apple-app-site-association JSON, Android intent filters, route mapping table, navigation handler code.
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
6000 tokens
Production or development?
├── Production → Universal Links (iOS) / App Links (Android)
│ Benefits: no confirmation dialog, exclusive to your app, SEO-friendly
│ Requires: HTTPS domain with verification files
├── Development → Custom URL scheme (myapp://)
│ Benefits: works immediately, no server needed
│ Drawbacks: confirmation dialog, multiple apps can register same scheme
└── Both → Universal/App Links for production + URL scheme fallback
URL scheme used when universal link format is needed for internal use
Need to attribute installs?
├── Yes → Branch, Adjust, or AppsFlyer
│ SDK stores click data before install, resolves on first launch
│ Use for: referral programs, ad attribution, personalized onboarding
├── Simple install tracking → Firebase Dynamic Links (deprecated)
│ Note: Firebase Dynamic Links deprecated — migrate to Branch or custom
└── No deferred needed → Standard universal links only
App must be installed for links to work
Navigation framework?
├── Jetpack Navigation (Android) → NavDeepLink with route patterns
│ Auto-extracts path/query params as arguments
├── SwiftUI / UIKit → Custom route registry
│ Parse URL → Match against route table → Navigate
├── Flutter → GoRouter deep link support
│ `router.config()` with initialLocation from deep link
├── React Native → React Navigation linking config
│ `linking.prefixes` + `linking.config` for route mapping
└── Expo → Expo Router + linking config
Built-in deep link support with file-based routing
URI scheme vs universal link vs app link — Three mechanisms for deep linking with different characteristics. Custom URL scheme (e.g., myapp://path): simplest, works in development, shows confirmation dialog on iOS, no HTTPS requirement, no verification, can be claimed by multiple apps. Universal links (iOS): HTTPS URLs that open your app silently, require apple-app-site-association file on server, verified by Apple at install, only your app can claim the domain. Android App Links: equivalent to universal links, require intent-filter with autoVerify, verified by Google via Digital Asset Links JSON. For production, always use universal links / app links — custom schemes are for development only.
Route configuration and mapping — Design a URL structure that mirrors your app's navigation hierarchy. URL path segments map to screen routes, query parameters map to screen arguments. Example: https://app.example.com/profile/42?tab=orders -> screen ProfileScreen with id=42, tab=orders. Maintain a route registry (array/table of pattern -> screen mappings) with support for path parameters (:id), wildcards (*), and optional segments. The parser iterates the registry and returns the first match. Support both path-based and query-based routing.
Deep link setup — iOS — Create apple-app-site-association JSON file (no .json extension) and host at https://{domain}/.well-known/apple-app-site-association. The file maps appID (Team ID + Bundle ID) to allowed URL paths. iOS fetches this file at first install and periodically thereafter. Verify success via device console logs (swcutil or search for [CoreBroker]). In AppDelegate.swift, implement application(_:continue:restorationHandler:) to receive incoming NSUserActivity of type NSUserActivityTypeBrowsingWeb. Extract the webpageURL and pass to your deep link router.
Deep link setup — Android — Add intent-filter to the activity in AndroidManifest.xml that should receive deep links. Include <data android:scheme="https" android:host="app.example.com" /> and android:autoVerify="true". For custom schemes, add a second intent-filter with android:scheme="myapp". Verify app links via Google Search Console: add Digital Asset Links JSON at https://{domain}/.well-known/assetlinks.json. Check verification with adb shell dumpsys package domain-preferred-apps. Handle incoming links in MainActivity.onCreate() or onNewIntent() by extracting the intent data URI.
Deep link handling and routing — Create a unified deep link handler that: (a) parses incoming URL using platform URL parsing, (b) matches against route registry to extract path and query parameters, (c) validates required parameters, (d) checks authentication requirements — if user not logged in, queue the deep link for post-login navigation, (e) pushes the target screen with extracted parameters, (f) tracks the deep link event in analytics. Support both cold start (app not running) and warm start (app in background) scenarios. Deep links arriving while app is in background should navigate from current state, not reset navigation stack.
Deferred deep linking — Standard universal links only work if the app is already installed. Deferred deep links work after install: user taps link -> opens App Store / Play Store -> installs app -> first launch -> SDK identifies the original link -> app navigates to the expected content. Requires an attribution SDK (Branch, Adjust, AppsFlyer, or custom solution). Implementation: SDK generates a tracking link, user taps it, SDK stores click data, on first launch SDK callback delivers the deep link data. Chain: install -> SDK init -> retrieve deferred link -> navigate. Fallback: if no deferred link, navigate to default home screen.
Testing and fallback behavior — iOS simulator: xcrun simctl openurl booted "https://app.example.com/profile/42". Android emulator: adb shell am start -W -a android.intent.action.VIEW -d "https://app.example.com/profile/42". Test with app in foreground, background, and not running. Test with and without app installed. Fallback: when app is not installed, the OS redirects to the website. Configure the webpage at the same URL to redirect to App Store / Play Store. Server-side redirect logic: detect mobile user-agent, redirect to appropriate store. Test deferred links with clean install (uninstall, tap link, install from test track).
| Feature | iOS (Universal Link) | Android (App Link) |
|---|---|---|
| Verification file | apple-app-site-association | .well-known/assetlinks.json |
| File format | JSON (no extension) | JSON |
| Verification timing | App install + periodic | Google Search Console |
| Confirmation prompt | None | None |
| HTTPS required | Yes | Yes |
| Debug verification | Device console logs | adb shell dumpsys |
| Fallback behavior | Opens website | Opens website |
apple-app-site-association with Content-Type: application/json (or application/pkix-cert for iOS). No redirect. Must be HTTPS with valid certificate.swcutil to force refresh or test on device.webpageURL.domain matches your app domain)// apple-app-site-association (no .json extension)
{
"applinks": {
"apps": [],
"details": [{ "appID": "TEAMID.com.example.app", "paths": ["*"] }]
}
}
<!-- Android AndroidManifest.xml -->
<activity android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="app.example.com" />
</intent-filter>
</activity>
In complex apps (e-commerce with multiple brands, white-label apps), deep links may come from multiple domains. Strategy: (a) register all domains in AASA/assetlinks files, (b) create a domain routing table in the app mapping domains to app contexts, (c) validate the domain at link entry before routing. AASA multi-domain format:
{
"applinks": {
"apps": [],
"details": [
{ "appID": "TEAMID.com.example.app", "paths": ["*"] },
{ "appID": "TEAMID.com.example.app", "paths": ["/product/*", "/category/*"] }
]
}
}
iOS enforces that each domain must have its own AASA at /.well-known/apple-app-site-association. Android auto-verifies each domain independently via Digital Asset Links. Route resolution with domain: maintain a Map<DomainPattern, Route> in the route registry. Match the link's domain against the registry before path matching. Reject links from unknown domains with an error event logged to analytics.
Deep links are an attack vector — malicious apps can register URL schemes, craft spoofed universal links, or inject parameters. Security measures: (a) validate the link's originating domain against the registered domain whitelist, (b) for URL schemes, never rely on scheme alone for security — always add a cryptographic signature (JWT or HMAC) to the URL params and validate server-side, (c) sanitize all extracted path/query params: reject non-printable characters, validate enum values, enforce length limits, (d) rate-limit deep link processing — max 1 link per second to prevent brute force, (e) never pass auth tokens or session IDs in deep link URLs (they appear in server logs and browser history), (f) use universal/App Links exclusively for production — disable custom URL schemes except in debug builds, (g) implement a deep link allowlist in the app that specifies which URL patterns are valid, (h) log and alert on unusual deep link traffic (geographic anomalies, frequency spikes).
| Scenario | iOS Command | Android Command |
|---|---|---|
| Cold start (app not running) | xcrun simctl openurl booted "https://app.example.com/profile/42" | adb shell am start -W -a android.intent.action.VIEW -d "https://app.example.com/profile/42" |
| Warm start (app in background) | Same command (app resumes) | Same command (calls onNewIntent) |
| URL scheme (debug) | xcrun simctl openurl booted "myapp://profile/42" | adb shell am start -W -a android.intent.action.VIEW -d "myapp://profile/42" |
| Invalid path (404) | Link should open default screen | Same |
| Malformed params | Should show error or ignore | Same |
| Deferred link (fresh install) | Use Branch/Adjust test dashboard | Same |
| Multiple URL schemes conflict | Only universal link should work | Only verified app link should work |
| Network off (install then link) | Link should queue, resolve on connect | Same |
Test on real devices with: (a) app installed and not installed (fallback), (b) app in all lifecycle states (foreground, background, terminated), (c) with and without network connectivity, (d) across app versions (old app, same app, updated app), (e) with complex paths and query parameters.
What data should the deep link carry?
├── Navigation parameters (path segments, query params)
│ Safe: user_id (number), product_id, screen_name, tab
│ NEVER: auth tokens, session IDs, API keys, passwords
├── Analytics parameters (utm_source, utm_medium, utm_campaign, utm_content)
│ Standard UTM params — compatible with all analytics platforms
│ Pass through to analytics SDK on link resolution
├── Attribution parameters (referrer_id, click_id, ad_id)
│ Used by attribution SDK (Branch, Adjust) — opaque to app
│ Resolved by SDK callback on first launch
├── Experiment parameters (variant_id, experiment_name)
│ A/B test assignment from the link itself
│ Override server-side experiment assignment if present
└── Custom app parameters (deep_link_version, feature_flags)
Future-proof: include a version param for link schema evolution
Always validate all params before using them
Deep link received but app not ready?
├── App not installed → Store redirect (App Store / Play Store)
│ Deferred deep link (Branch/Adjust) captures click, resolves post-install
├── App installed but not logged in → Queue link, route after auth
│ Store URL + params in UserDefaults/SharedPreferences
│ On login completion: check queue → navigate to stored link
│ Clear queue after successful navigation
├── App in onboarding flow → Wait for onboarding to complete
│ Onboarding screens shouldn't be interrupted by deep links
│ Complete onboarding → check deep link queue → navigate
└── Feature not available in current app version → Show upgrade prompt
Server returns min version for each deep link path
If app version < min version, show "Update required" dialog → App Store
Rather than hardcoding deep link URLs, generate them dynamically from server-side. This enables: (a) changing the target destination without app update (use server-side URL routing), (b) A/B testing link destinations, (c) adding analytics tracking parameters dynamically, (d) geo-targeting (different content for different regions), (e) personalization (different landing pages per user segment). Implementation: app sends a link creation request to server with target route + params, server validates, generates a short code or signed URL, returns it to app for sharing. When the link is tapped, server decodes the short URL, enriches with analytics params, and redirects to the actual deep link or fallback web page. Use server-side redirect 302 to track click-through before routing to app or store.
| Failure | Symptom | Mitigation |
|---|---|---|
| AASA not served correctly | Universal link opens website instead of app | Verify Content-Type: application/json, no redirects, HTTPS |
| iOS AASA cache stale | Link stops working after path changes | Clear Safari cache, test with swcutil |
| Android auto-verify timeout | App link not verified for hours | Use Digital Asset Links API, test with adb shell dumpsys |
| Deferred link race condition | Deep link not delivered on first launch | Queue deep link, resolve after SDK init completes |
| URL scheme conflict | Wrong app opens | Use universal/App Links exclusively |
| Deep link during onboarding | User not logged in | Queue link, replay after login completes |
| Link targeting wrong app version | Feature doesn't exist | Server-side version targeting in link generator |
https://domain/.well-known/apple-app-site-associationhttps://domain/.well-known/assetlinks.jsoncurl -v https://domain/.well-known/apple-app-site-association (check Content-Type)[CoreBroker] messages indicating AASA fetch statusadb shell dumpsys package domain-preferred-apps to verify Android app link statusimport UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]) -> Void
) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL else {
return false
}
return DeepLinkRouter.shared.handle(url: incomingURL)
}
// Custom URL scheme fallback (development only)
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
guard !url.absoluteString.contains("localhost") else { return false }
return DeepLinkRouter.shared.handle(url: url)
}
}
class DeepLinkRouter {
static let shared = DeepLinkRouter()
private var routeTable: [(pattern: String, handler: ([String: String]) -> Void)] = []
func register(pattern: String, handler: @escaping ([String: String]) -> Void) {
routeTable.append((pattern, handler))
}
func handle(url: URL) -> Bool {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
return false
}
let path = components.path
var params: [String: String] = [:]
components.queryItems?.forEach { params[$0.name] = $0.value }
// Auth check
guard AuthManager.shared.isLoggedIn else {
PendingLinkManager.shared.store(url: url)
return true
}
for route in routeTable {
if let extracted = matchPath(path: path, pattern: route.pattern) {
params.merge(extracted) { (current, _) in current }
route.handler(params)
return true
}
}
Analytics.logEvent("deep_link_unhandled", parameters: ["url": url.absoluteString])
return false
}
private func matchPath(path: String, pattern: String) -> [String: String]? {
let pathParts = path.split(separator: "/").map(String.init)
let patternParts = pattern.split(separator: "/").map(String.init)
guard pathParts.count == patternParts.count else { return nil }
var params: [String: String] = [:]
for (p, pat) in zip(pathParts, patternParts) {
if pat.hasPrefix(":") {
params[String(pat.dropFirst())] = p
} else if p != pat {
return nil
}
}
return params
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleDeepLink(intent)
}
private fun handleDeepLink(intent: Intent?) {
val data = intent?.data ?: return
val path = data.path ?: return
val params = mutableMapOf<String, String>()
data.queryParameterNames?.forEach { name ->
data.getQueryParameter(name)?.let { params[name] = it }
}
// Validate domain
val host = data.host ?: ""
if (host != "app.example.com") {
Analytics.logEvent("deep_link_invalid_domain", params)
return
}
// Auth gate
if (!AuthManager.isLoggedIn()) {
PendingLinkManager.store(data)
startActivity(Intent(this, LoginActivity::class.java))
return
}
DeepLinkRouter.navigate(this, path, params)
}
}
import 'package:go_router/go_router.dart';
final router = GoRouter(
initialLocation: '/home',
routes: [
GoRoute(
path: '/',
redirect: (context, state) => '/home',
),
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/profile/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
final tab = state.uri.queryParameters['tab'];
return ProfileScreen(userId: id, initialTab: tab);
},
),
GoRoute(
path: '/orders/:orderId',
builder: (context, state) {
final orderId = state.pathParameters['orderId']!;
return OrderDetailScreen(orderId: orderId);
},
),
],
);
void main() {
WidgetsFlutterBinding.ensureInitialized();
DeepLinkService().init(); // Initialize deferred deep link SDK
runApp(MyApp());
}
import { LinkingOptions } from '@react-navigation/native';
const linking: LinkingOptions<RootStackParamList> = {
prefixes: ['https://app.example.com', 'myapp://'],
config: {
screens: {
Home: '',
Profile: 'profile/:userId',
OrderDetail: 'orders/:orderId',
Settings: 'settings',
Product: {
path: 'product/:productId',
parse: { productId: Number },
},
},
},
// Handle auth-gated links
getStateFromPath: (path, config) => {
if (!AuthManager.isLoggedIn) {
PendingLinkService.store(path);
return { routes: [{ name: 'Login' }] };
}
return undefined; // Use default resolver
},
};
// In App.tsx:
// <NavigationContainer linking={linking}>
Hand off to mobile-analytics skill when deep link attribution and conversion tracking is needed.
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).