其實 deep linking 並不是一個新名詞,在 web 開發領域,區別於指向首頁的鏈接(http://tech.glowing.com/),deep linking 是指向具體內容頁的鏈接(http://tech.glowing.com/cn/advices-to-junior-developers/)。在移動開發領域,deep linking 則是指 mobile app 在 handle 特定 URI 的時候可以直接跳轉到對應的內容頁或觸發特定邏輯,而不僅僅是啟動 app。比如 dianping://shopinfo?id=1859284,如果你的手機上裝了大眾點評的話點擊這個鏈接可以直接跳轉到商鋪頁面。這樣做的好處主要有:
在 web 和 app 的切換過程中保留上下文
App 間帶上下文切換(用於實現 app 間參數的傳遞,如授權協議,分享 API 等)
Web 頁可以被搜索引擎索引,可以通過 SEO 增加訪問量從而提高 app 下載量和開啟率
目前處理 deep linking,主要有兩種方式:
在 universal links 出現之前的很長一段時間裡,iOS 上主要通過 custom URL scheme 來實現 deep linking,以及 app 間的通信。
在 info plist 裡設置了自定義 URL後,handle URL 的入口是 app delegate 方法 application:openURL:sourceApplication:annotation:
(iOS 9 開始被 deprecate)或 application:openURL:options:
(iOS 9 引入,但如果沒有實現這個方法,在 iOS 9 上還是會向前兼容 call 老方法,所以一般還是實現老方法)。
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { BOOL handled = NO; // code to handle the URL return handled; }
一個比較完整的 NSURL
可以包含以下部分:scheme://user:password@host:port/path?query#fragment
。但對於 deep linking 來說大部分時候只需要 scheme://host/path?query
。有時候會省去 path
部分,把 host
直接作為 command,如上文提到的點評的 link;也有些 app 會省去 query
部分,用 path
傳參,更接近 RESTful API 的風格。這取決於具體業務邏輯復雜程度以及 handler 的實現方式。有一點需要注意的是,規范的 URL 是 percentage encoded 的,所以取出來的參數需要用 stringByReplacingPercentEscapesUsingEncoding:
或 stringByRemovingPercentEncoding
(iOS 7+)方法 decode。反之,拼 URL 的時候應該使用 stringByAddingPercentEscapesUsingEncoding:
或 stringByAddingPercentEncodingWithAllowedCharacters:
(iOS 7+)方法 encode。
在 iOS 7+ 上處理 query
的時候也可以配合使用 NSURLComponents
類。
具體 handle URL 的時候,對於需要處理的業務邏輯較少的 app 來說,可以簡單地通過字符串比較來區分業務邏輯。對於業務邏輯相對復雜,特別是在跨團隊共同維護 URL handler 的時候,則需要引入 router 來分發請求。關於 router 已經有很多文章涉及,GitHub 上也有很多開源代碼可供參考或使用,比如:
DeepLinkKit
JLRoutes
Routable
HHRouter
具體選型或自己實現 router 的時候主要考慮一些問題比如:用 code 注冊還是配置文件;是否需要去中心化;如何傳參;以 view controller 還是 block (closure) 為單位來注冊 handler;是否需要像淘寶一樣做 web 版的 failover 等等……這裡不再展開。
相關文檔:Using URL Schemes to Communicate with Apps
Apple 在 iOS 9 上引入了 universal links,相較 custom URL scheme,universal links 有以下好處:
Custom URL scheme 因為是自定義的協議,所以在沒有安裝 app 的情況下是無法直接打開的,而 universal links 本身是一個 HTTP/HTTPS 鏈接,所以有更好的兼容性。
不同的 app 是可以定義相同的 custom URL scheme 的,所以會存在搶占或沖突的問題,而 universal links 是從 server 查詢由哪個 app 打開的,所以不存在上述問題。
Universal links 支持從其他 app 的 MKWebView 或 UIWebView 中跳轉到目標 app。
Universal links 本身可以被搜索引擎索引。
Universal links 的具體實現可以參考官方文檔:Support Universal Links。簡單來說你需要:
添加一個 apple-app-site-association
文件到你的網站來描述 URL 和 app 的關聯。
添加 com.apple.developer.associated-domains
entitlement 來指定要從哪些域名查詢 universal links support。
在 app delegate 的 application:continueUserActivity:restorationHandler:
方法中 handle userActivity.webpageURL
。
處理 URL 本身的方法跟前面處理 custom URL 類似,不再贅述。
顧名思義,deferred deep linking 是指用戶打開一個 web page 的時候並沒有安裝對應的 app,希望用戶在安裝 app 以後可以 deep link 到對應內容。這裡有三個需要解決的問題:
判斷是否已經安裝了 app,如果已經安裝了直接 deep link 到 app,否則跳轉 App Store。
用戶匹配(user matching),如何把一個 install 對應到某一次 web page view 或者某一次 click。
Deep linking
以前在使用 custom URL 的時候一般用類似這樣的一段 JS 處理:
window.location = 'lexie://'; setTimeout(function() { window.location = 'itms-apps://itunes.apple.com/us/app/eve-by-glow-period-tracker/id1002275138' }, 250);
這是因為在 iOS 9.2 以前,Safari 裡是否用 app 打開 custom URL 的提示是 blocking JS 的,所以如果用戶同意用 app 打開鏈接以後就不會跳轉 App Store,反之,用戶選擇取消或者並沒有安裝 app 的時候,會跳轉 App Store。iOS 9.2 Apple 做了一個更新就是這個提示不再 block JS,所以無論如何都會跳轉 App Store。因此現在會推薦使用 universal links 來實現這樣的邏輯,對於需要強制安裝 app 後才能浏覽的內容,可以提供一個直接跳轉 App Store 的中轉頁面,如果裝了 app,iOS 會自動跳轉到 app 內處理。
這曾經是個老大難的問題,受系統所限,在 iOS 上很難追蹤到一個安裝的來源,但是這樣的需求又很多,主要的場景有:
追蹤廣告效果
追蹤用戶推薦/邀請鏈接
在 app 內保持網頁浏覽的上下文,如登錄信息,購物車等
對於這個問題,在 iOS 9 以前常見的做法是猜,沒錯,就是用猜的。在訪問特定頁面或點擊特定鏈接的時候記錄用戶特征,如 IP,系統版本,手機型號,語言等等。然後在打開 app 的時候發送這些特征到服務器,查詢一段時間內(如 1 小時內)有可以匹配的用戶點擊過的鏈接,然後處理這個鏈接。這樣做的缺點很明顯,因為是通過特征模糊匹配的,所以很容易匹配不到或匹配到錯誤的上下文。但是其實大部分第三方服務會從不同來源收集更多信息,所以這個准確率其實比想象中高很多,尤其是在打開了 IDFA 的情況下。
這個問題卻在 iOS 9 引入 SFSafariViewController
以後得到了很好的解決,因為 SFSafariViewController
和 Safari 的 cookies 是互通的!所以理論上可以做到 100% 的 match。解決方案也很簡單,本地生成一個 UUID
並通過一個隱藏的 SFSafariViewController
傳回給 server,server 就可以把這個 UUID
跟之前的 session 對應起來,然後通過一般的 API call 查詢更多跟這個 session 有關的信息。具體的 code 可以參考 Branch SDK 的實現。
上個章節已經提到,不再贅述,只是處理 URL 的入口換成了某個 API 請求的 callback 裡。
有很多第三方提供了 deep linking 和 deferred deep linking 的服務,比如 AppsFlyer 和 Branch。目前在 Glow 的 app 裡這兩個 SDK 都有用到。
其中 AppsFlyer 的優勢在於他們跟很多公司有合作關系,比如 Facebook,所以用於追蹤 Facebook 廣告效果表現較好。另外 AppsFlyer 支持很多第三方服務的 server callback,可以方便集成很多第三方服務。缺點是 AppsFlyer 按 non-organic install 量收費。而且 AppsFlyer 的 SDK 和 API doc 寫的不是很好,在 track 安裝以後的後續 deep link 的時候感覺有很多 bug。
Branch 的優勢在於免費,SDK 和 API doc 都寫的比較好,而且有一些特殊的功能比如用戶邀請及獎勵之類的,適合做一些運營活動。另外 Branch 可以實現一個 link 根據平台自動跳轉不同 Store,甚至可以在 desktop 上通過短信發送可以追蹤的鏈接。缺點是 Branch 運營時間不久,服務穩定性有待驗證,dashboard 的功能也還比較輕量。
總的來說 AppsFlyer 更適合 track 廣告效果,Branch 更適合實現 feature。必須一提的是,因為這兩個服務都是主要面向海外市場的,所以曾經都遇到過國內短暫抽風的現象,所以國內的 app 如果要用的話風險自擔 :) 如果國內有類似的服務的話也歡迎留言補充。
Branch 的集成比較簡單,參見官方文檔。一個需要注意的是,自己實現的時候在 handle URL 或者 user activity 的時候可以直接處理 URL,但是用 Branch 的時候,第一級的 URL 是 Branch 的 URL,所以要通過 [[Branch getInstance] handleDeepLink:url]
和/或 [[Branch getInstance] continueUserActivity:userActivity]
交由 Branch 處理,然後在 init Branch 時傳入的 block (closure) 中處理各類參數:
[branch initSessionWithLaunchOptions:launchOptions andRegisterDeepLinkHandler:^(NSDictionary *params, NSError *error) { if (!error) { // params are the deep linked params associated with the link that the user clicked -> was re-directed to this app // params will be empty if no data found // ... insert custom logic here ... NSLog(@"params: %@", params.description); } }];