如何限制iOS Universal Links跳转

如何限制iOS Universal Links跳转

有时由于产品需求,我们需要使用一个WKWebView来呈现第三方平台的内容。当第三方平台拥有自己的App时,通常都会在网页端引导用户跳转到第三方App。iOS Universal Links是实现这种跳转的一种常见方式,已经有很多文章讨论如何实现Universal Links,本文则是反其道而行之,讨论如何禁用Universal Links触发的App跳转。

Universal Links原理

要想禁用Universal Links,首先就需要了解它的原理。本文只是简要介绍一下它的基本原理,详情可以参考下列文档:

官方文档:Support 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的实现

要实现Universal Links,需要对网页的服务器端和iOS App端同时做一些配置:

对于服务器:

  • 创建一个名为apple-app-site-association的JSON文件,用于描述App可以将哪些URL当做Universal Link来处理
  • 将这个apple-app-site-association文件上传到HTTPS web服务器。该文件可以被放置在服务器的根目录,或者.well-know子目录。例如,Bilibili的配置文件就是放置在根目录,http://www.bilibili.com/apple-app-site-association。有兴趣的可以打开这个链接,其实就是一个JSON文件,具体含义可以参考官方文档Support Universal Links

对于App:

  • 创建一个名为com.apple.developer.associated-domains的entitlement,包含App支持的Universal Link的domain。注意,不同的子域名都会被当做不同的domain,比如www.bilibili.comm.bilibili.com就是两个domain。
  • AppDelegate.application:continueUserActivity:restorationHandler:中响应WebView传入的Universal Links。

com.apple.developer.associated-domains v.s. apple-app-site-association

  • 前者配置在App端,后者配置在网页端。
  • 前者针对domain,后者针对domain下的URL,指明某个domain下哪些URL可以被当做Universal Links。

Universal Links的工作流程

上面一节介绍的是如何配置服务器端和App端以便支持Universal Links,那么当这一切都部署好之后,用户在WebView/Safari上点击了一个Universal Link后,本地App是如何被打开的?

  • App A安装成功后,iOS会根据其com.apple.developer.associated-domains中列出的domain,下载对应的apple-app-site-association文件。
  • 用户在App B的WebView中点击一个URL后,该WebView的webView(:, decidePolicyFor:, decisionHandler:)被触发(如果存在),决定是否允许访问该网址。
  • 如果上一步允许访问,则系统会结合com.apple.developer.associated-domainsapple-app-site-association判断该URL是否为Universal links,若不是,则直接在网页中打开。若是,则做出下列判断:
    • 若手动关闭了Universal Links跳转(见下一节说明),直接在网页中打开新网址。
    • 若不是用户手动点击的操作,直接在网页中打开新网址。
    • 若新旧网址属于同一域名,直接在网页中打开新网址。例如,www.bilibili.comm.bilibili.com属于不同域名,在它们之间切换会触发App跳转,但在m.bilibili.com的不同网址间切换并不会触发。
    • 否则,打开App A,并调用它的AppDelegate.application:continueUserActivity:restorationHandler:

Universal Links的坑

突破微信跳转限制-Universal Links那些坑 总结了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跳转到App后,又点击了屏幕右上角的URL(如下图),iOS会在网页端再次打开这个链接。此外,系统还会认为用户偏向于在网页端查看URL,因此用户再次点击超链时,系统不会再跳转到App,相当于用户手动关闭了Universal Links。如果想再次启动Universal Links,用户需要在网页端手动点击屏幕右上方的“打开

禁用Universal Links


禁用Universal Links

禁用Universal Links


启用Universal Links

限制Universal Links

上面介绍了Universal Links的基本原理,根据这些原理,我们有两个禁用Universal Links的思路。

思路1:webView(: decidePolicyFor: decisionHandler:)

上面讲到,当Universal Links被点击时,我们App的webView(: decidePolicyFor: decisionHandler:)会首先被触发,用来决定是否允许对该URL的访问,如果我们事先知道哪些URL属于Universal Links,就可以在这个地方将它们禁掉。

使用这个方法的好处在于简单,如果我们的App只会访问有限数量的第三方网站,那么只需要找到每个网站Universal Links的格式即可。但若我们App可能打开任意的网站,那就不能用这个办法了。

那么如何找到Univeral Links的网址格式?简单的方法就是在webView(: decidePolicyFor: decisionHandler:)中设置断点,然后点击某个会导致App跳转的链接即可。正常的网站通常会用一个非常有别于它们主域名的网址来作为会导致App跳转的网址,比如搜狐的跳转网址就是形如http://s1.h5.itc.cn/app/phone.html?xxxxx,因此这个方法基本够用。

如果想找到最完备的Universal Links列表,可以采用下面的方式,此处以搜狐视频为例:

  1. 在Mac上使用iTunes下载搜狐视频。iTunes通常把App保存在/Users/XXX/Music/iTunes/iTunes Media/Mobile Applications目录下,(把XXX替换为你的用户名)
  2. 使用解压工具解压搜狐视频的ipa包,生成一个文件夹,点击其中的Payload >> SOHUVideo >> 右击 >> 选择“Show Package Contents” >> 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: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都是主站的域名,我们显然也不可能禁用主站的域名,否则会导致网页无法访问的情况。有人可能有点不放心,觉得好多域名没有被禁用,是否可能出现App跳转的情况?万一出现从m.tv.sohu.com跳wx.m.tv.sohu.com的情况怎么办?一般来说,对于一个正常的网站,比如搜狐视频,在上线Universal Links时,肯定会避免跨域名跳转的情况,否则若用户安装了搜狐视频的App,然后使用Safari在浏览搜狐的网页,点着点着,突然毫无征兆的跳到了App,这绝对是很差的一个用户体验。

当然,有些网站就是喜欢不走平常路,所有的Universal links的domain都是主域名,而且还有跨域名跳转的情况,这时候你可能只能使用下面的思路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:跳转到首页

{
"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:用户点击产生的URL变化才能触发App跳转

Universal Links触发App跳转的一个很重要的前提是,这个URL的变化一定是用户点击造成的!直接粘贴或者通过JavaScript方式修改的Universal Links都是无效的。因此我们可以考虑向WebView注入JavaScript,监听用户URL的点击事件,当点击发生时,我们截断事件的传播,并使用定时器延时修改WebView的URL,让系统误以为这是一个纯JavaScript调用,与用户点击无关。

这个思路的好处在于可以做出比较通用的,适合所有站点的方案。但弊端在于,首先,延时更新URL会导致用户体验上的卡顿;其次,这个思路要求在用户点击的那一刻,我们必须获取真实的目的URL,如果<a>的格式为<a href='javascript:;'>,那我们就没办法了,因为真实的URL是通过一段JavaScript动态计算出来的,点击时拿不到。

这里以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

全是主域名。。。按下面的操作步骤,也确实出现了在Safari中点着点着就跳到Bilibili的App的情况。

iOS Safari中打开m.bilibili.com >> 点击上方Tab的“番剧” >> 点击某个“连载动画” >> 跳转到Bilibili App。

之所以会这样,是因为B站一般的域名都是m.bilibili.com,但番剧的某些域名是bangumi.bilibili.com。对于这个站点,显然是无法使用思路1中的方法了。

按照思路2,我们先创建下面的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上面,才能确保监听到所有<a>element的点击事件。
  • eventHandler()用于找到有效的<a>.href,并将值传递给updateHref()
  • updateHref()首先截断点击事件的传递,然后使用假URL和timer让系统误以为这次URL变化是纯粹的javascript调用。

JavaScript的代码到此为止,接下编写App代码。代码很简单,创建一个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")!))
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容