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 上的行为差异,直接影响稳定性设计:

维度AndroidiOS
未安装 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 跳转链路与失效判断 URI 结构 myapp://product/detail/10086?from=push&ts=1710000000 scheme host path query Android 注册 AndroidManifest.xml <data scheme="myapp" host="product" pathPrefix="/detail"/> 需 BROWSABLE + exported iOS 注册 Info.plist → CFBundleURLTypes CFBundleURLSchemes → ["myapp"] 回调:openURL / openURLContexts 无唯一性,可能被抢占 发起跳转请求 系统匹配 scheme 命中 → 唤起 App 解析 host/path/query 未命中 → 失败 Android 抛异常 / iOS 静默 多 App 冲突 Android 选择器 / iOS 抢占

把这张链路图记在脑子里,排查时就能按“注册是否生效 → 系统是否匹配 → 路由是否解析 → 页面是否渲染”逐层缩小范围,而不是盲目改代码。自定义 Scheme 的稳定性上限不高,但它是兜底链路中不可或缺的一环,理解得越透,后续引入 App Links 与 Universal Link 时就越清楚该在哪些环节做互补。