Android Deep Link 与 App Link 深度解析:原理、实践与演进
深入探讨移动端引流的核心技术,从通用 Scheme 到官方的 App Link,并提供详尽的实践指南。
1. 引言:为何需要“唤端”技术?
在移动互联网生态中,孤岛式的应用无法满足用户无缝连接的需求。我们经常遇到这样的场景:
- 点击朋友圈的广告,直接跳转到电商APP的商品页
- 浏览新闻时看到一个有趣的短视频,点击后唤起了抖音并定位到该视频
- 收到银行转账短信,点击链接直接打开手机银行APP查看详情
- 朋友分享的购物链接,点击后直接在淘宝APP打开对应商品
这种从短信、Web(H5)页面、社交媒体等场景无缝跳转到原生应用(Native APP)的技术,被称为 "唤端" 或深度链接。
2. Android Deep Link (URL Scheme):通用但存瑕的元老
2.1 核心原理
想象一下,每个APP都像一座独立的城堡,URL Scheme就是为每个城堡设计的"秘密密道"。这个密道有一个特殊的暗号格式,比如知乎的密道暗号是 zhihu://。
工作机制:
- 应用向操作系统"登记"自己的密道暗号
- 当系统遇到
zhihu://questions/123456这样的暗号时 - 系统会找到所有能听懂这个暗号的APP
- 弹出一个选择器:"你要用哪个城堡的密道?"
2.2 短信场景中的 URL Scheme
短信是深度链接最常见的应用场景之一:
<!-- 银行发送的转账通知短信 -->
尊敬的客户,您尾号8888的储蓄卡收入5000.00元。
<a href="icbc://transfer/result?order=20240119001">点击查看详情</a>
立即下载:<a href="https://appstore/icbc">应用商店</a>
短信中的实现方式:
- 在短信中直接嵌入
scheme://path格式的链接 - 用户点击后,如果已安装APP → 直接打开
- 如果未安装APP → 显示无法打开的提示
2.3 实现方式
H5页面触发跳转:
// 方法1:直接跳转
function openAppByScheme() {
window.location.href = 'zhihu://question/123456';
}
// 方法2:通过iframe跳转(更稳定)
function openAppByIframe() {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'zhihu://question/123456';
document.body.appendChild(iframe);
// 2秒后移除iframe
setTimeout(() => {
document.body.removeChild(iframe);
}, 2000);
}
// 方法3:处理短信链接
function handleSmsLink() {
// 短信链接通常是直接可点击的
// 用户点击 <a href="zhihu://question/123">短信链接</a> 即可
}
Android原生端配置(AndroidManifest.xml):
<!-- 接收Scheme链接的Activity -->
<activity android:name=".DeepLinkActivity"
android:exported="true">
<!-- 声明Scheme处理能力 -->
<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:host="deeplink"
android:scheme="zhihu" />
<!-- 可选:更精确的路径匹配 -->
<data android:pathPrefix="/question/" />
</intent-filter>
<!-- 可以声明多个intent-filter处理不同格式 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- 另一个Scheme -->
<data android:scheme="zh" />
</intent-filter>
</activity>
2.4 在Activity中处理传入的链接
class DeepLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_deeplink)
// 处理传入的Intent
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// 防止重复创建Activity
setIntent(intent)
handleDeepLink(intent)
}
private fun handleDeepLink(intent: Intent?) {
intent?.data?.let { uri ->
when {
// 处理知乎问答链接
uri.toString().startsWith("zhihu://question/") -> {
val questionId = uri.lastPathSegment
openQuestionDetail(questionId)
}
// 处理用户主页链接
uri.toString().startsWith("zhihu://user/") -> {
val userId = uri.lastPathSegment
openUserProfile(userId)
}
// 处理短信中的银行链接
uri.toString().startsWith("icbc://") -> {
handleBankTransaction(uri)
}
}
}
}
private fun openQuestionDetail(questionId: String?) {
// 跳转到问题详情页
val intent = Intent(this, QuestionDetailActivity::class.java)
intent.putExtra("QUESTION_ID", questionId)
startActivity(intent)
finish()
}
}
2.5 优缺点分析
优点:
- ✅ 兼容性极佳:从Android 1.0到最新版本都支持
- ✅ 实现简单:只需在AndroidManifest中声明即可
- ✅ 短信友好:短信中直接可点击,无需特殊处理
缺点:
- ❌ 弹出选择器:每次都要问用户"用哪个应用打开?"
- ❌ 无法判断是否成功:只能靠猜(定时器方案不准确)
- ❌ 容易被劫持:恶意应用可注册相同Scheme
- ❌ 微信/QQ中被屏蔽:这些应用会拦截非白名单Scheme
- ❌ 无降级方案:如果APP没安装,链接就打不开
3. Android App Link:官方出品的优雅方案
3.1 核心原理:基于域名的所有权验证
把App Link想象成官方的快递系统:
-
使用标准地址:不再用密道暗号,而是用大家都能看懂的"街道地址"(HTTPS网址)
- 原来是:
zhihu://question/123 - 现在是:
https://www.zhihu.com/question/123
- 原来是:
-
域名所有权验证:你要证明"知乎大街888号"确实是你家的
- 在
www.zhihu.com这个"房产证"上,加上你的"指纹信息" - Android系统会派人去查看"房产证"
- 如果指纹匹配,就直接把快递(用户)送到你家,不再问"送哪家?"
- 在
3.2 详细配置流程(保姆级教程)
第1步:在AndroidManifest中声明
<activity android:name=".AppLinkActivity"
android:exported="true">
<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" />
<!-- 声明你的HTTPS链接格式 -->
<data
android:scheme="https"
android:host="www.zhihu.com"
android:pathPrefix="/question/" />
<!-- 可以匹配更多路径 -->
<data android:pathPrefix="/answer/" />
<data android:pathPrefix="/user/" />
</intent-filter>
<!-- 可以支持多个域名 -->
<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="zhihu.com"
android:pathPrefix="/m/" />
</intent-filter>
</activity>
第2步:获取应用的"指纹"
每个Android应用都有唯一的"指纹"(SHA-256证书指纹),获取方法:
方法A:从签名文件中获取(推荐)
# 使用keytool查看签名信息
keytool -list -v -keystore your-release-key.keystore
# 会看到类似这样的输出:
# SHA256: 33:5A:45:9B:0C:... (很长一串)
方法B:从已安装的APP获取(调试用)
# 1. 先获取签名证书的MD5
adb shell pm dump com.zhihu.android | grep "Signatures"
# 2. 用keytool转换
keytool -printcert -file platform.x509.pem
方法C:在代码中获取
fun getAppSignature(context: Context): String {
val packageInfo = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
val signatures = packageInfo.signatures
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(signatures[0].toByteArray())
return digest.joinToString(":") { "%02X".format(it) }
}
第3步:创建assetlinks.json文件
创建一个JSON文件,内容格式如下:
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.zhihu.android", // 你的应用包名
"sha256_cert_fingerprints": [
"33:5A:45:9B:0C:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX"
// ↑ 这里填上一步获取的指纹
]
}
},
// 如果你有多个签名(比如调试版和发布版不同签名)
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.zhihu.android",
"sha256_cert_fingerprints": [
"AA:BB:CC:...", // 调试版签名指纹
"DD:EE:FF:..." // 发布版签名指纹
]
}
}
]
第4步:部署assetlinks.json到服务器
部署位置必须精确:
https://你的域名/.well-known/assetlinks.json
示例:
- 如果你的域名是
www.zhihu.com - 文件必须放在:
https://www.zhihu.com/.well-known/assetlinks.json
注意事项:
- 必须通过HTTPS访问
- Content-Type必须是
application/json - 必须返回200状态码
- 不能有重定向
- 文件大小不超过100KB
验证是否部署成功:
# 在浏览器中直接访问
curl -I https://www.zhihu.com/.well-known/assetlinks.json
# 应该看到:
# HTTP/2 200
# content-type: application/json
第5步:测试验证
方法A:使用ADB命令测试
# 测试普通深度链接
adb shell am start -W -a android.intent.action.VIEW -d "https://www.zhihu.com/question/123456"
# 验证域名关联状态
adb shell pm get-app-links com.zhihu.android
# 强制验证(第一次安装时系统会自动验证)
adb shell pm verify-app-links --re-verify com.zhihu.android
方法B:在Android Studio中测试
- 运行APP到手机
- 点击Android Studio的"Logcat"
- 过滤"DigitalAssetLinks"
- 查看验证日志
方法C:通过网页测试
<!-- 创建一个测试HTML文件 -->
<!DOCTYPE html>
<html>
<body>
<a href="https://www.zhihu.com/question/123456">
点击这里应该直接打开知乎APP
</a>
</body>
</html>
3.3 短信中的App Link使用
在短信中使用App Link时,用户会有无缝的体验:
<!-- 银行发送的转账成功短信(使用App Link) -->
【XX银行】您尾号8888的账户收入5,000.00元。
<a href="https://bank.example.com/transfer/20240119001">点击查看详情</a>
<!-- 电商订单短信 -->
【XX商城】您的订单已发货,快递单号:YT123456789
<a href="https://m.taobao.com/order/123456">查看物流</a>
<!-- 社交分享短信 -->
张三给您分享了一个视频:
<a href="https://www.douyin.com/video/123456">点击观看</a>
短信中App Link的优势:
- 无缝体验:已安装APP → 直接打开;未安装 → 打开网页版
- 安全性高:HTTPS链接,防止劫持
- 可追踪:通过网页可统计点击率、转化率
- 兼容性好:所有手机短信都支持HTTPS链接
3.4 处理App Link的Activity代码
class AppLinkActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 处理深度链接
handleAppLink(intent)
}
private fun handleAppLink(intent: Intent) {
val uri = intent.data ?: return
when {
// 处理问题详情页
uri.path?.startsWith("/question/") == true -> {
val questionId = uri.lastPathSegment
openQuestionPage(questionId)
}
// 处理回答详情页
uri.path?.startsWith("/answer/") == true -> {
val answerId = uri.lastPathSegment
openAnswerPage(answerId)
}
// 处理用户主页
uri.path?.startsWith("/user/") == true -> {
val userId = uri.lastPathSegment
openUserProfile(userId)
}
// 处理查询参数
else -> {
val id = uri.getQueryParameter("id")
val type = uri.getQueryParameter("type")
handleGenericLink(id, type)
}
}
}
// 处理用户从短信或其他地方打开的情况
private fun handleSmsAppLink(uri: Uri) {
// 记录来源
val referrer = intent.getStringExtra(Intent.EXTRA_REFERRER)
val fromSms = referrer?.contains("sms") == true
if (fromSms) {
// 特殊处理短信来源的流量
trackEvent("sms_deeplink_open")
}
}
}
3.5 处理边缘情况
// 1. 处理多任务栈
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 如果是从历史记录中恢复,清除任务栈重新开始
if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) {
finish()
return
}
}
// 2. 处理重复打开
private var isProcessing = false
private fun handleDeepLinkSafely(intent: Intent) {
if (isProcessing) {
return
}
isProcessing = true
try {
// 处理链接
processIntent(intent)
} finally {
isProcessing = false
}
}
// 3. 延迟初始化处理
override fun onResume() {
super.onResume()
if (shouldHandleIntent) {
// 等UI加载完成后再处理
view.post {
handleDeepLink(intent)
shouldHandleIntent = false
}
}
}
4. 最佳实践:完整唤端方案
4.1 智能唤端策略
// 完整的H5唤端方案
class DeepLinkManager {
constructor(options = {}) {
this.options = {
appLink: 'https://www.zhihu.com/question/123456',
schemeLink: 'zhihu://question/123456',
appStoreLink: 'https://apps.apple.com/app/id123456', // iOS
playStoreLink: 'https://play.google.com/store/apps/details?id=com.zhihu.android',
timeout: 1500,
...options
};
this.timer = null;
this.hasApp = false;
}
// 主唤端方法
open() {
// 检查是否在特定浏览器中
if (this.isInWechat()) {
this.handleWechat();
return;
}
if (this.isInQQ()) {
this.handleQQ();
return;
}
// 正常唤端流程
this.startWatching();
this.tryOpenAppLink();
}
// 尝试App Link
tryOpenAppLink() {
// 先尝试App Link
this.openLink(this.options.appLink);
// 设置超时检查
this.timer = setTimeout(() => {
if (!this.hasApp) {
this.tryScheme();
}
}, this.options.timeout);
}
// 降级到Scheme
tryScheme() {
this.openLink(this.options.schemeLink);
// Scheme也失败,跳转应用商店
this.timer = setTimeout(() => {
if (!this.hasApp) {
this.goToAppStore();
}
}, 1000);
}
// 监听页面隐藏(表示唤端成功)
startWatching() {
this.hasApp = false;
const onVisibilityChange = () => {
if (document.hidden) {
this.hasApp = true;
if (this.timer) clearTimeout(this.timer);
}
};
const onBlur = () => {
this.hasApp = true;
if (this.timer) clearTimeout(this.timer);
};
document.addEventListener('visibilitychange', onVisibilityChange);
window.addEventListener('pagehide', onVisibilityChange);
window.addEventListener('blur', onBlur);
// 清理监听
this.cleanup = () => {
document.removeEventListener('visibilitychange', onVisibilityChange);
window.removeEventListener('pagehide', onVisibilityChange);
window.removeEventListener('blur', onBlur);
};
}
// 处理微信环境
handleWechat() {
// 微信中无法直接打开App
// 引导用户在其他浏览器中打开
this.showWechatGuide();
}
// 工具方法
isInWechat() {
return /MicroMessenger/i.test(navigator.userAgent);
}
isInQQ() {
return /QQ//i.test(navigator.userAgent);
}
openLink(url) {
// 使用iframe方式,避免页面跳转
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(() => {
document.body.removeChild(iframe);
}, 1000);
}
goToAppStore() {
if (this.isIOS()) {
window.location.href = this.options.appStoreLink;
} else {
window.location.href = this.options.playStoreLink;
}
}
isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
destroy() {
if (this.timer) clearTimeout(this.timer);
if (this.cleanup) this.cleanup();
}
}
// 使用示例
const deeplink = new DeepLinkManager({
appLink: 'https://www.zhihu.com/question/123456',
schemeLink: 'zhihu://question/123456',
timeout: 2000
});
// 点击按钮时触发
document.getElementById('open-app-btn').addEventListener('click', () => {
deeplink.open();
});
4.2 Android端完整配置
<!-- AndroidManifest.xml 完整示例 -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"> <!-- 建议使用singleTask -->
<!-- Deep Link (URL Scheme) -->
<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="zhihu"
android:host="deeplink" />
</intent-filter>
<!-- App Link 1: 主域名 -->
<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="www.zhihu.com"
android:pathPrefix="/question/" />
</intent-filter>
<!-- App Link 2: 短链接域名 -->
<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="zhihu.cn"
android:pathPattern="/q/.*" />
</intent-filter>
<!-- App Link 3: 分享链接域名 -->
<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="share.zhihu.com" />
</intent-filter>
</activity>
5. 常见问题与解决方案
问题1:App Link验证失败
可能原因:
- assetlinks.json文件访问不到
- 证书指纹不匹配
- 包名写错了
- 服务器返回了错误的Content-Type
解决方案:
# 1. 检查文件是否可访问
curl -I https://yourdomain.com/.well-known/assetlinks.json
# 2. 查看验证状态
adb shell pm get-app-links your.package.name
# 3. 强制重新验证
adb shell pm verify-app-links --re-verify your.package.name
# 4. 清除验证结果
adb shell pm set-app-links --package your.package.name 0 all
问题2:用户选择了"记住选择"
场景:用户第一次点击时,选择了"用浏览器打开"并勾选了"总是"。
解决方案:
// 引导用户去设置中修改
fun showOpenByDefaultDialog(context: Context) {
AlertDialog.Builder(context)
.setTitle("打开方式设置")
.setMessage("检测到您之前选择了用浏览器打开,是否要修改设置?")
.setPositiveButton("去设置") { _, _ ->
// 跳转到应用的默认打开方式设置
val intent = Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
intent.data = Uri.parse("package:${context.packageName}")
context.startActivity(intent)
}
.setNegativeButton("取消", null)
.show()
}
问题3:短信链接在国产手机上被拦截
现象:在小米、华为等手机上,点击短信中的链接会先打开浏览器。
解决方案:
// 检查并引导用户
fun checkAndHandle(context: Context, uri: Uri) {
val isFromSms = intent?.getStringExtra("from") == "sms"
if (isFromSms && !isAppDefaultHandler(context)) {
// 显示引导页
showGuidePage(context)
} else {
// 正常处理
handleDeepLink(uri)
}
}
// 检查APP是否是默认处理器
fun isAppDefaultHandler(context: Context): Boolean {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://yourdomain.com"))
val resolveInfo = context.packageManager.resolveActivity(
intent,
PackageManager.MATCH_DEFAULT_ONLY
)
return resolveInfo?.activityInfo?.packageName == context.packageName
}
6. 实战:电商App完整示例
6.1 短信营销链接
<!-- 双11促销短信 -->
【XX商城】双11提前购!爆款5折起,限时抢购!
<a href="https://m.xxshop.com/promo/11?source=sms">立即抢购</a>
(点击直接打开APP,未安装可先查看详情)
6.2 Android配置
<!-- 商品详情页 -->
<activity
android:name=".ProductDetailActivity"
android:exported="true"
android:launchMode="singleTask">
<!-- 支持的商品链接格式 -->
<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="m.xxshop.com"
android:pathPrefix="/product/" />
</intent-filter>
<!-- 促销活动链接 -->
<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="m.xxshop.com"
android:pathPrefix="/promo/" />
</intent-filter>
<!-- 订单链接 -->
<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="m.xxshop.com"
android:pathPrefix="/order/" />
</intent-filter>
</activity>
6.3 服务器assetlinks.json
[ { "relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.xxshop.app",
"sha256_cert_fingerprints": [
"发布版指纹1",
"发布版指纹2"
]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.xxshop.app.debug",
"sha256_cert_fingerprints": [
"调试版指纹"
]
}
}
]
7. 总结与建议
核心要点回顾
- URL Scheme:兼容性好但体验差,适合作为兜底方案
- App Link:体验好但需要配置验证,是未来趋势
- 短信场景:优先使用App Link,提供更好的降级体验
推荐方案
// 根据场景选择策略
function getDeepLinkStrategy(scenario) {
const strategies = {
'sms': 'applink', // 短信用App Link
'email': 'applink', // 邮件用App Link
'social_media': 'hybrid', // 社交媒体用混合
'web_page': 'hybrid', // 网页用混合
'qr_code': 'hybrid' // 二维码用混合
};
return strategies[scenario] || 'hybrid';
}
实施建议
- 新项目:直接上App Link为主,Scheme兜底
- 老项目:逐步迁移到App Link
- 短信营销:一定要用App Link
- 用户引导:做好未安装APP的引导页
- 数据监控:统计各渠道的唤端成功率
未来趋势
- Instant Apps:无需安装即可使用部分功能
- App Clips:轻量级应用体验
- 跨平台方案:Firebase Dynamic Links、Branch.io等
深度链接技术是移动端流量获取和用户体验的关键。正确实施可以显著提升转化率和用户留存。随着Android生态的成熟,App Link已成为必备技能,建议所有Android开发者都要掌握。