背景
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 android:name=".SplashActivity" android:launchMode="singleTask" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </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" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lss" android:host="com.lss" /> </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" /> <data android:scheme="myapp" /> </intent-filter> </activity>
</application> </manifest>
|
intent-filter 关键属性说明
| 属性 |
必填 |
作用 |
action.VIEW |
是 |
声明该 Activity 可以查看(处理)数据 |
category.DEFAULT |
是 |
允许隐式 Intent 启动 |
category.BROWSABLE |
是 |
允许从浏览器中通过链接唤起 |
data.scheme |
是 |
自定义协议名,如 lss、myapp |
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);
handleDeepLink(getIntent()); }
@Override protected void onNewIntent(Intent intent) { super.onNewIntent(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) { String fullUrl = uri.toString(); Log.d(TAG, "Deep Link URL: " + fullUrl);
String scheme = uri.getScheme(); String host = uri.getHost(); String path = uri.getPath(); String query = uri.getQuery();
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"); String source = uri.getQueryParameter("source"); String id = uri.getQueryParameter("id");
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'; }
function tryOpenApp() { var device = getDeviceType();
if (device === 'ios') { window.location.href = scheme; } else if (device === 'android') { var startTime = Date.now();
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); } });
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
|
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'; },
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; }
if (device.isQQ) { this.tryLaunch(url); return; }
if (device.isIOS) { this.launchIOS(url); } else if (device.isAndroid) { this.launchAndroid(url); } else { window.location.href = this.config.downloadUrl; } },
launchAndroid: function(url) { var self = this; var startTime = Date.now(); var hasHidden = false;
var iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = url; document.body.appendChild(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) { window.location.href = self.config.downloadUrl; } }, this.config.timeout);
setTimeout(function() { document.body.removeChild(iframe); }, 3000); },
launchIOS: function(url) { 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; } };
|
多场景 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;
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); }
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。
解决方案:
- 使用独特的 Scheme 名称(避免
app、game 等常见词)
- 同时注册 HTTPS App Links 作为 fallback
- 在 H5 页面提供明确的唤起按钮和下载按钮
问题二:应用已启动时的处理
现象:应用已在后台运行,Scheme 唤起后需要正确路由。
解决方案:
1 2 3 4 5 6 7
| @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); handleDeepLink(intent); }
|
问题三:参数中文乱码
现象:Scheme URL 中的中文参数解析出现乱码。
解决方案:
1 2 3 4 5 6 7
| var url = 'lss://com.lss/search?keyword=' + encodeURIComponent('中文关键词');
String keyword = uri.getQueryParameter("keyword");
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 深度链接的实现要点:
- Manifest 配置:在目标 Activity 添加
intent-filter,必须包含 BROWSABLE 和 DEFAULT 类别
- 参数解析:通过
Intent.getData() 获取 Uri 对象,使用 getQueryParameter() 读取参数
- H5 唤起:使用
location.href 或 iframe 方式跳转 Scheme,配合超时检测判断是否安装
- 兼容性处理:微信环境引导用浏览器打开,iOS/Android 分别处理
- 路由设计:建立统一的路由表管理不同路径的跳转逻辑
- 异常处理:处理应用已启动、参数编码、Scheme 拦截等边界情况
合理的 Scheme 设计和完善的唤起方案,可以实现流畅的 H5 到 App 跳转体验,提升用户转化率。