iOS無(wú)埋點(diǎn)數(shù)據(jù)SDK的整體設(shè)計(jì)與技術(shù)實(shí)現(xiàn)

iOS無(wú)埋點(diǎn)數(shù)據(jù) SDK 實(shí)踐之路
iOS無(wú)埋點(diǎn)SDK 之 RN頁(yè)面的數(shù)據(jù)收集

本篇文章是講述 iOS 無(wú)埋點(diǎn)數(shù)據(jù)收集 SDK 系列的第三篇,之前的兩篇文章都只是講述了某一方面的內(nèi)容,而本篇會(huì)詳細(xì)介紹下 SDK 的整體設(shè)計(jì)以及各個(gè)模塊的功能和實(shí)現(xiàn)思路。

SDK 的整體設(shè)計(jì)

先看一張 SDK 的整體設(shè)計(jì)圖:

從上圖看出,SDK 整體上主要包含 4 個(gè)部分:AOP、Event CollectorEvent CacheEvent Upload。其中,每個(gè)部分是一個(gè)相對(duì)獨(dú)立的功能模塊,同時(shí)模塊之間通過(guò)圖中的方式進(jìn)行通信。

SDK 中的這 4 個(gè)模塊各自的主要功能如下:

  • AOP:提供數(shù)據(jù)收集所需要的時(shí)機(jī),即通過(guò) Method Swizzling 來(lái) hook 相應(yīng)類(lèi)的方法,然后以 Post Notification 的方式提供出去。
  • Event Collector:監(jiān)聽(tīng)通知,針對(duì)當(dāng)前事件執(zhí)行相應(yīng)的數(shù)據(jù)收集,并將收集的事件數(shù)據(jù)提交給緩存模塊。
  • Event Cache:負(fù)責(zé)事件數(shù)據(jù)的緩存、序列化以及讀取操作,其中包括內(nèi)存緩存與磁盤(pán)緩存。
  • Event Upload:基于一定的上報(bào)策略執(zhí)行對(duì)已收集的事件數(shù)據(jù)的上報(bào)。

接下來(lái)逐個(gè)介紹上述 4 個(gè)模塊的具體實(shí)現(xiàn)細(xì)節(jié)。

AOP

這個(gè)模塊的主要功能就是提供 SDK 執(zhí)行數(shù)據(jù)收集所需的時(shí)機(jī),在實(shí)現(xiàn)上又可以細(xì)分為 2 個(gè)方面:

  1. 實(shí)現(xiàn) AOP 編程
  2. hook 類(lèi)的方法

實(shí)現(xiàn) AOP 編程

在 iOS 中實(shí)現(xiàn) AOP 編程的技術(shù)就是基于 Objective-C Runtime 特性的 Method Swizzling。而在 Github 上已經(jīng)有一個(gè)很不錯(cuò)的實(shí)現(xiàn)了 AOP 的開(kāi)源庫(kù)-Aspects,它的實(shí)現(xiàn)也是利用了 Objective-C 的消息轉(zhuǎn)發(fā)機(jī)制與 Method Swizzling 黑魔法。

但是,SDK 最終并未使用 Aspects 庫(kù),雖然 Aspects 封裝的很好而且很好用,但是它并不能完全滿(mǎn)足項(xiàng)目的需要,主要表現(xiàn)在如下 2 個(gè)方面:

  1. Aspects 無(wú)法 hook 類(lèi)中不存在的方法,或者未實(shí)現(xiàn)的方法。
  2. Aspects 不支持 hook 類(lèi)的類(lèi)方法。

因此,SDK 單獨(dú)實(shí)現(xiàn)并封裝了一個(gè)用于執(zhí)行 hook 的類(lèi),其實(shí)現(xiàn)也是對(duì) NSObject 的擴(kuò)展,類(lèi)似于 Aspects。

hook 的方法

上篇文章 中簡(jiǎn)單提了一下,SDK 在實(shí)現(xiàn)對(duì)基本事件數(shù)據(jù)的自動(dòng)收集時(shí),主要 hook 的方法分為 3 類(lèi):

  • 系統(tǒng)類(lèi)的方法
  • 系統(tǒng)類(lèi)的 Delegate 方法
  • 自定義類(lèi)的方法

那么,接下來(lái)就詳細(xì)的介紹一下,SDK 在實(shí)現(xiàn)對(duì)事件的收集時(shí),具體 hook 了哪些類(lèi)的哪些方法。

各類(lèi)點(diǎn)擊事件的攔截

對(duì)于 SDK 來(lái)說(shuō),收集用戶(hù)的所有點(diǎn)擊的行為數(shù)據(jù)是非常重要的一部分。另外,這部分?jǐn)?shù)據(jù)對(duì)于用戶(hù)行為分析以及統(tǒng)計(jì)路徑轉(zhuǎn)化率時(shí),都是至關(guān)重要的。

那么 SDK 對(duì)于用戶(hù)的各類(lèi)點(diǎn)擊事件的收集,主要 hook 了如下的一些系統(tǒng)類(lèi)的方法:

針對(duì)上圖,做一些簡(jiǎn)要的說(shuō)明:

  1. 所有的 UIControl 類(lèi)型的控件、UITabBarButton 以及在導(dǎo)航欄上自定義添加的 UIBarButtonItem 的點(diǎn)擊事件,都可以通過(guò) hook 系統(tǒng)類(lèi)UIApplicationsendAction:to:from:forEvent: 方法進(jìn)行攔截。但是,這個(gè)方法并不能攔截到導(dǎo)航欄上系統(tǒng)自動(dòng)添加的返回按鈕的點(diǎn)擊,因此 SDK 又 hookUINavigationControllernavigationBar:shouldPopItem: 方法來(lái)實(shí)現(xiàn)對(duì)它的點(diǎn)擊的攔截。
  2. 針對(duì)與手勢(shì)相關(guān)的事件,SDK 首先通過(guò) hook 系統(tǒng)類(lèi) UIGestureRecognizerinitWithTarget:action:addTarget:action: 這 2 個(gè)方法拿到 target 對(duì)象與 action 方法,然后再去 hook target 的 action 方法,從而能夠攔截到手勢(shì)相關(guān)的事件。
  3. 對(duì)于 UITableView、UICollectionView 某一行的點(diǎn)擊,首先 hook 它們的 setDelegate: 方法,從而拿到 delegate 對(duì)象,然后再去 hook delegate 的 didSelectRowAtIndexPath: 方法即可。
  4. 對(duì)于 RN 頁(yè)面中的點(diǎn)擊,是通過(guò) hook RN 框架中的 RCTUIManager 類(lèi)的 setJSResponder:blockNativeResponder: 方法,具體原因可以看 這篇文章 的詳細(xì)講解。另外,為了避免 SDK 對(duì) RN 框架產(chǎn)生依賴(lài),通過(guò) NSClassFromString(@"RCTUIManager") 來(lái)判斷當(dāng)前主工程是否使用了 RN 框架,如果未獲取到此類(lèi),則不執(zhí)行 hook 操作。
  5. 對(duì)于系統(tǒng)彈窗的點(diǎn)擊這塊,需要攔截到 UIAlertView、UIActionSheet 以及 iOS8 上新增的 UIAlertController 這 3 個(gè)彈窗的點(diǎn)擊。對(duì)于前2個(gè),只需要 hook 它們的 delegate 方法。而對(duì)于 UIAlertController 是沒(méi)有提供相應(yīng)的 delegate 方法的,這里可以通過(guò) hook UIAlertAction 類(lèi)的 actionWithTitle:style:handler: 類(lèi)方法來(lái)攔截到其點(diǎn)擊事件。
頁(yè)面事件的攔截

對(duì)于頁(yè)面事件的收集,主要通過(guò) hook 系統(tǒng)類(lèi) UIViewController 的生命周期方法來(lái)實(shí)現(xiàn),具體看下圖:

滑動(dòng)事件 & UIWebView加載事件

對(duì)于 iOS 中的滑動(dòng)事件、UIWebView 的加載事件的收集,SDK 主要 hook 了 setDelegate: 方法以及 UIScrollViewDelegate、UIWebViewDelegate 中的方法。其原理上與 UITableView 的類(lèi)似。具體見(jiàn)下圖:

Event Collector

SDK 通過(guò) AOP 層已經(jīng)可以拿到執(zhí)行各個(gè)事件的數(shù)據(jù)收集的時(shí)機(jī),接下來(lái)就是執(zhí)行真正的數(shù)據(jù)收集了,其中包括了對(duì) 點(diǎn)擊事件的收集、頁(yè)面事件的收集、滑動(dòng)事件的收集等。

這些要收集的事件數(shù)據(jù)中包含一些基本信息,如:eventName、appKey、eventTime、sessionId、deviceId 等。除此之外,還有一些與特定事件相關(guān)的信息,例如對(duì)于 view 的點(diǎn)擊事件,還需要收集與 view 的相關(guān)信息;對(duì)于列表行的點(diǎn)擊,還需要收集點(diǎn)擊行的 indexPath 信息;而對(duì)于 webView 加載事件則需要收集其 url 與 error 等信息。

接下來(lái)主要說(shuō)一下 SDK 中點(diǎn)擊事件的收集。

首先,對(duì)于 UIControl 控件與添加了 UITapGestureRecognizer 的 view,在收集它們的點(diǎn)擊事件的數(shù)據(jù)時(shí),重點(diǎn)收集了 2 部分內(nèi)容:pageName、viewInfo。其中,pageName 是表明點(diǎn)擊事件發(fā)生在哪個(gè)頁(yè)面,一般用 viewController 的類(lèi)名表示;viewInfo 是指當(dāng)前被點(diǎn)擊的view的一些相關(guān)信息,有:viewClass、viewPath、frame、title(如果有)、viewId 等。而 viewPath 是最關(guān)鍵的一項(xiàng)信息,能夠唯一標(biāo)識(shí)當(dāng)前 view。

其次,對(duì)于導(dǎo)航欄上的點(diǎn)擊事件的收集,與上面要收集的信息幾乎是一樣的,只是在收集 pageName 的數(shù)據(jù)時(shí)不一樣。導(dǎo)航欄的點(diǎn)擊事件默認(rèn)的 pageName 是 UINavigationController,但是為了能夠更好的分析用戶(hù)行為,這里將 App 當(dāng)前正在顯示的頁(yè)面作為其 pageName。

同理,收集系統(tǒng)彈窗的點(diǎn)擊事件時(shí),也將 App 當(dāng)前正在顯示的頁(yè)面作為其 pageName。除此之外,由于同一個(gè)頁(yè)面中可能會(huì)出現(xiàn)多個(gè)彈窗,它們的按鈕文字信息有可能一樣,比如經(jīng)常會(huì)用 “確定”、“取消” 等文字,這時(shí)單純靠按鈕的 title 無(wú)法區(qū)分這些不同的彈窗,為了解決這個(gè)問(wèn)題,又加入了系統(tǒng)彈窗的標(biāo)題(title、message)。

最后,講一下 SDK 中獲取 viewPath 的實(shí)現(xiàn)邏輯,具體如下圖所示:

Event Cache

這個(gè)模塊主要負(fù)責(zé)所有事件數(shù)據(jù)的存取及序列化操作,具體可分為如下 3 部分:

  1. 采用雙緩存的結(jié)構(gòu)將數(shù)據(jù)存儲(chǔ)在內(nèi)存中。具體實(shí)現(xiàn)是,將新添加的事件數(shù)據(jù)先存儲(chǔ)到全局?jǐn)?shù)組 eventArray 中,等滿(mǎn)足數(shù)據(jù)上報(bào)條件時(shí),從 eventArray 中讀出一部分?jǐn)?shù)據(jù)并隨機(jī)生成一個(gè)唯一的 eventsID,將其以 key-value 的形式存放到全局字典 popedEventDict 中,等這部分?jǐn)?shù)據(jù)上傳成功后再將 eventsID 對(duì)應(yīng)項(xiàng)從 popedEventDict 中移除。
  2. 在某些情況下(App 即將被殺死、程序拋出異常),將內(nèi)存中的數(shù)據(jù)以文件的形式持久化存儲(chǔ)至磁盤(pán)中,以防數(shù)據(jù)丟失。
  3. 將從內(nèi)存或文件中讀取的數(shù)據(jù)執(zhí)行 protobuf 序列化操作,以便后續(xù)的數(shù)據(jù)上傳操作。

另外,為了確保對(duì)數(shù)據(jù)存取的多線(xiàn)程安全,上述操作全部都放到了同一個(gè)串行隊(duì)列中執(zhí)行。

Event Upload

這個(gè)模塊的主要功能就是根據(jù)一定的數(shù)據(jù)上報(bào)策略,上報(bào)已收集的所有事件數(shù)據(jù)。數(shù)據(jù)上報(bào)主要包括對(duì)內(nèi)存數(shù)據(jù)和本地文件這2部分,下面分別介紹一下它們的上報(bào)策略與實(shí)現(xiàn)思路。

內(nèi)存數(shù)據(jù)的實(shí)時(shí)上報(bào)

首先,針對(duì)內(nèi)存數(shù)據(jù)的上報(bào)策略有 2 個(gè):

  1. 每隔 30 秒
  2. 每累積 10 條數(shù)據(jù)。

當(dāng)滿(mǎn)足上述條件之一時(shí),會(huì)觸發(fā)從內(nèi)存中讀取數(shù)據(jù),并執(zhí)行上傳操作。對(duì)于內(nèi)存數(shù)據(jù)的上傳,單獨(dú)創(chuàng)建了一個(gè)并發(fā)隊(duì)列,并限制其最大并發(fā)數(shù)為 10,以防由于數(shù)據(jù)頻繁時(shí)上報(bào)引起開(kāi)啟的線(xiàn)程數(shù)太多。

本地文件數(shù)據(jù)的上傳

為了盡早的上傳本地文件,以防用戶(hù)卸載 App 造成本地?cái)?shù)據(jù)的丟失,針對(duì)本地文件的上傳策略有如下 3 個(gè):

  1. App 冷啟動(dòng)
  2. App 進(jìn)入前臺(tái)
  3. App 進(jìn)入后臺(tái)

這里創(chuàng)建了一個(gè)單獨(dú)的串行隊(duì)列,來(lái)實(shí)現(xiàn)對(duì)本地文件進(jìn)行逐個(gè)上傳,即等上一個(gè)文件上傳成功后,再觸發(fā)下一個(gè)文件的上傳。因此,上述 3 個(gè)觸發(fā)時(shí)機(jī)并不會(huì)造成文件的重復(fù)上傳,并以較小的代價(jià)完成本地文件的上傳。

數(shù)據(jù)存取與上傳的實(shí)現(xiàn)流程

其實(shí)上面已經(jīng)講了大致的實(shí)現(xiàn)思路,里面設(shè)計(jì)到了使用 GCD 隊(duì)列來(lái)控制數(shù)據(jù)上傳與保證多線(xiàn)程安全。為了更清晰的展示出這 2 部分的實(shí)現(xiàn)邏輯,簡(jiǎn)單畫(huà)了一個(gè)流程圖展示出來(lái):

END

本篇文章主要介紹了無(wú)埋點(diǎn)數(shù)據(jù) SDK 的整體設(shè)計(jì),以及各個(gè)模塊的功能和實(shí)現(xiàn)思路,其中重點(diǎn)介紹了執(zhí)行事件收集所需 hook 的具體方法,和事件數(shù)據(jù)的存取與上報(bào)功能的實(shí)現(xiàn)流程。如果對(duì)本文有問(wèn)題,請(qǐng)留言評(píng)論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容