2. URI Scheme深度解析
自定义 URI Scheme 是 Deep Link 最古老、兼容性最广的实现方式。它不依赖域名验证,也不需要服务端配合,只要 App 在系统里注册了某个 scheme,任何应用或网页都能通过一个形如 myapp://path?key=value 的字符串唤起它。理解它的语法结构、双端注册机制与跳转差异,是排查“点了没反应”“跳错 App”“参数丢失”等问题的前提。
一、URI Scheme 的语法结构
一个完整的自定义 Scheme URI 遵循 RFC 3986 的通用语法,可拆解为以下几个部分:
myapp://product/detail/10086?from=push&ts=1710000000#reviews
└─┬─┘ └──────┬──────┘└────┬─────┘└────────┬────────┘└──┬──┘
scheme authority path query fragment
- scheme:协议名,自定义时通常使用 App 品牌名,如
myapp、taobao。它只能包含字母、数字、+、-、.,且必须以字母开头,大小写不敏感(系统匹配时通常按小写处理)。 - authority(host):
//之后到第一个/、?或#之前的部分。自定义 Scheme 中它没有 DNS 含义,但常被用作“业务域”,例如product、user、order。 - path:以
/分隔的路径段,用于定位具体页面,如/detail/10086。 - query:
?之后的键值对,多个参数用&分隔。值必须做 URL 编码,否则&、=、中文、空格会破坏解析。 - fragment:
#之后的部分。在自定义 Scheme 中它通常被忽略,但部分框架会把它当作锚点或透传参数。
需要特别注意的是:scheme 与 host 之间必须有两个斜杠。写成 myapp:product/detail 虽然语法上合法,但 Android 的 Uri.parse 会把它解析成 scheme=myapp、schemeSpecificPart=product/detail,host 为空,很多路由框架会因此匹配失败。生产环境应统一使用 myapp://host/path 形式。
二、Android 的注册机制
Android 通过 AndroidManifest.xml 中的 <intent-filter> 声明 scheme。系统在安装时解析所有 Activity 的 intent-filter,建立“scheme → 组件”的映射表。
<activity android:name=".deeplink.RouterActivity">
<intent-filter android:autoVerify="false">
<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="product"
android:pathPrefix="/detail" />
</intent-filter>
</activity>
关键点有三:其一,BROWSABLE 类别必须声明,否则从浏览器点击链接无法唤起;其二,data 的 scheme、host、path 是“与”关系,只有全部匹配才会命中该 filter;其三,多个 Activity 可以声明相同 scheme,此时系统会弹出选择器让用户挑选,这往往是“跳错页面”的根源。Android 12 起,若未显式声明 android:exported="true",带 intent-filter 的组件将无法安装,这是升级后常见的构建失败原因。
三、iOS 的注册机制
iOS 在 Info.plist 的 CFBundleURLTypes 中注册 scheme:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
系统在 App 安装时把 scheme 写入 LaunchServices 数据库。当有 URL 打开请求时,系统按“最近使用优先”或“注册顺序”选择处理者,没有唯一性保证。这意味着另一个 App 只要也注册了 myapp,就可能抢走跳转。iOS 的处理入口是 AppDelegate 的 application(_:open:options:),在 Scene 架构下则是 scene(_:openURLContexts:)。若 App 尚未启动,系统会先启动 App 再回调;若已在后台,则直接回调并切到前台。
四、双端跳转行为差异
Android 与 iOS 在自定义 Scheme 上的行为差异,直接影响稳定性设计:
| 维度 | Android | iOS |
|---|---|---|
| 未安装 App | 抛出 ActivityNotFoundException,可捕获后跳应用商店 | 静默失败,无任何回调,无法直接感知 |
| 多 App 注册同 scheme | 弹出选择器,用户手动选择 | 系统直接选一个,用户无感知 |
| 从浏览器唤起 | 需 BROWSABLE,部分浏览器有拦截 | 需用户手势触发,地址栏直接输入常被拦截 |
| 参数传递 | Intent data 完整保留 | URL 完整保留,但需自行解码 |
最典型的差异是“未安装”场景:Android 可以用 try/catch 捕获异常并引导下载,iOS 则必须借助 UIApplication.canOpenURL 预判,或使用 Universal Link 兜底。这也是为什么生产环境通常采用“Scheme + Universal Link/App Links”双通道策略。
五、常见失效场景
- scheme 大小写不一致:注册为
MyApp,调用写myapp://,部分系统匹配失败。 - 缺少 BROWSABLE:Android 从浏览器点击无反应。
- host 为空:写成
myapp:///detail,路由框架解析不到 host。 - 参数未编码:值里含
&或中文,导致参数截断或乱码。 - 被其他 App 抢占:iOS 上同 scheme 冲突,跳到了别的应用。
- iOS 未做 canOpenURL 白名单:iOS 9 起需在
LSApplicationQueriesSchemes声明才能查询。 - Android 12 exported 缺失:安装失败或运行时无法唤起。
六、基础调试方法
Android 可用 adb 直接模拟跳转,观察是否命中以及参数是否正确:
adb shell am start -a android.intent.action.VIEW \
-d "myapp://product/detail/10086?from=push" \
com.example.myapp
若返回 Error: Activity not started, unable to resolve Intent,说明 scheme/host/path 未匹配;若成功但页面不对,则检查路由解析逻辑。iOS 可在 Safari 地址栏输入 scheme 测试(需用户手势),或用 xcrun simctl openurl booted "myapp://product/detail/10086" 在模拟器验证。更系统的做法是在 App 内埋点,记录“收到 URL → 解析出的 host/path/参数 → 路由结果”,这样线上失效时能快速定位是注册问题、解析问题还是页面问题。
把这张链路图记在脑子里,排查时就能按“注册是否生效 → 系统是否匹配 → 路由是否解析 → 页面是否渲染”逐层缩小范围,而不是盲目改代码。自定义 Scheme 的稳定性上限不高,但它是兜底链路中不可或缺的一环,理解得越透,后续引入 App Links 与 Universal Link 时就越清楚该在哪些环节做互补。