你的支持對我意義重大孟辑!
?? Hi扑浸,我是旭銳燕偶。本文已收錄到 GitHub · Android-NoteBook 中指么。這里有 Android 進階成長路線筆記 & 博客,有志同道合的朋友巫财,歡迎跟著我一起成長哩陕。(聯(lián)系方式在 GitHub)
1. 背景
- 你一定遇到過這個場景悍及,你在微信上瀏覽一個朋友分享的淘寶商品心赶,但是你想要在淘寶 App 上打開(畢竟原生 App 才能提供更完整和流暢的體驗)。此時椭符,你需要打開淘寶 App销钝,然后再通過搜索功能一步步找到剛才的商品曙搬。然后鸽嫂,然后就沒有然后了据某。
- 跳轉過程的操作路徑太長了癣籽,用戶體驗很一般筷狼,用戶很容易在這個過程中流失匠童。那么汤求,有沒有可能實現(xiàn)瀏覽器點擊鏈接直達商品頁的流暢用戶體驗呢严拒?這就是 App 深度鏈接的要做的事了裤唠。
簡單來說种蘸,App 深度鏈接(Deep Link)是一項基礎的 App 優(yōu)化方法劈彪,通過技術手段縮短了用戶操作路徑沧奴,從而優(yōu)化了產(chǎn)品服務的用戶體驗长窄,最終幫助實現(xiàn)了轉化率提升挠日、用戶增長等業(yè)務目標。
2. 應用場景
一鍵跳轉是深度鏈接比較重要的使用場景,但它的能力不僅于此只冻,主要包括以下幾種:
- 一鍵跳轉: 在用戶已安裝 App 的情況下喜德,從瀏覽器或 QQ垮媒、微信等社交平臺睡雇,一鍵拉起 App 并直達落地頁它抱;
- 傳參安裝(也叫延遲深度鏈接): 在用戶未安裝 App 的情況下秕豫,引導用戶到應用市場下載安裝應用,并在應用首次啟動后自動直達落地頁抗愁;
這兩個場景分別對應用戶已安裝 App 和未安裝 App 的兩種情況馁蒂,在此基礎上呵晚, 還可以衍生出其他一些業(yè)務化的場景:
- 分享閉環(huán)(也叫場景還原): 用戶將 App 內容分享到微信等社交平臺,其他用戶通過分享鏈接打開或安裝后打開 App沫屡,自動直達分享內容饵隙,實現(xiàn)流量閉環(huán);
-
無碼邀請: 用戶通過二維碼 / 鏈接等形式邀請新用戶安裝金矛,新用戶下載安裝后可以識別出邀請來源,免除填寫邀請碼勺届,對用戶更友好(通常是在鏈接中拼接來源業(yè)務標識驶俊,例如 code=
[內容頁類型]_[內容 ID]_[App標識]_[用戶標識]
); - 渠道追蹤: 用戶通過 Web 下載引導頁安裝 App 后免姿,首次啟動時 App 識別并統(tǒng)計下載渠道饼酿,實現(xiàn)渠道效果歸因。
3. 數(shù)據(jù)流轉流程
在深度鏈接的工作流程需要 Wap 端胚膊、客戶端和服務端協(xié)同配合故俐,整體的數(shù)據(jù)流轉示意圖如下:
- Wap 端: 判斷設備是否安裝指定 App,已安裝則直接拉起 App 并傳遞深度鏈接參數(shù)紊婉,未安裝則引導用戶重定向到應用市場安裝 App药版,并將深度鏈接參數(shù)暫存到服務器;
- 服務端: 主要是為了兼容設備未安裝 App 的場景喻犁,可以理解為是前端和客戶端之間的通訊橋梁槽片。前端會將設備唯一標識和深度鏈接參數(shù)的映射關系臨時存儲在服務器,將來一段時間內肢础,客戶端可以憑借設備唯一標識從服務端讀取參數(shù)还栓;
- 客戶端: 根據(jù)前端直傳的深度鏈接直達落地頁,或者在首次啟動時嘗試從服務端讀取暫存的參數(shù)乔妈,再直達落地頁蝙云。
4. 一鍵跳轉實現(xiàn)原理
在用戶已安裝 App 的情況氓皱,可以通過標準的協(xié)議實現(xiàn)一鍵拉起 App 并傳遞深度鏈接參數(shù)路召,目前主要有以下三種協(xié)議:
Deep Link | 描述 | 適用系統(tǒng) |
---|---|---|
Scheme 協(xié)議 | 所有系統(tǒng)支持的 App 相互調用的協(xié)議,并且可以傳遞參數(shù) | 所有系統(tǒng) |
App Links | Google 在 Android M 提出的深度鏈接實現(xiàn)波材,我還沒發(fā)現(xiàn)它比 Scheme 的優(yōu)勢在哪里股淡。如果你們項目用了,請告訴我為什么 | Android M(6)+ |
Universal links | Apple 在 iOS 9(WWDC 2015)推出的通用鏈接的 deep link 特性 | iOS 9 + |
這里我們主要介紹 Android 端的實現(xiàn)廷区,主要分為以下幾個步驟:
-
配置 AndroidManifest.xml: 在 AndroidManifest.xml 中定義接收參數(shù)的 Activity唯灵,并配置 IntentFilter 篩選期望接收的參數(shù)。例如:
<activity android:name=".app.ProxyActivity" android:exported="true" android:launchMode="singleTask" android:theme="@style/SplashTheme"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <!-- 注意:path 必須有 / 前綴 --> <data android:scheme="xiaopeng" android:path="goodsId" /> </intent-filter> </activity>
這里需要注意下幾個細節(jié):
-
android:exported: 從 Android 12 開始隙轻,所有支持隱式啟動的組件必須顯式設置
android:exported
屬性埠帕,因此這里必須設置 android:exported="true"垢揩; -
SplashTheme 主題: Scheme 協(xié)議是支持冷啟動拉起 App 的,為了保證與點擊 Launcher 冷啟動的體驗相同(如
windowBackground
占位圖)敛瓷,需要用到 SplashActivity 的主題叁巨。
-
android:exported: 從 Android 12 開始隙轻,所有支持隱式啟動的組件必須顯式設置
-
解析 Intent: 通過 Scheme 拉起的 Activity,在其 Intent 中會包含 Web 端傳遞過來的深度鏈接參數(shù)呐籽。參數(shù)實體是一個 URI 格式的字符串锋勺,獲取到這個 URI 后,App 就可以根據(jù)自定義協(xié)議來拉起落地頁狡蝶。例如:
class ProxyActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (null == savedInstanceState) { dispatchIntent(intent) } finish() } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) dispatchIntent(intent) finish() } private fun dispatchIntent(intent: Intent?) { if (null == intent) { return } val uri : Uri = intent.data ?: return // 根據(jù)自定義協(xié)議解析 Uri } }
-
延遲直達: 嚴格來說庶橱,每次啟動
ProxyActivity
就立刻拉起落地頁并不是一個可靠的方式,因為有時候在拉起落地頁前有一些無法跳過的初始化頁面贪惹。比如用戶之前清除過 App 數(shù)據(jù)苏章,或者 App 隱私政策更新,這個時候一定需要用戶先同意隱私政策奏瞬,再拉起落地頁布近。又比如需要用戶先進入啟動廣告,或進行一些必要的設置頁(選擇個性標簽丝格、選擇城市等)才允許進入落地頁撑瞧。經(jīng)過分析,可以歸納出這些場景都是在 App 冷啟動的時候發(fā)生的显蝌,所以我們只要區(qū)分下冷啟動和熱啟動進入ProxyActivity
的情況即可预伺。偽代碼示例:class ProxyActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (null == savedInstanceState) { dispatchIntent(intent) } finish() } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) dispatchIntent(intent) finish() } private fun dispatchIntent(intent: Intent?) { if (null == intent) { return } val uri : Uri = intent.data ?: return if (SchemeHelper.getRunningActivityCount() == 1) { // 1. 冷啟動 // 1.1 將 Uri 臨時存儲到全局靜態(tài)域 SchemeHelper.setPendingSchemeUri(intent.data) // 1.2 轉而啟動 SplashActivity,走正常點 Launcher 的啟動流程 startActivity(Intent(this, SplashActivity::class.java)) } else { // 2. 熱啟動曼尊,直接打開落地頁 SchemeHelper.handleDeepLink(this, uri) } } } /** * 主頁面 */ class MainActivity : BaseActivity() { // 啟動初始化邏輯走完后調用: SchemeHelper.handleDeepLink(this) } - 1酬诀、SchemeHelper.getRunningActivityCount():registerActivityLifecycleCallbacks 回調,在 Activity onCreate 和 onDestroy 時記維護 runningActivityCount - 2骆撇、SchemeHelper.setPendingSchemeUri(): 將 Uri 臨時存儲到全局靜態(tài)域 - 3瞒御、SchemeHelper.handleDeepLink(): 根據(jù)自定義協(xié)議解析 Uri
-
統(tǒng)一路由入口: 除了剛才提到了冷啟動和熱啟動的兩個場景,實踐中的 App 跳轉或路由行為可遠不止這兩種神郊,例如:
- App 原生跳轉
- App 網(wǎng)頁容器跳轉 App 原生頁
- Scheme 協(xié)議喚醒 App 的跳轉(如前所述肴裙,分為冷啟動和熱啟動)
- Push 消息息喚醒 App 跳轉(分為端外推送和端內推送)
那么,這么多入口的路由行為如果不統(tǒng)一起來涌乳,對于后序的維護工作會劣化蜻懦。所以這塊需要把 SchemeHelper 中的協(xié)議解析部分統(tǒng)一封裝為全局可用的路由分發(fā)器 RootUrlDispatcher ,實現(xiàn)收口夕晓。
-
adb 測試: 如果開發(fā)階段自測時需要依賴 Web 端給我們提供一個網(wǎng)頁來拉起 App宛乃,測試效率就太低了。我們可以使用 adb 命令來自測:
命令模板: adb shell am start -W -a android.intent.action.VIEW -d <URI-定義的URI> <PACKAGE-需要測試的應用包名> 示例: adb shell am start -W -a android.intent.action.VIEW -d "xiaopeng://www.myapp.com/goods/?goodsId=123456" com.xiaopeng.app
5. 自定義 Scheme 協(xié)議設計
自定義 Scheme 協(xié)議本質上就是定義一套標識 App 行為的規(guī)則,實踐中采用的 URI(Uniform Resource Identifier征炼,統(tǒng)一資源標識符) 方案析既,下圖是 URI 的通用格式:
實踐中的設計過程多少會帶點 Restful API 的風格。Restful 本身是接口命名的一種規(guī)范谆奥,用 URI 標識一種資源渡贾,再用 HTTP 方法來定義對資源的操作。比如定義 /goods/{goodsId} 是商品的路徑雄右,那么對于商品這個資源的操作可以分為以下幾種:
-
獲取商品信息:
GET /goods/123456
-
修改商品信息:
POST/PATCH /goods/123456
-
刪除商品:
DELETE /goods/123456
把 Restful API 這套理論帶到 App 這邊空骚,是不是也適用呢?比如以下行為是不是也可以用 Restful API 的風格表示:
-
打開 App 商品詳情頁:
GET GoodsDetailActivity/123456
- 修改 / 刪除商品詳情: 經(jīng)過分析擂仍,這個行為在 Scheme 的場景不成立囤屹;
-
打開 App 商品推薦列表:
GET GoodsListActivity
-
打開 App 商品評價頁:
GET GoodsCommentDetailActivity/123456
-
打開 App 商品評價修改頁:
GET GoodsToCommentActivity/123456
(是的,即使是修改的行為也用 GET逢渔,這就是 App 相對于 API 的差異性肋坚,因為 GoodsToCommentActivity 本身就帶修改的動作)
既然在 App 端對資源的訪問行為只有 GET,那么就可以省略掉 GET 這個元素肃廓。再考慮到鏈接需要跨平臺智厌,還有多參數(shù)等因素,鏈接模板需要再進一步改進盲赊。一般推薦采用這種格式的 URI:scheme://host/path?query铣鹏。 例如,鏈接 xiaopeng://www.myapp.com/goods/?goodsId=123456&size=1
打開商品詳情頁哀蘑,并且選擇 size=1 的規(guī)格诚卸。
部分 | 參數(shù) | 描述 |
---|---|---|
scheme | xiaopeng:// |
業(yè)務獨有的領域,一個 App 可以支持多個 Scheme |
host | www.myapp.com/ |
某一個子域名 |
path | goods/ |
頁面路徑绘迁,可以多級別 |
query | ?goodsId=123456&size=1 |
頁面參數(shù)合溺,可以多參數(shù) |
這里需要注意下幾個細節(jié):
-
登錄引導: 我們定義了 needLogin 這個參數(shù)呢,因為實踐中發(fā)現(xiàn)用戶的賬單詳情頁這一類落地頁是一定要求用戶登錄的缀台。所以我們在拉起落地頁之前增加了一個登錄引導棠赛,在登錄成功后再進入落地頁。例如膛腐,鏈接
xiaopeng://www.myapp.com/goods/?goodsId=123456&size=1?needLogin=1
表示打開 App睛约,先要求用戶登錄后再打開商品詳情頁,并且選擇 size=1 的規(guī)格依疼; -
H5 跳轉: 有一些活動頁是需要通過網(wǎng)頁容器來承載的痰腮,因此我們希望打開 App 后喚起
MyWebViewActivity
網(wǎng)頁容器來顯示。對于這樣的場景我們可以直接使用 http 或 https 作為 Scheme律罢,App 將這類鏈接直接轉交給MyWebViewActivity
去呈現(xiàn); - 數(shù)據(jù)加密: 為了提高安全性,URI 中的 path?query 的部分可以使用加密算法误辑,scheme://host 的部分需要用于匹配沧踏,并且不帶有風險數(shù)據(jù),可以不加密巾钉。
6. 總結
在 PC 端翘狱,瀏覽器是用戶流量的主要入口,但在移動端砰苍,用戶的流量(使用時間)被分散到大大小小的 APP 上潦匈,而不再是瀏覽器。用戶感興趣的內容分散在各個 APP 里赚导,當用戶想在 APP 上找到某個感興趣的頁面時茬缩,深度鏈接(Deeplink)是一個可以從任何地方將用戶帶到應用內容頁的簡單方式。你用起來了嗎吼旧?
參考資料
- 創(chuàng)建指向應用內容的深層鏈接 —— Android 官方文檔
- URI Scheme 的最佳實踐 —— 極光 官方文檔
- WMRouter:美團外賣Android開源路由框架 —— 子健 淵博 云馳(美團技術團隊)著
你的點贊對我意義重大凰锡!希望大家可以一起討論技術,找到志同道合的朋友圈暗,我們下次見掂为!