2. URI Scheme 原理与注册流程
URI Scheme 是操作系统识别并路由深层链接的核心机制。本章从 Scheme 语法规范、双平台注册方式、冲突处理到安全校验,完整覆盖从原理到生产的实现路径。
2.1 URI Scheme 语法基础
URI Scheme 遵循 RFC 3986 规范,标准格式为:
scheme://host/path?query#fragment
各部分含义:
| 部分 | 示例 | 说明 |
|---|---|---|
| scheme | myapp | 协议名称,需要小写字母开头,后跟字母/数字/+/./- |
| host | open | 通常为商品类目或动作标识 |
| path | /product/123 | 资源路径,与 HTTP URL 的路径类似 |
| query | ?from=share | 参数,用于传递上下文 |
| fragment | #detail | 锚点,部分平台忽略 |
命名建议:自定义 Scheme 建议采用反向域名形式(例如 com.example.myapp://),可以有效降低与其他应用的冲突概率,同时便于团队维护。
2.2 Android 注册方式
Android 通过 AndroidManifest.xml 中的 <intent-filter> 声明 Scheme:
<activity android:name=".DeepLinkActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="myapp"
android:host="open"
android:pathPrefix="/product" />
</intent-filter>
</activity>
android:scheme— 必须,指定 URI 协议名。android:host— 可选,用于二次过滤。android:pathPrefix/android:pathPattern— 可选,匹配路径。android.intent.category.BROWSABLE— 允许来自浏览器的跳转。
2.3 iOS 注册方式
iOS 在 Info.plist 中配置 CFBundleURLTypes:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
同时需要在 SceneDelegate (iOS 13+) 或 AppDelegate 中实现处理回调:
// iOS 13+ SceneDelegate
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
handleDeepLink(url: url)
}
// iOS 12 及以下 AppDelegate
func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
handleDeepLink(url: url)
return true
}
2.4 多 Scheme 冲突处理
当多个应用注册相同 Scheme 时,系统行为因平台而异:
| 平台 | 默认行为 | 用户选择 | 开发者应对 |
|---|---|---|---|
| Android | 弹出“完成操作方式”选择器 | 可勾选“一律使用” | 优先使用独特前缀(如域名反转) |
| iOS | 仅首个注册 App 生效(无选择器) | 用户可手动修改系统设置 | 建议使用 Universal Link 替代纯 Scheme |
| HarmonyOS | 与 Android 类似,弹出选择器 | 可记忆选择 | 显式声明 priority 属性 |
常见错误:使用 http/https 作为自定义 Scheme 会导致与系统浏览器冲突,应严格避免。推荐长度不少于 6 字符、包含至少一个点号(如 shop.myapp)。
2.5 安全校验
URI Scheme 的安全校验必须在业务处理前完成,防止恶意 App 构造虚假 URI 劫持或欺骗用户。校验清单:
- 来源校验:检查
referrer(Android)或sourceApplication(iOS)是否为已知可信包 - host & path 白名单:只处理预定义的合法 host 和 path
- 参数消毒:对 query 参数进行 URL 解码后校验,防止 SQL 注入/XSS
- 深度字符限制:限制 URI 总长度(一般 ≤2048 字符)
- 签名校验(Android):通过
PackageManager获取调用者签名,比对白名单
安全校验代码示例(Android):
fun isTrustedSource(context: Context, intent: Intent): Boolean {
val referrer = intent.getStringExtra("referrer") ?: return false
val packageName = extractPackageFromReferrer(referrer) ?: return false
val trustedPackages = listOf("com.example.legitimate")
if (packageName !in trustedPackages) return false
val packageManager = context.packageManager
val signature = packageManager.getPackageInfo(packageName,
PackageManager.GET_SIGNATURES).signatures[0].toCharsString()
return signature == EXPECTED_SIGNATURE_HASH
}
2.6 调试技巧
测试 URI Scheme 是否正常注册:
- Android:执行
adb shell am start -W -a android.intent.action.VIEW -d "myapp://open/product/123",观察日志中 ActivityManager 的输出。 - iOS:在 Safari 地址栏直接输入
myapp://,检查是否弹出提示“打开此页面”。 - 通用:在终端使用
open命令(macOS)或第三方工具 URL Scanner 验证。
2.7 常见错误与稳定性优化
| 错误场景 | 原因 | 解决方案 |
|---|---|---|
| URI 无响应(App 未弹出) | Scheme 未正确声明或大小写不匹配 | 检查 Manifest/Info.plist;Scheme 强制小写 |
| Android Intent 参数丢失 | 未正确调用 getData() 或 getExtras() | 使用 intent.data 读取完整 URI |
| iOS 中从外部调用无回调 | 未实现 SceneDelegate 的 openURLContexts | 添加对应代理方法,注意 iOS 13+ 的分支 |
| 冲突导致弹窗干扰用户 | 与第三方应用共用通用 Scheme | 升级为唯一反向域名 Scheme;或采用 Android App Links / iOS Universal Link |
| 安全漏洞(任意 App 可打开) | 缺少签名校验或来源阻断 | 实施第 2.5 节中的完整校验流程 |
性能优化建议:在 onCreate 或 SceneDelegate 入口处尽快处理深度链接,避免主线程阻塞。将 URI 解析、参数校验放到异步协程/队列中,确保启动耗时低于 100ms。