Web 唤起 Android app 的实现及原理

最近在项目中遇到 web 唤起 Android app 的需求,实现很简单,简单记录下实现方式与背后原理。

实现

先不管唤起的原理,用一个简单的例子描述它的实现:

首先需要在 AndroidManifest.xml 中定义 scheme,scheme 不能和 http、https、ftp、sms、mailto 等已使用的相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- web 唤起添加的 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="my.scheme"
android:host="my.host"
/>
</intent-filter>
</activity>

下面是测试网页:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Web 唤起 app</title>
</head>
<body style="text-align: center">
<a href="my.scheme://my.host?name=xxx&title=xxx" style="font-: 27px">点击唤起 Demo app</a>
</body>
</html>

上面的链接中有 name 和 title 两个参数,app 也能接收到,所以在唤起 app 时也能传一些数据

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = getIntent();
if (null != intent && null != intent.getData()) {
Uri uri = intent.getData(); // uri 就相当于 web 页面中的链接
String name = uri.getQueryParameter("name");
String title = uri.getQueryParameter("title");
}
}

原理

my.scheme://my.host?name=xxx&title=xxx其实也是一个链接,为什么点击这个链接浏览器就会启动相应的 app 呢?

其实关键在 WebView 的 WebViewClient 的 shouldOverrideUrlLoading 方法,基本上所有的浏览器都会有类似的实现,下面分析 Android 浏览器的源码。

Android 6.0 的原生浏览器的 shouldOverrideUrlLoading 方法的核心实现在 UrlHandler 这个类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
...
// The "about:" schemes are internal to the browser; don't want these to
// be dispatched to other apps.
if (url.startsWith("about:")) {
return false;
}
...
if (startActivityForUrl(tab, url)) {
return true;
}
if (handleMenuClick(tab, url)) {
return true;
}
return false;
}

从上面第 5 行代码中可以看到 scheme 也不能为 about,这是原生浏览器内部用的,唤起 app 的关键在第 10 行的 startActivityForUrl 方法。

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
boolean startActivityForUrl(Tab tab, String url) {
Intent intent;
// perform generic parsing of the URI to turn it into an Intent.
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException ex) {
Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
return false;
}
// check whether the intent can be resolved. If not, we will see
// whether we can download it from the Market.
ResolveInfo r = null;
try {
r = mActivity.getPackageManager().resolveActivity(intent, 0);
} catch (Exception e) {
return false;
}
...
// sanitize the Intent, ensuring web pages can not bypass browser
// security (only access to BROWSABLE activities).
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
Intent selector = intent.getSelector();
if (selector != null) {
selector.addCategory(Intent.CATEGORY_BROWSABLE);
selector.setComponent(null);
}
...
try {
intent.putExtra(BrowserActivity.EXTRA_DISABLE_URL_OVERRIDE, true);
if (mActivity.startActivityIfNeeded(intent, -1)) { // 唤起 app 的最终代码在这里
// before leaving BrowserActivity, close the empty child tab.
// If a new tab is created through JavaScript open to load this
// url, we would like to close it as we will load this url in a
// different Activity.
mController.closeEmptyTab();
return true;
}
} catch (ActivityNotFoundException ex) {
// ignore the error. If no application can handle the URL,
// eg about:blank, assume the browser can handle it.
}
return false;
}

上面 22 行intent.addCategory(Intent.CATEGORY_BROWSABLE);也可以看出我们之前在 <category android:name="android.intent.category.BROWSABLE" />的原因。

而如果第三方的浏览器在这个地方对 scheme 屏蔽,就可以让 web 唤起 app 实效,微信中网页不能唤起应用就是这个原因。