如何限制iOS Universal Links跳轉(zhuǎn)
有時由于產(chǎn)品需求,我們需要使用一個WKWebView來呈現(xiàn)第三方平臺的內(nèi)容。當(dāng)?shù)谌狡脚_擁有自己的App時,通常都會在網(wǎng)頁端引導(dǎo)用戶跳轉(zhuǎn)到第三方App。iOS Universal Links是實現(xiàn)這種跳轉(zhuǎn)的一種常見方式,已經(jīng)有很多文章討論如何實現(xiàn)Universal Links,本文則是反其道而行之,討論如何禁用Universal Links觸發(fā)的App跳轉(zhuǎn)。
Universal Links原理
要想禁用Universal Links,首先就需要了解它的原理。本文只是簡要介紹一下它的基本原理,詳情可以參考下列文檔:
Raywenderlich:Universal Links – Make the Connection
官方對Universal Links的描述如下:
When you support universal links, iOS users can tap a link to your website and get seamlessly redirected to your installed app without going through Safari. If your app isn’t installed, tapping a link to your website opens your website in Safari.
Universal links give you several key benefits that you don’t get when you use custom URL schemes. Specifically, universal links are:
- Unique. Unlike custom URL schemes, universal links can’t be claimed by other apps, because they use standard HTTP or HTTPS links to your website.
- Secure. When users install your app, iOS checks a file that you’ve uploaded to your web server to make sure that your website allows your app to open URLs on its behalf. Only you can create and upload this file, so the association of your website with your app is secure.
- Flexible. Universal links work even when your app is not installed. When your app isn’t installed, tapping a link to your website opens the content in Safari, as users expect.
- Simple. One URL works for both your website and your app.
- Private. Other apps can communicate with your app without needing to know whether your app is installed.
Universal Links的實現(xiàn)
要實現(xiàn)Universal Links,需要對網(wǎng)頁的服務(wù)器端和iOS App端同時做一些配置:
對于服務(wù)器:
- 創(chuàng)建一個名為apple-app-site-association的JSON文件,用于描述App可以將哪些URL當(dāng)做Universal Link來處理
- 將這個apple-app-site-association文件上傳到HTTPS web服務(wù)器。該文件可以被放置在服務(wù)器的根目錄,或者.well-know子目錄。例如,Bilibili的配置文件就是放置在根目錄,http://www.bilibili.com/apple-app-site-association。有興趣的可以打開這個鏈接,其實就是一個JSON文件,具體含義可以參考官方文檔Support Universal Links。
對于App:
- 創(chuàng)建一個名為com.apple.developer.associated-domains的entitlement,包含App支持的Universal Link的domain。注意,不同的子域名都會被當(dāng)做不同的domain,比如www.bilibili.com和m.bilibili.com就是兩個domain。
- 在
AppDelegate.application:continueUserActivity:restorationHandler:中響應(yīng)WebView傳入的Universal Links。
com.apple.developer.associated-domains v.s. apple-app-site-association
- 前者配置在App端,后者配置在網(wǎng)頁端。
- 前者針對domain,后者針對domain下的URL,指明某個domain下哪些URL可以被當(dāng)做Universal Links。
Universal Links的工作流程
上面一節(jié)介紹的是如何配置服務(wù)器端和App端以便支持Universal Links,那么當(dāng)這一切都部署好之后,用戶在WebView/Safari上點擊了一個Universal Link后,本地App是如何被打開的?
- App A安裝成功后,iOS會根據(jù)其com.apple.developer.associated-domains中列出的domain,下載對應(yīng)的apple-app-site-association文件。
- 用戶在App B的WebView中點擊一個URL后,該WebView的
webView(:, decidePolicyFor:, decisionHandler:)被觸發(fā)(如果存在),決定是否允許訪問該網(wǎng)址。 - 如果上一步允許訪問,則系統(tǒng)會結(jié)合com.apple.developer.associated-domains和apple-app-site-association判斷該URL是否為Universal links,若不是,則直接在網(wǎng)頁中打開。若是,則做出下列判斷:
- 若手動關(guān)閉了Universal Links跳轉(zhuǎn)(見下一節(jié)說明),直接在網(wǎng)頁中打開新網(wǎng)址。
- 若不是用戶手動點擊的操作,直接在網(wǎng)頁中打開新網(wǎng)址。
- 若新舊網(wǎng)址屬于同一域名,直接在網(wǎng)頁中打開新網(wǎng)址。例如,www.bilibili.com和m.bilibili.com屬于不同域名,在它們之間切換會觸發(fā)App跳轉(zhuǎn),但在m.bilibili.com的不同網(wǎng)址間切換并不會觸發(fā)。
- 否則,打開App A,并調(diào)用它的
AppDelegate.application:continueUserActivity:restorationHandler:。
Universal Links的坑
突破微信跳轉(zhuǎn)限制-Universal Links那些坑 總結(jié)了Universal Links失效的一些情況:
- Universal Links will not work if you paste the link into the browser URL field.
- Universal Links work with a user driven
<a href="...">element click across domains. Example: if there is a Universal Link on google.com pointing to bnc.lt, it will open the app.- Universal Links will not work with a user driven
<a href="...">element click on the same domain. Example: if there is a Universal Link on google.com pointing to a different Universal Link on google.com, it will not open the app.- Universal Links cannot be triggered via Javascript (in window.onload or via a .click() call on an
<a>element), unless it is part of a user action.
除了上述情況外,若用戶通過Universal Links跳轉(zhuǎn)到App后,又點擊了屏幕右上角的URL(如下圖),iOS會在網(wǎng)頁端再次打開這個鏈接。此外,系統(tǒng)還會認為用戶偏向于在網(wǎng)頁端查看URL,因此用戶再次點擊超鏈時,系統(tǒng)不會再跳轉(zhuǎn)到App,相當(dāng)于用戶手動關(guān)閉了Universal Links。如果想再次啟動Universal Links,用戶需要在網(wǎng)頁端手動點擊屏幕右上方的“打開”
禁用Universal Links

禁用Universal Links

限制Universal Links
上面介紹了Universal Links的基本原理,根據(jù)這些原理,我們有兩個禁用Universal Links的思路。
思路1:webView(: decidePolicyFor: decisionHandler:)
上面講到,當(dāng)Universal Links被點擊時,我們App的webView(: decidePolicyFor: decisionHandler:)會首先被觸發(fā),用來決定是否允許對該URL的訪問,如果我們事先知道哪些URL屬于Universal Links,就可以在這個地方將它們禁掉。
使用這個方法的好處在于簡單,如果我們的App只會訪問有限數(shù)量的第三方網(wǎng)站,那么只需要找到每個網(wǎng)站Universal Links的格式即可。但若我們App可能打開任意的網(wǎng)站,那就不能用這個辦法了。
那么如何找到Univeral Links的網(wǎng)址格式?簡單的方法就是在webView(: decidePolicyFor: decisionHandler:)中設(shè)置斷點,然后點擊某個會導(dǎo)致App跳轉(zhuǎn)的鏈接即可。正常的網(wǎng)站通常會用一個非常有別于它們主域名的網(wǎng)址來作為會導(dǎo)致App跳轉(zhuǎn)的網(wǎng)址,比如搜狐的跳轉(zhuǎn)網(wǎng)址就是形如http://s1.h5.itc.cn/app/phone.html?xxxxx,因此這個方法基本夠用。
如果想找到最完備的Universal Links列表,可以采用下面的方式,此處以搜狐視頻為例:
- 在Mac上使用iTunes下載搜狐視頻。iTunes通常把App保存在/Users/XXX/Music/iTunes/iTunes Media/Mobile Applications目錄下,(把XXX替換為你的用戶名)
- 使用解壓工具解壓搜狐視頻的ipa包,生成一個文件夾,點擊其中的Payload >> SOHUVideo >> 右擊 >> 選擇“Show Package Contents” >> archived-expanded-entitlements.xcent >> 使用文本工具打開,就可以看到下列內(nèi)容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:m.tv.sohu.com</string>
<string>applinks:wx.m.tv.sohu.com</string>
<string>applinks:t.mtv.sohu.com</string>
<string>applinks:s1.h5.itc.cn</string>
<string>applinks:tv.sohu.com</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.sohu.SohuVideo</string>
</array>
</dict>
</plist>
可以看到com.apple.developer.associated-domains包含了以下domain:
- m.tv.sohu.com
- wx.m.tv.sohu.com
- t.mtv.sohu.com
- s1.h5.itc.cn
- tv.sohu.com
除了s1.h5.itc.cn外,其他domain都是主站的域名,我們顯然也不可能禁用主站的域名,否則會導(dǎo)致網(wǎng)頁無法訪問的情況。有人可能有點不放心,覺得好多域名沒有被禁用,是否可能出現(xiàn)App跳轉(zhuǎn)的情況?萬一出現(xiàn)從m.tv.sohu.com跳wx.m.tv.sohu.com的情況怎么辦?一般來說,對于一個正常的網(wǎng)站,比如搜狐視頻,在上線Universal Links時,肯定會避免跨域名跳轉(zhuǎn)的情況,否則若用戶安裝了搜狐視頻的App,然后使用Safari在瀏覽搜狐的網(wǎng)頁,點著點著,突然毫無征兆的跳到了App,這絕對是很差的一個用戶體驗。
當(dāng)然,有些網(wǎng)站就是喜歡不走平常路,所有的Universal links的domain都是主域名,而且還有跨域名跳轉(zhuǎn)的情況,這時候你可能只能使用下面的思路2了。
對于上面的domain,我們可以通過拼接的方式找到apple-app-site-association的下載地址,有興趣的可以試試。
https://m.tv.sohu.com/apple-app-site-association
https://wx.m.tv.sohu.com/apple-app-site-association
https://t.mtv.sohu.com/apple-app-site-association:無法訪問
https://s1.h5.itc.cn/apple-app-site-association
https://tv.sohu.com/apple-app-site-association:跳轉(zhuǎn)到首頁
{
"applinks":{
"apps":[],
"details":[
{
"appID":"X3XWZ5HCGK.com.sohu.iPhoneVideo",
"paths":[
"/app/*"
]
},
{
"appID":"VB2VQ6GKB2.com.sohu.inhouse.iphonevideo",
"paths":[
"/app/*"
]
},
{
"appID":"4AW78593E8.com.sohu.mobile.iPhoneVideo",
"paths":[
"/app/*"
]
},
{
"appID":"89DSCLLV97.com.sohu.SohuVideo",
"paths":[
"/app/*"
]
},
{
"appID":"VB2VQ6GKB2.com.sohu.inhouse.sohuvideoipad",
"paths":[
"/app/*"
]
},
{
"appID":"4AW78593E8.com.sohu.mobile.SohuVideo",
"paths":[
"/app/*"
]
}
]
}
}
思路2:用戶點擊產(chǎn)生的URL變化才能觸發(fā)App跳轉(zhuǎn)
Universal Links觸發(fā)App跳轉(zhuǎn)的一個很重要的前提是,這個URL的變化一定是用戶點擊造成的!直接粘貼或者通過JavaScript方式修改的Universal Links都是無效的。因此我們可以考慮向WebView注入JavaScript,監(jiān)聽用戶URL的點擊事件,當(dāng)點擊發(fā)生時,我們截斷事件的傳播,并使用定時器延時修改WebView的URL,讓系統(tǒng)誤以為這是一個純JavaScript調(diào)用,與用戶點擊無關(guān)。
這個思路的好處在于可以做出比較通用的,適合所有站點的方案。但弊端在于,首先,延時更新URL會導(dǎo)致用戶體驗上的卡頓;其次,這個思路要求在用戶點擊的那一刻,我們必須獲取真實的目的URL,如果<a>的格式為<a href='javascript:;'>,那我們就沒辦法了,因為真實的URL是通過一段JavaScript動態(tài)計算出來的,點擊時拿不到。
這里以Bilibili為例。首先,參考思路1中獲取Universal Links domain的方法,拿到它的archived-expanded-entitlements.xcent。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:bangumi.bilibili.com</string>
<string>applinks:live.bilibili.com</string>
<string>applinks:www.bilibili.com</string>
<string>applinks:m.bilibili.com</string>
<string>applinks:space.bilibili.com</string>
<string>applinks:d.bilibili.com</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.tv.danmaku.bilianime</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>746845GC96.tv.danmaku.bilianime</string>
</array>
</dict>
</plist>
com.apple.developer.associated-domains包括以下domains:
- bangumi.bilibili.com
- live.bilibili.com
- www.bilibili.com
- m.bilibili.com
- space.bilibili.com
- d.bilibili.com
全是主域名。。。按下面的操作步驟,也確實出現(xiàn)了在Safari中點著點著就跳到Bilibili的App的情況。
iOS Safari中打開m.bilibili.com >> 點擊上方Tab的“番劇” >> 點擊某個“連載動畫” >> 跳轉(zhuǎn)到Bilibili App。
之所以會這樣,是因為B站一般的域名都是m.bilibili.com,但番劇的某些域名是bangumi.bilibili.com。對于這個站點,顯然是無法使用思路1中的方法了。
按照思路2,我們先創(chuàng)建下面的JavaScript代碼:
(function() {
var url = window.location.host;
if (url.search(".bilibili") >= 0) {
function updateHref(href, event) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
// use timer and fakeURL to fool the system
var fakeURL = 'javascript:;';
setTimeout(function () {
window.location.href = fakeURL;
setTimeout(function () {
window.location.href = href;
}, 20);
}, 20);
};
function eventHandler(event) {
var element = event.target;
while (element) {
if (element.tagName == 'A') {
break;
};
element = element.parentElement;
};
if (!element || element.tagName != 'A') {
return;
};
if (element.href == undefined || element.href.length == 0) {
return;
};
if (element.href.search('javascript') == 0) {
return;
};
var hrefAttr = element.href;
if (!hrefAttr.includes('bilibili')) {
return;
};
updateHref(hrefAttr, event);
};
// Some <a> elements are added after this JavaScript is injected, so add event to body to make sure all element events could be handled.
document.body.addEventListener('click', eventHandler, true);
};
})();
上面的代碼有以下重點:
-
addEventListener必須添加到body上面,才能確保監(jiān)聽到所有<a>element的點擊事件。 -
eventHandler()用于找到有效的<a>.href,并將值傳遞給updateHref() -
updateHref()首先截斷點擊事件的傳遞,然后使用假URL和timer讓系統(tǒng)誤以為這次URL變化是純粹的javascript調(diào)用。
JavaScript的代碼到此為止,接下編寫App代碼。代碼很簡單,創(chuàng)建一個WKWebView,并在. atDocumentEnd時加載上面的JavaScript代碼即可。
class ViewController: UIViewController {
lazy var webView: WKWebView = {
let webView = WKWebView(frame: .zero)
let js = try! String(contentsOfFile: Bundle.main.path(forResource: "appJump", ofType: "js")!)
let appJumpScript = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(appJumpScript)
return webView
}()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
webView.frame = view.bounds
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
webView.load(URLRequest(url: URL(string: "http://m.bilibili.com")!))
}
}