https://mp.weixin.qq.com/s/u-HmmrSAgtER1N2pKxCm0A
隨著公司業務的發展,數據的重要性越來越突出。大中型公司甚至一些小型互聯網公司,都建立了自己的數據采集、上報和分析平臺。而數據的采集是整個流程非常重要的一個環節,只有保證數據的采集的全面和精準,后面的分析才有意義。為了解決數據的正確性、維護難度和開發效率問題上,很多公司都提出了自己的技術方案。這些埋點方案大體可以分為三類:
代碼埋點
由開發人員在觸發事件的具體方法里,植入多行代碼把需要的數據存下來,然后根據上報策略把前一個時間段收集的數據上傳到后臺。
可視化埋點
通過可視化工具圈選具體頁面元素并生成配置,在用戶操作時,自動根據配置判斷是否需要采集該事件。
無埋點
無埋點并不是不需要埋點,而是在應用頁面的加載、點擊等事件前自動嵌入監測代碼來采集數據,它會采集所有感興趣的事件類型的埋點。其實我們更愿意稱它為全埋點。
京東客戶端現在主要使用第一種方案,即代碼埋點。這種方案的好處是,用起來比較簡單,在收集個性化數據時也比較靈活。但是也有一些問題,比如:
**+ **新增埋點依賴App發版,影響數據收集時機。
**+ **App發版需要埋點工作完成,影響版本進度。
**+ **埋點代碼和業務代碼耦合在一起,增加代碼維護難度。
**+ **如果埋點錯誤只能更新版本解決(Apple在2017年初全面禁止使用HotFix來修復bug)。
為了解決這些問題,我們調研了市面上的方案,在調研過程中,我們發現很多公司都看到了這些問題,他們也提出了自己的解決方案,基本上都是圍繞可視化埋點方案來做的。這種方案好處是,埋點提報方式和數據后臺基本不需要修改,風險也比較可控。而無埋點方案由于全部數據都收集,造成數據量巨大,這給服務器和網絡傳輸帶來較大負載,另外數據清洗難度也非常大,基于這些原因,大部分公司都沒有選擇這種方案?;谶@些這些原因和結合我們的場景后,我們選擇了可視化埋點方案來解決代碼埋點的問題。
可視化埋點并不是擯棄了代碼埋點,而是在代碼埋點的上層封裝的一套邏輯來代替手工埋點,大體上架構如下圖:
不過要實現可視化埋點也有很多問題需要解決,比如如何確定頁面元素的唯一標識、如何攜帶業務參數、如何添加有判斷邏輯的埋點和配置信息的版本管理。下面我們會整體介紹可視化埋點的使用方式和技術細節,另外針對上面的問題我們會嘗試給出解決方案和一些思考。
整體概覽
整體概覽的介紹分為2個部分:產品原型概覽和技術原理概覽。首先介紹產品原型概覽,可以更直觀了解可視化埋點的基本運作流程。
產品原型概覽
首先在App中嵌入可視化埋點SDK。當開啟圈選開關之后,會在屏幕上始終懸浮一個圈選開關,用于埋點維護人員采集埋點配置信息。如下圖所示:
圈選開關的按鈕一共有3個,當選擇圈選按鈕的時候,點擊頁面上的元素,SDK會攔截點擊事件,彈出一個用于收集配置信息的視圖。檢測按鈕用于版本管理,下文再詳細介紹。關閉按鈕用于關閉圈選功能,可以正常的操作App頁面元素。
開啟圈選開關,選擇頁面元素進行埋點配置采集。例如,點擊上圖所示頁面右下方的加入購物車按鈕,彈出配置視圖,如下圖所示:
視圖會展示一些信息,其中最重要的是SDK生成的唯一標識符,用于對埋點進行標識。埋點維護人員需要填寫eventId,選擇一些要上報的數據字段等操作。上圖左上角的增加按鈕,用于一個點擊事件有多個埋點的需求,點擊增加,會在下方新增一個信息采集視圖,以供埋點采集。上報類型是跟我們具體業務相關的,可以忽略。頁面參數、事件參數的選擇,會在下文說明攜帶上報數據部分的思路介紹。總之,采集完畢會形成一條配置信息,上傳到服務器。采集完成全部的配置信息,形成一個埋點配置列表。
在用戶啟動App時,埋點配置列表會被下載下來。當用戶點擊加入購物車按鈕時,SDK會使用和上文中配置采集階段相同的方法,生成唯一標識符,用于在埋點配置列表查找相關配置項,如果匹配成功,則利用這些配置數據,自動的進行埋點上報。
整體的產品原型概覽先介紹到這,下面看一下技術原理概覽。
技術原理概覽
采用AOP(Aspect-Oriented-Programming)即面向切面編程的思想,基于 Runtime 的 Method Swizzling能力,來 hook 相應的方法,從而在hook方法中進行統一的埋點處理。例如所有的按鈕被點擊時,都會觸發UIApplication的sendAction方法,我們hook這個方法,即可攔截所有按鈕的點擊事件。
這里的處理分為2個部分:采集埋點配置信息,和真實的埋點數據上報。這個和上文產品原型概覽部分介紹的處理流程相對應。
以按鈕點擊事件的處理為例,大致的流程如下圖所示:
這里僅僅是以按鈕為例說明,UITableView、UICollectionView、UIView的手勢等等,都是同樣的處理邏輯,對可視化埋點有過研究的人應該都了解這個過程,這里不再過多闡述。下面來詳細的探討SDK的關鍵模塊的技術實現思路。
關鍵模塊實現思路
我們要討論的SDK的關鍵模塊分為3個部分:生成唯一標識符、埋點數據攜帶、版本管理。其余部分,例如hook的具體實現、數據的上傳、下載匹配、圈選工具的交互等,雖然也都是需要解決一些技術問題,但是都有比較清晰的實現方案,這些方面不作討論。下面來看第一個問題。
唯一標識符
市面上可視化埋點方案,大多都使用viewPath生成唯一標識符。我們知道App的視圖層次是一個樹狀結構。一個 view可以被認為是一個節點,處于視圖樹的某一個位置,從根節點到這個view節點的深度信息構成了一個path,用來唯一標識該view。
如下圖所示,
**^ **view1的viewPath形如:0-0,
^ view2的viewPath形如:0-1,
^ view3的viewPath形如:0-1-0,
**^ **view4的viewPath形如:0-1-1。
這種方式有諸如可讀性、數據計算量、系統視圖干擾等一系列的麻煩要處理。除此之外,最關鍵的問題是這種方式僅僅適用于靜態視圖。還是拿上圖舉例,假如某一時刻,view1被移除,那么view2的viewPath變成了0-0,它的子視圖的viewPath也相應發生變化,這種情況下,viewPath無法用來唯一標識某個視圖,唯一標識符就不再唯一了。盡管也有相應的優化措施,例如在viewPath中引入className,但是這種方式只是很輕微的緩解了問題。在強調頁面配置化的場景,整個頁面的元素的位置、順序、是否展示,幾乎都要依靠服務端下發,引入className的優化恐怕并沒有明顯效果,而且增加了復雜度。所以這種方案還需要很大程度的提升和優化才行。
位置信息是可變的,所以viewPath這種方式是從可變的要素來生成唯一標識符,我們并沒有在研究viewPath上花費太多時間,而是換一種角度思考,引入相對不變的要素來生成唯一標識符:target+action。
獲取target+action的方式非常簡單高效,可以直接獲取一個UIButton的target和action,UIView可以通過UIRecognizeGesture獲取target和action、UITableview的delegate和didSelectedRowAtIndexPath等等??梢园l現,無論一個view顯示在任何位置,它的target和action都不會變化(除非某一個特殊情況下,功能發生變化,target和action才會變,不過顯然這個時候原始的埋點應該也及時廢棄或者添加新的埋點)。這樣去除了可變要素,利用不可變要素來生成唯一標識符,相對來說會更加可靠。
但是現實場景下,并不會總是一個按鈕對應著一個單一邏輯,在某種條件下進行區分埋點非常常見,例如在一個按鈕的處理事件中,可能會需要在condition1的情況下,需要執行A邏輯,然后埋A點,在condition2的情況下,執行B邏輯,埋B點。這時,無論使用viewPath還是target+action,都不能解決唯一標識問題。特別是condition多種多樣,增加了問題的復雜度,比如有些地方使用某一個對象的屬性或者服務端下發的字段作為條件判斷,有些地方使用視圖的狀態等信息作為條件判斷。從這里我們也能發現代碼埋點的優勢,你可以利用一切編程的靈活和便利性來達到目的,這恰恰成為了可視化埋點要面臨的困難。而使用target+action的方式還有一個麻煩需要處理,比如2個view的target+action是一樣的,但是要求不一樣的埋點。這兩種情況增加了生成唯一標識符的困難。本質上這兩種情況可以歸并為一種,多條件埋點或者有條件埋點。
我們的方案是增加一個protocol如下:
舉個例子:假如有一個按鈕,在condition1的情況下要執行doSom1這件事情,在condition2的情況下要執行doSom2這件事情
那么開發者要讓target在實現點擊事件的同時,還要實現上面的協議方法。
SDK會自動調用這個方法,把返回的標識追加到target+action的后面。protocol這種做法雖然解決了唯一標識問題,但是其實是把問題的復雜度拋出去,把區分condition的工作交給了開發者。所以這種方式也只是折衷的處理方案。
總是有一些方案,使用覆蓋率來衡量可視化埋點方案的適用情況。就是說,使用可視化埋點,可以替換百分之多少的代碼埋點。但是有條件埋點問題不解決的話,這個覆蓋率是沒有意義的。因為在埋點采集(圈選)階段,采集者可能壓根不知道這個按鈕是單一埋點還是帶條件的,如果采集錯誤的話,上報的埋點數據就會不準確。也有些人推薦多種組合的埋點方式,即代碼埋點、可視化埋點、無埋點等組合使用,這看起來是個很不錯的想法,不過在此之前,我們首先要解決明確邊界問題,讓多種方式協同工作。
埋點數據攜帶
一個具體的埋點上報內容,可能還要求攜帶一些頁面或者業務數據。埋點攜帶數據的問題,其實并不僅僅是可視化埋點需要面對,這是一個普遍的問題,跟埋點方式無關。還有前端埋點、后端埋點之爭,如果采取后端埋點,有些數據可能只能從前端獲取到。大家都覺得代碼埋點帶來代碼耦合,而且也比較繁瑣,但是采取無埋點的話,如何解決巨大的數據流量和后端數據清洗的問題??傊?,自動化埋點的技術探索還處于蠻荒階段,各家自成一體,沒有一個成熟的解決方案。
由于這么多的問題難以解決,現在市面上主要還是依賴代碼埋點的方式。代碼埋點繁瑣,需要跟業務邏輯緊緊耦合在一起;但是這種方式特別靈活,在應用開發中,一個復雜頁面可能包含許多模塊、視圖、業務處理類等,數據也可能從多個接口下發,分布在這些零散的地方。代碼埋點跟隨業務邏輯各自分布在這些零散模塊中,所以可以精準的獲取這些模塊中的數據,這就成為了代碼埋點的優勢。我們提出可視化埋點的解決方案,其實還是站在代碼埋點的視角看待問題,希望能用統一的方式解決代碼埋點的繁瑣、耦合問題,又不能犧牲代碼埋點的優勢和功能。
許多的可視化埋點方案,都是把埋點數據攜帶環節一筆帶過。僅僅指出用KVC等方式進行取值上報,并沒有實際的技術方案。其實這里存在非常多的疑點。譬如,數據存在什么地方?綁定在view上,還是在controller上,數據需要集中堆放到某個地方么。最基本的數據有服務端下發的字段和object本身的屬性,key是怎么規定的?KVC的方式是運行時特性,如果字段疏忽大意寫錯了,或者發生了變化但是key沒有及時更新,KVC的方式如何給出提示?
KVC要處理和面對如此多的問題,所以我們認為這種方式來存取數據并不合適。在上面,我們也引出了埋點數據上報方案的各種問題,總之由于各種各樣技術和現實問題的制約,各個公司可能都發展了自己的一套埋點方案,經過了很長時間的發展,各端都趨于穩定。如果修改數據上報方案,還要兼顧考慮各端需要改造的成本和風險,在徹底解決這些問題之前,我們提出的方案還是要基于當前的數據上報方案。
既然是基于我們當前的數據上報方案,還是先看看攜帶的埋點數據的一些特征:埋點攜帶的業務相關的數據主要分為3類:頁面參數、事件參數、擴展參數。頁面參數,顧名思義是跟頁面相關的數據,一般情況下,一個頁面下的所有埋點,頁面參數應該都是一致的,或者說就固定的幾種類型。事件參數類型非常多,一般都是跟具體的點擊事件的業務相關。而擴展參數,我們這邊是作為一個補充,用來上報一些額外的參數,例如商品詳情頁面,擴展參數可能會有店鋪id,商品的分類等等信息,一般也都是幾種固定的類型。
上報的這些數據,并不都是服務端下發什么,然后原樣傳回去,而是會經過客戶端的一系列處理。比如可能會寫一大堆邏輯,if某種業務,采集字段A,else某種業務,采集字段B;而且也不僅僅是條件判斷,許多的字段會抽象成0、1這樣的數字來表示,比如并不會直接使用字段A,而是用0來替代,B用1來替代等等,最后許多個這樣的字段會使用下劃線拼接起來,形如:0_1_1_0_1_0_0_0_1。這些數據上報到后臺,如果需要提數,會有專門的程序來解析這些數字拼接的字符串。
前面我們說過,不會改變數據上報方案,上面介紹的方式改造成本很高,我們還是用上述的方式來處理數據。但是需要提供一個統一的入口,以便于可視化埋點SDK可以訪問。這里,我們提供一個protocol,可以讓target或者是controller實現協議,這個方法返回一個字典,把之前處理數據的邏輯遷移到這里作為value,key用來標識數據。protocol如下所示:
target或者是controller實現協議,如下所示:
這種方式還便于采集配置,在圈選的時候,SDK會自動調用target和controller的這個方法,并且把所有的key值顯示出來,采集字段的時候,直接選擇某一個key即可,如下圖所示:
當選擇某一個key的時候,會同時增加一個source字段,記錄下這個key是來自于target還是controller。這些信息都作為配置信息被采集。當用戶在使用App,進行真實的埋點上報的時候,會根據source決定調用target還是controller的方法,同樣返回的字典,使用key來獲取對應的數據即可。
版本管理
生成控件的唯一標識符是可視化埋點的一個重要環節,無論是viewPath也好,還是target+action的方式,標識符都會包含一些跟控件本身相關的信息。假如采用的是target+action的方式:在1.0版本,有一個按鈕Button,它的處理方法是actionA,在采集埋點配置信息的時候,生成的唯一標識符是target+actionA。如果在2.0版本,他的處理方法被修改為actionB。如果拿著這個2.0版本的target+actionB去1.0版本采集過的配置表中進行查找,就會找不到對應的配置項。那么在2.0版本中,是否還需要重復采集在1.0版本采集過的埋點信息?如果重復采集的話,這可意味著可能會有非常大的工作量,一個大型App,可能全部的埋點個數有幾千個不等,每一個版本都把之前采集過的埋點在重新采集一次的話,工作量非常的可怕,也沒有必要。
還有就是,如果在2.0版本,干脆刪除了這個按鈕,那么這個按鈕的埋點自然也就不再需要了。但是和這個按鈕相關的埋點配置卻不能自動從配置表中刪除。長此以往,配置表中會冗余越來越多的無效埋點配置項,增加配置管理的成本,無論對于網絡還是系統的性能都是一個越來越嚴重的問題。
一個很有趣的現象是,目前市面上的可視化埋點方案,大多數沒有提到版本管理。其實版本管理,是一個必須要面對的問題。我們必須要能在版本遷移的時候,指出哪些埋點是繼續有效的,而哪些埋點已經失效了,以供采集人員及時的更新處理。一開始我們設想,通過代碼來模擬點擊頁面的所有元素,觸發了元素的處理事件,一定會走到自動埋點上報的邏輯。設置一個標識,當在版本檢測過程中時,查找到配置項之后,不再進行上報邏輯,而是把該配置項標識為有效。代碼來模擬點擊頁面的元素,需要通過調用UIControl的sendAction,或者是直接調用target的action方法等。這種方式理論上可行,但是特別麻煩,要處理的問題也特別多。
其實有一個很取巧的方式,不需要代碼模擬點擊事件。循環遍歷頁面的所有元素,直接利用SDK生成這些元素的唯一標識符,然后用唯一標識符去配置列表中查找,查找到配置項之后該配置項標識為有效。這種方案非常簡單輕巧,但是也有一些問題要處理,比如有些視圖是在controller下面,有些視圖在window下面,還有有一些視圖是延遲加載的,比如點擊了某個按鈕,然后頁面中增加一些新的元素。針對這兩個問題,我們通過設計版本檢測的交互方式來解決。在上文的SDK整體概覽的產品原型概覽章節,我們提到了一個檢測按鈕。當選擇檢測功能的時候,點擊頁面的某一個元素,SDK會向上尋找這個元素處于的根視圖。然后從這個根視圖出發,遞歸遍歷這個根視圖的所有子視圖。這樣無論視圖是在controller,window,navigationBar下面,只要點擊這些地方,都可以被檢測到。對于延遲加載的視圖,可以先關閉檢測按鈕,操作app把相關視圖加載出來之后,在用同樣的方式來進行檢測。
檢測完畢后,會彈出一個版本管理的視圖。按照所有埋點、有效埋點、無效埋點三種類型列舉出所有的配置項。然后針對無效埋點進行確認和相關處理即可。
總結與展望
我們的可視化埋點探索的技術方案先介紹到這里。對可視化埋點進行過深入研究之后,會發現上面介紹的這些問題處理起來比較困難,上面僅僅是介紹我們的方案和思考。除了支持App發版后新增埋點的能力,我們特別希望通過這種方式得到埋點效率提升,把開發者從體力活中解放出來。從當下來看,如果數據攜帶依然需要一系列邏輯處理的話,想通過自動化埋點的方式獲得效率提升還是比較有限的。同時,把從前代碼實現的埋點,替換成可視化圈選的方式,雖然解決了埋點代碼和業務邏輯耦合的問題,但似乎像是一種人力成本的遷移,畢竟圈選采集信息還是依賴于人工處理。
計算機本身的目的之一就是解決一些重復的繁瑣的事務。當下代碼埋點的成本很高,不僅僅是開發者,許多方面都要投入大量的時間和精力維護。所以我們相信自動化埋點這個需求,會驅動更多人持續不斷地研究,不斷地提出新的思路和解決方案,最后有一天實現真正高效的自動化埋點。