如何为 Navigation Graph 设置 deep link 以支持外部 App 启动?

解读

在国内面试中,这道题考察的不仅是“能不能跳进来”,而是“跳进来之后能不能活”。面试官想确认:

  1. 你是否理解 Navigation 组件的 deep link 声明方式(XML 与代码双通道);
  2. 能否把 URI 结构与参数映射到 NavDestination,并保证参数类型安全;
  3. 是否知道国内 ROM 对“后台唤起”的限制,以及如何在不同 Android 版本上把 deep link 稳态拉起;
  4. 能否在单一 Activity 多 Fragment 场景下,把 deep link 直接落到目标页,同时恢复回退栈,避免用户按返回键直接退出 App;
  5. 是否具备防劫持、签名校验、Intent 过滤优先级排序等安全意识。

一句话:让外部 App 能拉得动、拉得准、拉得稳、拉得安全。

知识点

  1. NavDeepLinkBuilder / <deepLink> 标签两种声明方式,支持 scheme、host、path、pathPattern、pathPrefix。
  2. 自动生成的 AndroidManifest 会追加 <nav-graph> 标签,合并后生成隐式 Intent-filter,优先级与显式声明一致。
  3. 参数传递支持 {argName} 占位符,支持 autoVerify=true 开启 App Links,国内需配套上传 assetlinks.json 到服务器并保证 HTTPS 可访问。
  4. 国内 ROM(小米、华为、OPPO、vivo)对后台拉起加白名单,需在各自厂商后台申请“自启动”与“关联启动”权限,否则 deep link 会被系统拦截。
  5. 单一 Activity 架构下,NavController 的 handleDeepLink() 会在 onCreate() 与 onNewIntent() 阶段自动完成目标 Fragment 的重建与回退栈恢复;若采用多 Activity 架构,需手动解析并调用 navigate()。
  6. 安全加固:对外部 Intent 做 getDataString() 非空与 Uri 白名单校验;对敏感业务加签名校验,防止第三方伪造 Intent 调起。
  7. 国内渠道包与 Google Play 包并存时,scheme 需隔离,避免不同签名包抢占同一 URI 导致“打开方式”弹窗。
  8. 测试工具:adb shell am start -W -a android.intent.action.VIEW -d <URI> <包名>;配合 logcat | grep Navigation 可快速验证是否命中目标 Destination。

答案

步骤一:在 nav_graph.xml 的目标 Fragment 节点内声明 deep link <fragment android:id="@+id/productDetailFragment" android:name="com.xxx.ProductDetailFragment"> <argument android:name="skuId" app:argType="string" /> <deepLink android:autoVerify="true" app:uri="https://mall.xxx.com/product/{skuId}" /> </fragment>

步骤二:确保应用具备可验证的 App Links

  1. 在服务器根目录放置 https://mall.xxx.com/.well-known/assetlinks.json,内容包含应用签名 SHA256 与包名;
  2. 国内 CDN 必须支持 HTTPS 且证书链完整,否则 autoVerify 会失败,系统仍把链接当普通 deep link 处理,弹出选择框。

步骤三:在唯一 Activity 的 onCreate() 与 onNewIntent() 中统一交给 NavController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navController = findNavController(R.id.nav_host) navController.handleDeepLink(intent) }

override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) intent?.let { findNavController(R.id.nav_host).handleDeepLink(it) } }

步骤四:国内 ROM 适配

  1. 在小米、华为、OPPO、vivo 后台申请“关联启动”白名单,否则应用被切后台后无法被 deep link 拉起;
  2. 若业务强依赖推送,可把 deep link 与推送平台(如友盟、极光)结合,使用厂商通道自带“跳转落地页”能力,降低被拦截概率。

步骤五:安全校验

  1. 在目标 Fragment 的 onCreate() 中对 arguments?.getString("skuId") 做空值与正则校验;
  2. 对敏感业务(如支付页)追加签名校验: val callingPkg = activity?.callingPackage val sigs = context?.packageManager?.getPackageInfo(callingPkg!!, PackageManager.GET_SIGNATURES)?.signatures if (!whiteListSigs.contains(sigs?.get(0)?.toCharsString())) { findNavController().popBackStack() return }

完成以上五步,即可实现“外部 App 通过 URI 精准启动到任意 NavDestination,并在国内主流 ROM 上稳定运行”。

拓展思考

  1. 如果业务需要一次性携带 20+ 参数,URI 长度超限,如何设计?
    可改用 App Links + 短链服务:外部只传短码 https://s.xxx.com/8aY9k,目标页再拉接口补全参数,避免 4 kB 限制与浏览器兼容问题。

  2. 当应用已采用 Jetpack Navigation 多模块(feature module)动态交付,deep link 声明在 dynamic-feature 的 graph 中,如何在未安装模块时被即时触发?
    需在 base module 的 manifest 中预埋相同的 <intent-filter>,并在代理 Activity 内通过 SplitInstallManager 动态安装模块,安装完成后再手动 navigate() 到目标 Destination,否则系统会提示“无法找到 Activity”。

  3. 折叠屏与多窗口场景下,deep link 拉起后屏幕旋转导致 NavController 重建,如何防止重复导航?
    在 Activity 的 savedInstanceState 中记录 hasHandledDeepLink 标志位,仅当 savedInstanceState == null 时才调用 handleDeepLink(),避免重复入栈;同时结合 Intent.FLAG_ACTIVITY_CLEAR_TASK 与 FLAG_ACTIVITY_NEW_TASK,保证外部拉起时任务栈干净。

  4. 国内小程序生态(微信、支付宝)禁止直接跳转到外部 App,如何把 deep link 与小程序路径桥接?
    可在小程序内嵌 web-view,加载中间页 H5,H5 再通过“开放标签”或“JS-SDK”拉端,此时需把 scheme 换成 universalLink 形式,并在微信后台配置“业务域名”与“可信域名”,否则会被拦截为“非法跳转”。

  5. 未来 Android 14 对隐式 Intent 的限制进一步收紧,deep link 是否会被废弃?
    官方倾向推动 App Links 完全替代自定义 scheme,但国内生态碎片化严重,短期内 scheme 仍是最低成本兼容方案。建议新立项直接上 https + autoVerify,老项目保留 scheme 作为降级通道,通过 URI 白名单与签名校验双保险,平滑过渡到下一代“隐私沙盒”时代。