Android URL Scheme 深度链接实战:从 H5 唤起 App 到参数传递

背景

URL Scheme 深度链接是从浏览器、短信、邮件或其他 App 跳转到特定应用页面的常用技术。营销推广里的”点击下载”按钮、社交分享里的”打开 App 查看详情”,都是靠深度链接实现的。这篇文章分享 Android 平台 URL Scheme 的完整实现方案,包括 Scheme 配置、参数解析、H5 唤起策略以及兼容性处理。

深度链接技术概览

什么是 URL Scheme

URL Scheme 是一种特殊的 URL 格式,用于在操作系统中标识和启动特定应用。与标准的 http://https:// 不同,Scheme 使用自定义协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────────────────────────────────────────┐
│ URL Scheme 结构解析 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ weixin://dl/officialaccounts │
│ │ │ │ │
│ │ │ └── 路径和参数 │
│ │ └───── 分隔符 │
│ └──────────── Scheme 名称(协议) │
│ │
│ lss://com.lss/app?x=web&source=share │
│ │ │ │ │ │
│ │ │ │ └── 查询参数(key=value) │
│ │ │ └────── 路径 │
│ │ └────────────── Host │
│ └───────────────────── Scheme │
│ │
└─────────────────────────────────────────────────────────────────────┘

深度链接 vs 应用链接

特性 URL Scheme Android App Links Universal Links (iOS)
协议格式 myapp:// https:// https://
需要验证域名
安全性 较低
兼容性 所有 Android 版本 Android 6.0+ iOS 9+
被拦截风险 较高

URL Scheme 虽然安全性不如 App Links,但由于其兼容性好、实现简单,仍然是最广泛使用的深度链接方案。

Android Scheme 配置

AndroidManifest.xml 配置

在需要接收 Scheme 跳转的 Activity 中添加 intent-filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

<!-- 主 Activity:同时处理 Scheme 唤起 -->
<activity
android:name=".SplashActivity"
android:launchMode="singleTask"
android:exported="true">
<!-- 标准 Launcher 入口 -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- 深度链接处理 Activity -->
<activity
android:name=".DeepLinkActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<!-- 默认类别:必须添加 -->
<category android:name="android.intent.category.DEFAULT" />

<!-- BROWSABLE 类别:允许从浏览器中打开 -->
<category android:name="android.intent.category.BROWSABLE" />

<!-- Scheme 和 Host 配置 -->
<data
android:scheme="lss"
android:host="com.lss" />
</intent-filter>

<!-- 支持多个 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="myapp" />
</intent-filter>
</activity>

</application>
</manifest>

intent-filter 关键属性说明

属性 必填 作用
action.VIEW 声明该 Activity 可以查看(处理)数据
category.DEFAULT 允许隐式 Intent 启动
category.BROWSABLE 允许从浏览器中通过链接唤起
data.scheme 自定义协议名,如 lssmyapp
data.host 域名或标识符,增加匹配精确度
data.pathPrefix 路径前缀过滤
data.pathPattern 路径模式匹配

Activity 中获取 Scheme 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.example.myapp;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

public class DeepLinkActivity extends AppCompatActivity {

private static final String TAG = "DeepLinkActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_deep_link);

// 处理 Scheme 跳转
handleDeepLink(getIntent());
}

@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// singleTask 模式下需要处理新 Intent
handleDeepLink(intent);
}

/**
* 解析并处理深度链接参数
*/
private void handleDeepLink(Intent intent) {
if (intent == null) return;

String action = intent.getAction();
Uri uri = intent.getData();

if (Intent.ACTION_VIEW.equals(action) && uri != null) {
// 获取完整的 Scheme URL
String fullUrl = uri.toString();
Log.d(TAG, "Deep Link URL: " + fullUrl);

// 获取各个组成部分
String scheme = uri.getScheme(); // "lss"
String host = uri.getHost(); // "com.lss"
String path = uri.getPath(); // "/app"
String query = uri.getQuery(); // "x=web&source=share"

Log.d(TAG, "Scheme: " + scheme);
Log.d(TAG, "Host: " + host);
Log.d(TAG, "Path: " + path);
Log.d(TAG, "Query: " + query);

// 获取具体参数值
String x = uri.getQueryParameter("x"); // "web"
String source = uri.getQueryParameter("source"); // "share"
String id = uri.getQueryParameter("id"); // null(如果不存在)

Log.d(TAG, "Param x: " + x);
Log.d(TAG, "Param source: " + source);

// 根据参数路由到不同页面
routeToPage(path, x, source);
}
}

/**
* 根据解析结果路由到对应页面
*/
private void routeToPage(String path, String x, String source) {
if ("/app".equals(path)) {
if ("web".equals(x)) {
// 从网页跳转过来
openWebEntryPage(source);
} else if ("game".equals(x)) {
// 从游戏相关页面跳转
openGamePage(source);
}
} else if ("/shop".equals(path)) {
openShopPage();
}
}

private void openWebEntryPage(String source) {
Log.d(TAG, "Opening web entry page from source: " + source);
// 打开网页入口页面,并携带来源参数
}

private void openGamePage(String source) {
Log.d(TAG, "Opening game page from source: " + source);
}

private void openShopPage() {
Log.d(TAG, "Opening shop page");
}
}

H5 页面唤起 App

基础实现方案

在 H5 页面中通过 location.href 跳转 Scheme:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>打开 App</title>
<style>
.btn {
display: block;
width: 200px;
padding: 15px;
margin: 50px auto;
text-align: center;
background: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
font-size: 18px;
}
</style>
</head>
<body>
<a href="lss://com.lss/app?x=web&source=share" class="btn" id="openApp">打开 App</a>
<a href="https://example.com/download" class="btn" id="download" style="background: #2196F3; display: none;">下载 App</a>

<script>
(function() {
var scheme = 'lss://com.lss/app?x=web&source=share';
var downloadUrl = 'https://example.com/download';
var openBtn = document.getElementById('openApp');
var downloadBtn = document.getElementById('download');
var timeout;

/**
* 判断设备类型
*/
function getDeviceType() {
var ua = navigator.userAgent;
if (/iPhone|iPad|iPod/.test(ua)) return 'ios';
if (/Android/.test(ua)) return 'android';
return 'other';
}

/**
* 唤起 App(带未安装检测)
*/
function tryOpenApp() {
var device = getDeviceType();

if (device === 'ios') {
// iOS 使用 Universal Links 或 URL Scheme
window.location.href = scheme;
} else if (device === 'android') {
// Android 方案:设置超时跳转下载页
var startTime = Date.now();

// 尝试打开 Scheme
window.location.href = scheme;

// 设置超时检测
timeout = setTimeout(function() {
var elapsed = Date.now() - startTime;
// 如果应用未安装,页面不会被切换,超时后跳下载页
if (elapsed < 3000) {
window.location.href = downloadUrl;
}
}, 2500);

// 页面隐藏时清除超时(说明唤起成功)
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
clearTimeout(timeout);
}
});

// pagehide 事件(iOS 和部分 Android)
window.addEventListener('pagehide', function() {
clearTimeout(timeout);
});
} else {
// 其他设备直接显示下载按钮
openBtn.style.display = 'none';
downloadBtn.style.display = 'block';
}
}

// 绑定点击事件
openBtn.addEventListener('click', function(e) {
e.preventDefault();
tryOpenApp();
});
})();
</script>
</body>
</html>

兼容性完善的唤起方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
* 跨平台 App 唤起工具类
*/
var AppLauncher = {

// 配置项
config: {
scheme: 'lss://com.lss/app',
iosUniversalLink: 'https://example.com/app',
androidPackage: 'com.example.myapp',
downloadUrl: 'https://example.com/download',
timeout: 2500
},

/**
* 获取设备信息
*/
getDeviceInfo: function() {
var ua = navigator.userAgent;
return {
isIOS: /iPhone|iPad|iPod/.test(ua),
isAndroid: /Android/.test(ua),
isWechat: /MicroMessenger/.test(ua),
isQQ: /QQ\//.test(ua),
isWeibo: /Weibo/.test(ua),
version: this.parseVersion(ua)
};
},

parseVersion: function(ua) {
var match = ua.match(/Android ([0-9.]+)/) || ua.match(/OS ([0-9_]+)/);
return match ? match[1].replace(/_/g, '.') : '0';
},

/**
* 构建 Scheme URL
*/
buildSchemeUrl: function(params) {
var query = [];
for (var key in params) {
if (params.hasOwnProperty(key)) {
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
}
}
return this.config.scheme + (query.length ? '?' + query.join('&') : '');
},

/**
* 主唤起方法
*/
launch: function(params) {
var device = this.getDeviceInfo();
var url = this.buildSchemeUrl(params || {});

// 微信内置浏览器:引导用系统浏览器打开
if (device.isWechat) {
this.showWechatGuide();
return;
}

// QQ 内置浏览器:部分版本支持 Scheme
if (device.isQQ) {
this.tryLaunch(url);
return;
}

if (device.isIOS) {
this.launchIOS(url);
} else if (device.isAndroid) {
this.launchAndroid(url);
} else {
// PC 或其他设备
window.location.href = this.config.downloadUrl;
}
},

/**
* Android 唤起逻辑
*/
launchAndroid: function(url) {
var self = this;
var startTime = Date.now();
var hasHidden = false;

// 使用 iframe 方式(兼容性更好)
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);

// 同时尝试直接跳转(部分浏览器 iframe 无效)
window.location.href = url;

// 监听页面隐藏事件
var handleVisibility = function() {
if (document.hidden || document.webkitHidden) {
hasHidden = true;
clearTimeout(timer);
}
};

document.addEventListener('visibilitychange', handleVisibility);
document.addEventListener('webkitvisibilitychange', handleVisibility);

// 超时未唤起则跳转下载页
var timer = setTimeout(function() {
document.removeEventListener('visibilitychange', handleVisibility);
document.removeEventListener('webkitvisibilitychange', handleVisibility);

if (!hasHidden) {
// 可能未安装 App
window.location.href = self.config.downloadUrl;
}
}, this.config.timeout);

// 清理 iframe
setTimeout(function() {
document.body.removeChild(iframe);
}, 3000);
},

/**
* iOS 唤起逻辑
*/
launchIOS: function(url) {
// iOS 9+ 推荐使用 Universal Links
// iOS 9 以下使用 Scheme
window.location.href = url;
},

/**
* 微信环境引导
*/
showWechatGuide: function() {
var guide = document.createElement('div');
guide.innerHTML = '<div style="position:fixed;top:0;left:0;width:100%;height:100%;' +
'background:rgba(0,0,0,0.8);z-index:9999;text-align:center;padding-top:50px;"' +
'onclick="this.remove()">' +
'<p style="color:white;font-size:18px;">请点击右上角菜单,选择"在浏览器打开"</p>' +
'</div>';
document.body.appendChild(guide);
},

/**
* 通用唤起尝试
*/
tryLaunch: function(url) {
window.location.href = url;
}
};

// 使用示例
// AppLauncher.launch({ x: 'web', source: 'share', id: '12345' });

多场景 Scheme 设计

典型的 Scheme 路由表

URL Scheme 功能 参数说明
lss://com.lss/app?x=web 从网页唤起主界面 x=web 标识来源
lss://com.lss/game?room=123 直接进入游戏房间 room 房间号
lss://com.lss/shop?item=456 打开商店指定商品 item 商品 ID
lss://com.lss/user?id=789 打开用户主页 id 用户 ID
lss://com.lss/share?type=img&url=xxx 唤起分享面板 type 分享类型

统一路由处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.example.myapp;

import android.net.Uri;
import java.util.HashMap;
import java.util.Map;

/**
* Scheme 路由处理器
*/
public class SchemeRouter {

public interface RouteHandler {
void handle(Map<String, String> params);
}

private Map<String, RouteHandler> routes = new HashMap<>();

public SchemeRouter() {
// 注册路由
register("/app", params -> openMainPage(params));
register("/game", params -> openGamePage(params));
register("/shop", params -> openShopPage(params));
register("/user", params -> openUserPage(params));
}

private void register(String path, RouteHandler handler) {
routes.put(path, handler);
}

/**
* 处理 Scheme URI
*/
public void route(Uri uri) {
if (uri == null) return;

String path = uri.getPath();
RouteHandler handler = routes.get(path);

if (handler != null) {
// 收集所有参数
Map<String, String> params = new HashMap<>();
for (String key : uri.getQueryParameterNames()) {
params.put(key, uri.getQueryParameter(key));
}
handler.handle(params);
}
}

private void openMainPage(Map<String, String> params) {
String source = params.get("source");
String x = params.get("x");
// 打开主页面,处理来源参数
}

private void openGamePage(Map<String, String> params) {
String roomId = params.get("room");
// 进入指定游戏房间
}

private void openShopPage(Map<String, String> params) {
String itemId = params.get("item");
// 打开商店,定位到指定商品
}

private void openUserPage(Map<String, String> params) {
String userId = params.get("id");
// 打开用户个人主页
}
}

常见问题与解决方案

问题一:Scheme 被其他应用劫持

现象:某些浏览器或安全软件会拦截自定义 Scheme。

解决方案

  1. 使用独特的 Scheme 名称(避免 appgame 等常见词)
  2. 同时注册 HTTPS App Links 作为 fallback
  3. 在 H5 页面提供明确的唤起按钮和下载按钮

问题二:应用已启动时的处理

现象:应用已在后台运行,Scheme 唤起后需要正确路由。

解决方案

1
2
3
4
5
6
7
// 使用 singleTask 启动模式,并在 onNewIntent 中处理
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent); // 重要:更新 Intent
handleDeepLink(intent);
}

问题三:参数中文乱码

现象:Scheme URL 中的中文参数解析出现乱码。

解决方案

1
2
3
4
5
6
7
// H5 端:对中文参数进行编码
var url = 'lss://com.lss/search?keyword=' + encodeURIComponent('中文关键词');

// Android 端:
String keyword = uri.getQueryParameter("keyword");
// URL 解码
keyword = URLDecoder.decode(keyword, "UTF-8");

问题四:微信内置浏览器限制

现象:微信内置浏览器直接拦截所有 Scheme 跳转。

解决方案

1
2
3
4
5
// 检测微信环境并引导
if (/MicroMessenger/.test(navigator.userAgent)) {
// 显示遮罩层,引导用户点击右上角用浏览器打开
showOpenInBrowserGuide();
}

测试验证清单

测试项 预期结果 优先级
Chrome 浏览器唤起 正常打开 App
系统自带浏览器唤起 正常打开 App
微信内置浏览器唤起 显示引导层
应用未安装时 fallback 跳转下载页
应用已运行时唤起 路由到正确页面
中文参数传递 无乱码
特殊字符参数 正确解析
长参数串 完整接收

小结

Android URL Scheme 深度链接的实现要点:

  1. Manifest 配置:在目标 Activity 添加 intent-filter,必须包含 BROWSABLEDEFAULT 类别
  2. 参数解析:通过 Intent.getData() 获取 Uri 对象,使用 getQueryParameter() 读取参数
  3. H5 唤起:使用 location.href 或 iframe 方式跳转 Scheme,配合超时检测判断是否安装
  4. 兼容性处理:微信环境引导用浏览器打开,iOS/Android 分别处理
  5. 路由设计:建立统一的路由表管理不同路径的跳转逻辑
  6. 异常处理:处理应用已启动、参数编码、Scheme 拦截等边界情况

合理的 Scheme 设计和完善的唤起方案,可以实现流畅的 H5 到 App 跳转体验,提升用户转化率。