版權(quán)歸屬于微信公眾號(hào)文章網(wǎng)易HubbleData之Android無(wú)埋點(diǎn)實(shí)踐
文末有彩蛋哦?
1 背景
網(wǎng)易HubbleData是一個(gè)洞察用戶行為的數(shù)據(jù)分析系統(tǒng),提供一套完整的數(shù)據(jù)解決方案。一個(gè)典型的數(shù)據(jù)平臺(tái),對(duì)于數(shù)據(jù)的處理,是由如下的5個(gè)步驟組成的:
其中,第一個(gè)步驟,也即數(shù)據(jù)采集是最核心的問(wèn)題。網(wǎng)易HubbleData支持全端數(shù)據(jù)采集,包括iOS、Android、JS、JAVA等多個(gè)平臺(tái)。本文主要討論Android平臺(tái)的數(shù)據(jù)采集方案。業(yè)內(nèi)各家公司從不同角度,提出了多種技術(shù)方案,這些方案大體上可以歸為三類(lèi):
(1) 代碼埋點(diǎn):在某個(gè)事件發(fā)生時(shí)調(diào)用SDK里面相應(yīng)的接口發(fā)送埋點(diǎn)數(shù)據(jù),百度統(tǒng)計(jì)、友盟、TalkingData、Sensors Analytics等第三方數(shù)據(jù)統(tǒng)計(jì)服務(wù)商大都采用這種方案。
- 優(yōu)點(diǎn):使用者控制精準(zhǔn),自由地選擇什么時(shí)候發(fā)送數(shù)據(jù);使用者控制精準(zhǔn),自由地選擇什么時(shí)候發(fā)送數(shù)據(jù)。
- 缺點(diǎn):開(kāi)發(fā)及測(cè)試代價(jià)大;需要等待APP更新。
(2) 可視化埋點(diǎn):通過(guò)可視化工具配置采集節(jié)點(diǎn),在Android端自動(dòng)解析配置并上報(bào)埋點(diǎn)數(shù)據(jù),從而實(shí)現(xiàn)所謂的自動(dòng)埋點(diǎn),代表方案是已經(jīng)開(kāi)源的Mixpanel。
- 優(yōu)點(diǎn):解放開(kāi)發(fā)人員,解決了代碼埋點(diǎn)代價(jià)大的問(wèn)題;通過(guò)服務(wù)端配置埋點(diǎn),解決等待APP更新的問(wèn)題。
- 缺點(diǎn):覆蓋功能有限,只能配置一些公共屬性;埋點(diǎn)只能從當(dāng)前時(shí)刻開(kāi)始,無(wú)法“回溯”。
(3) 無(wú)埋點(diǎn):它并不是真正的不需要埋點(diǎn),而是Android端自動(dòng)采集全部事件并上報(bào)埋點(diǎn)數(shù)據(jù),在后端數(shù)據(jù)計(jì)算時(shí)過(guò)濾出有用數(shù)據(jù),代表方案是國(guó)內(nèi)的GrowingIO。
- 優(yōu)點(diǎn):解放開(kāi)發(fā)人員,解決了代碼埋點(diǎn)代價(jià)大的問(wèn)題;解決了等待APP更新和數(shù)據(jù)“回溯”的問(wèn)題;可以自動(dòng)獲取很多啟發(fā)性的信息。
- 缺點(diǎn):覆蓋的功能有限,不能靈活地自定義屬性;給網(wǎng)絡(luò)傳輸和耗電等性能帶來(lái)更大的負(fù)載。
網(wǎng)易HubbleData的Android SDK早已有之,公司內(nèi)部諸如考拉、易信、LOFTER、美學(xué)、漫畫(huà)等多款產(chǎn)品都已接入使用。原有Android SDK采用手動(dòng)代碼埋點(diǎn)的方案,主要關(guān)注的是事件模型、埋點(diǎn)接口、上報(bào)策略等問(wèn)題。整體架構(gòu)如下圖所示:
代碼埋點(diǎn)雖然使用起來(lái)靈活,但是開(kāi)發(fā)成本較高,并且一旦上線就很難修改。參考業(yè)界先進(jìn)方案并結(jié)合網(wǎng)易公司內(nèi)部產(chǎn)品的埋點(diǎn)需求,網(wǎng)易HubbleData的Android SDK在代碼埋點(diǎn)整體架構(gòu)的基礎(chǔ)上新增了無(wú)埋點(diǎn)功能,本文主要針對(duì)網(wǎng)易HubbleData在Android SDK中無(wú)埋點(diǎn)實(shí)踐進(jìn)行簡(jiǎn)單分享。
2 無(wú)埋點(diǎn)關(guān)鍵技術(shù)
2.1 View的唯一ID
2.1.1 如何唯一地標(biāo)識(shí)一個(gè)View?
SDK內(nèi)部在自動(dòng)收集控件數(shù)據(jù)時(shí),需要將界面上的任何一個(gè)View與其他View區(qū)分開(kāi)來(lái)。這就需要為界面上的每一個(gè)控件分配一個(gè)唯一的ViewID。此ViewID除了具有區(qū)分性,還需要具有一致性,即同一個(gè)View無(wú)論界面布局如何動(dòng)態(tài)變化,或者說(shuō)多次進(jìn)入同一頁(yè)面,此ViewID理論上保持不變。
View中可以找到的特征信息:
Id: 靜態(tài)整數(shù)。在編譯期,aapt會(huì)生成R類(lèi),其中包含所有資源ID。
Resource Id:開(kāi)發(fā)者操作控件的唯一標(biāo)識(shí)。一般由開(kāi)發(fā)者在布局文件中指定android:id,通過(guò)findViewById找到View。
Class Name:View所屬的Class,例如TextView、LinearLayout、ListView、ViewPager等。
這些特征信息中的Id如果能夠使用,是可以直接用作ViewID的,但是,從aapt生成id的原則來(lái)看,不同版本相同的resource Id對(duì)應(yīng)的整數(shù)Id 是有可能不一樣的,所以沒(méi)有辦法使用Id來(lái)唯一標(biāo)識(shí)。
Resource Id是開(kāi)發(fā)者定義的View標(biāo)識(shí),對(duì)于有Resource Id 的View可以說(shuō)具備了唯一標(biāo)識(shí),那么沒(méi)有Resource Id的View,我們考慮通過(guò)一個(gè)index屬性來(lái)區(qū)分,index屬性可以取每個(gè)控件所屬父組件的index(也即每個(gè)控件是其父控件的第幾個(gè)孩子),并逐級(jí)向上遍歷找到根節(jié)點(diǎn),最后形成一個(gè)View Path即可用來(lái)唯一地標(biāo)識(shí)這個(gè)View。
2.1.2 ViewID構(gòu)造
通過(guò)上述分析,我們得到一條View Path:獲取每個(gè)控件自身的ID、類(lèi)名、Resource Id以及位于所屬父組件的Index等特征信息,并逐級(jí)向上遍歷找到根節(jié)點(diǎn)。
并結(jié)合該View所在的頁(yè)面信息,我們得到ViewID的構(gòu)造形式如下:
sha-256(page : path)
- page: ActivityName
- path: view在控件樹(shù)中的全路徑,按照如下形式進(jìn)行拼接,其中index為當(dāng)前view所屬父組件的index,id為編寫(xiě)布局文件時(shí)的android:id屬性值,有則拼接,且index固定為0,無(wú)則不拼接。
parent1[index]#id/parent2[index]#id/.../view[index]#id
簡(jiǎn)單示例如下:
2.1.3 ViewID優(yōu)化
考慮到在實(shí)際布局中有可能存在一些動(dòng)態(tài)插入、刪除的控件,或者說(shuō)控件被復(fù)用,都可能引起View Path的變化,從而導(dǎo)致ViewID不唯一。為了保證ViewID的一致性,我們從以下幾個(gè)方面著手,對(duì)ViewID進(jìn)行了一定程度地優(yōu)化。
(1) Index
如上圖所示,當(dāng)頁(yè)面布局發(fā)生動(dòng)態(tài)變化時(shí),比如說(shuō)刪除一個(gè)子view,其他子view所屬父組件的index也可能會(huì)改變,為此,我們對(duì)view所屬父組件的index進(jìn)行改造,通過(guò)如下算法對(duì)index賦值:
每個(gè)ViewGroup下的所有View作為一個(gè)數(shù)組,從0開(kāi)始;
每個(gè)ViewGroup下的所有View先按照Class分類(lèi),然后再把每個(gè)類(lèi)型中的數(shù)據(jù)按照數(shù)組的方式,從0開(kāi)始;
每個(gè)ViewGroup下的所有View先按照Class分類(lèi),再確認(rèn)是否有Resource Id,如果存在,則index為0,否則index為所屬Class類(lèi)型數(shù)組下的序號(hào)。
該優(yōu)化處理對(duì)所有View適用。優(yōu)化后效果如下:即動(dòng)態(tài)改變一些控件后,只會(huì)影響同類(lèi)型的控件,其他類(lèi)型控件的index不受影響,也即ViewID不受影響。
(2) 可復(fù)用View
先來(lái)看一個(gè)應(yīng)用場(chǎng)景:
如圖所示,當(dāng)ListView上滑時(shí),屏幕下方即將顯示的<元素6>其實(shí)復(fù)用了屏幕上方即將滑出的<元素0>,也就是說(shuō)<元素6>與<元素0>的index均為0,在這種情況下,我們無(wú)法通過(guò)前述index的定義來(lái)區(qū)分這兩個(gè)列表Item。
所幸,針對(duì)這種情況,我們可以用position的取值進(jìn)行區(qū)分,也就是令index = position。
通過(guò)實(shí)踐發(fā)現(xiàn),發(fā)生上述復(fù)用情形的View主要有以下幾類(lèi):AdapterView、RecyclerView和ViewPager,其api都提供了獲取position的接口。
a. AdapterView
AdapterView的派生類(lèi)均可通過(guò)getPositionForView獲取position。
index = position = ((AdapterView) group).getPositionForView(child);
作為AdapterView的派生類(lèi)之一,ExpandableListView因?yàn)樯婕暗絞roupPosition和childPosition,因此需要特殊處理。在構(gòu)造ViewID時(shí),將能夠采集到的position信息都添加到View Path中,具體策略如下:
先將ExpandableListView作為普通AdapterView計(jì)算position
列表Item為header元素,View Path中添加[header:position]
-
列表Item為footer元素,footer的index需要額外計(jì)算,計(jì)算公式如下,View Path中添加[footer:footerIndex]
// Calculates the footer index among footers; // For instance, there are five footers, so the footer index ranges from zero to four. // The first footer index is zero. footerIndex = position - (expandableListView.getCount() - expandableListView.getFooterViewsCount()); 列表Item為組元素,View Path中添加[group:groupPosition]
列表Item為組內(nèi)元素,View Path中添加[group:groupPosition,child:childPosition]
涉及到的api接口如下:
((AdapterView) expandableListView).getPositionForView();
public long getExpandableListPosition(int flatListPosition);
public static int getPackedPositionType(long packedPosition);
public static int getPackedPositionGroup(long packedPosition);
public static int getPackedPositionChild(long packedPosition);
示例如下:
b. V7-RecyclerView
RecyclerView的情形比較簡(jiǎn)單,可通過(guò)調(diào)用getChildPosition和getChildAdapterPosition獲取position。
@Deprecated
public int getChildPosition(View child);
public int getChildAdapterPosition(View child);
c. V4 - ViewPager
V4 - ViewPager可通過(guò)調(diào)用getCurrentItem獲取position。
public int getCurrentItem();
(3) Fragment節(jié)點(diǎn)
主流App的主頁(yè)均是采用如圖所示的Tab切換Fragment的設(shè)計(jì)。在這種情形下,如果主頁(yè)內(nèi)嵌的Fragment采用“懶加載”方案,則底部Tab的點(diǎn)擊順序決定了該Tab對(duì)應(yīng)Fragment的初始化順序,從而導(dǎo)致Fragment所屬父組件的index動(dòng)態(tài)變化。
也就是說(shuō),F(xiàn)ragment初始化順序影響ViewID。而前述Index優(yōu)化方案并不能解決這一問(wèn)題。
Fragment節(jié)點(diǎn)特殊處理
針對(duì)Fragment初始化順序影響ViewID的問(wèn)題,我們采用的解決方案是:
如果能夠獲取到Fragment實(shí)例的類(lèi)名,則使用Fragment實(shí)例的類(lèi)名替換View Path中的Fragment,并設(shè)置[index]為特殊標(biāo)記[-]。例如:使用控件篇Tab對(duì)應(yīng)的Fragment實(shí)例ControlSetFragment以及特殊標(biāo)記[-]替換原View Path中的Fragment[3]
如何獲取Fragment實(shí)例?
采用代碼埋點(diǎn)或后續(xù)即將講到的插件埋點(diǎn),在Fragment各實(shí)例類(lèi)中重載下面的幾個(gè)方法,并在各方法中插入SDK提供的方法調(diào)用,從而實(shí)現(xiàn)Fragment生命周期監(jiān)聽(tīng):
@Override
public void onResume() {
super.onResume();
DATracker.getInstance().onFragmentResume(this);
}
@Override
public void onPause() {
super.onPause();
DATracker.getInstance().onFragmentPause(this);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
DATracker.getInstance().setFragmentUserVisibleHint(this, isVisibleToUser);
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
DATracker.getInstance().onFragmentHiddenChanged(this, hidden);
}
通過(guò)上述調(diào)用,當(dāng)Fragment生命周期變化時(shí),SDK能夠記錄當(dāng)前活躍的所有Fragment。當(dāng)某個(gè)活躍的Fragment上的控件被點(diǎn)擊了,SDK構(gòu)造該控件的ViewID時(shí),會(huì)自動(dòng)將該Fragment實(shí)例的類(lèi)名寫(xiě)入View Path。
V4 - ViewPager內(nèi)嵌Fragment
這里要說(shuō)明的是,ViewPager內(nèi)嵌的View不僅是可復(fù)用的,同時(shí),由于其“懶加載”、“預(yù)加載”機(jī)制,其內(nèi)嵌View的加載順序也是動(dòng)態(tài)的。特別地,當(dāng)ViewPager內(nèi)嵌Fragment時(shí),按照前述對(duì)Fragment節(jié)點(diǎn)的處理,我們會(huì)使用Fragment實(shí)例的類(lèi)名替換View Path中的Fragment,并設(shè)置[index]為特殊標(biāo)記[-]。之所以將[index]設(shè)置為特殊標(biāo)記[-],是因?yàn)镕ragment動(dòng)態(tài)加載導(dǎo)致index不可靠,而ViewPager中內(nèi)嵌的Fragment卻可以調(diào)用ViewPager的getCurrentItem拿到position作為index,這種情況下,是可以將index的值添加到View Path中的。
2.2 無(wú)埋點(diǎn)實(shí)現(xiàn)
通過(guò)前述方案,我們可以使用ViewID唯一地標(biāo)識(shí)屏幕上的控件。那么,比如一個(gè)Button,當(dāng)這個(gè)Button被點(diǎn)擊了,SDK又是如何捕捉到這一點(diǎn)擊事件,并且拿到Button實(shí)例的呢,也就是如何實(shí)現(xiàn)自動(dòng)埋點(diǎn)的呢?這里,我們提供了兩種方案。
2.2.1 代理監(jiān)聽(tīng)
原理
在應(yīng)用程序中,輔助功能事件是用戶與可視界面組件交互的消息。這些消息是由輔助功能服務(wù)處理。輔助功能服務(wù)使用在這些事件中的信息產(chǎn)生附加的反饋和提示。Android 4.0(API14)及更高版本上,輔助功能方法屬于View類(lèi)的一部分,也是View.AccessibilityDelegate的一部分。其中可用于實(shí)現(xiàn)無(wú)埋點(diǎn)的方法如下:
sendAccessibilityEvent()
當(dāng)用戶在一個(gè)視圖上操作時(shí)調(diào)用此方法。事件按照用戶操作類(lèi)型分類(lèi),涵蓋以下事件類(lèi)型:
- TYPE_VIEW_CLICKED
- TYPE_VIEW_LONG_CLICKED
- TYPE_VIEW_FOCUSED
- TYPE_VIEW_SELECTED
- TYPE_VIEW_HOVER_ENTER
- TYPE_VIEW_SCROLLED
- TYPE_VIEW_TEXT_CHANGED
- ...
采用輔助功能事件實(shí)現(xiàn)無(wú)埋點(diǎn),簡(jiǎn)單來(lái)講,就是給View設(shè)置AccessibilityDelegate,當(dāng)View產(chǎn)生了click,long_click等事件時(shí),會(huì)在響應(yīng)原有的Listener方法后,發(fā)送消息給AccessibilityDelegate,然后在sendAccessibilityEvent方法下搜集自動(dòng)埋點(diǎn)事件。
private class TrackingAccessibilityDelegate extends View.AccessibilityDelegate {
public TrackingAccessibilityDelegate(ViewNode viewNode, View.AccessibilityDelegate realDelegate) {
mViewNode = viewNode;
mRealDelegate = realDelegate;
}
public View.AccessibilityDelegate getRealDelegate() {
return mRealDelegate;
}
@Override
public void sendAccessibilityEvent(View host, int eventType) {
if (eventType == mEventType && host == mViewNode.getView()) {
...
// 自動(dòng)埋點(diǎn)
fireEvent(mViewNode, type);// sends tracking data
}
// 響應(yīng)原AccessibilityDelegate
if (null != mRealDelegate) {
mRealDelegate.sendAccessibilityEvent(host, eventType);
}
}
private View.AccessibilityDelegate mRealDelegate;
private ViewNode mViewNode;
}
設(shè)置代理的時(shí)機(jī)
實(shí)現(xiàn)Application.ActivityLifecycleCallbacks,用來(lái)監(jiān)聽(tīng)Activity生命周期,當(dāng)監(jiān)聽(tīng)到某個(gè)Activity進(jìn)入onResumed狀態(tài)時(shí),通過(guò)以下方式獲取RootView:
mViewRoot = this.mActivity.getWindow().getDecorView().getRootView()
從RootView出發(fā)深度優(yōu)先遍歷控件樹(shù),為滿足特定條件的View設(shè)置代理監(jiān)聽(tīng)。
界面動(dòng)態(tài)變化怎么辦?
實(shí)現(xiàn)ViewTreeObserver.OnGlobalLayoutListener,用來(lái)監(jiān)聽(tīng)界面變化。當(dāng)監(jiān)聽(tīng)到界面變化時(shí),重新遍歷控件樹(shù),為滿足特定條件的View設(shè)置代理監(jiān)聽(tīng),已經(jīng)設(shè)置過(guò)代理的View不再重復(fù)設(shè)置。
界面的監(jiān)測(cè)操作需要放在界面主線程中,起初我們擔(dān)心這樣會(huì)對(duì)應(yīng)用本身的界面交互產(chǎn)生影響,所幸,經(jīng)過(guò)實(shí)際測(cè)試,這樣實(shí)現(xiàn)是可行的,界面交互感知不到任何影響。
監(jiān)控哪些View?
-
AutoCompleteTextView(搜索框)
添加 TextWatcher 監(jiān)聽(tīng)文本變化,2s 后延時(shí)發(fā)送文本輸入結(jié)果
-
AbsListView(列表)
OnItemClickListener 存在 - 對(duì)原有OnItemClickListener作一層包裝,在響應(yīng)原有的Listener方法后,搜集自動(dòng)埋點(diǎn)事件。
-
一般View
hasOnClickListeners 或 isClickable 返回 true - 設(shè)置AccessibilityDelegate
2.2.2 gradle插件
原理
試想一下我們代碼埋點(diǎn)的過(guò)程:首先定位到事件響應(yīng)函數(shù),例如Button的onClick函數(shù),然后在該事件響應(yīng)函數(shù)中調(diào)用SDK數(shù)據(jù)搜集接口。下面,我們介紹使用gradle插件自動(dòng)在目標(biāo)響應(yīng)函數(shù)中插入SDK數(shù)據(jù)搜集代碼,達(dá)到自動(dòng)埋點(diǎn)的目的。
我們的gradle插件采用 Android gradle 插件提供的最新的Transform API,在Apk編譯環(huán)節(jié)中、class打包成dex之前,插入了中間環(huán)節(jié),調(diào)用 ASM API對(duì)class文件的字節(jié)碼進(jìn)行掃描,當(dāng)掃描到目標(biāo)事件響應(yīng)函數(shù)時(shí),在函數(shù)頭部或尾部插入SDK數(shù)據(jù)搜集代碼。
監(jiān)控哪些View?
我們?cè)谀繕?biāo)View的事件響應(yīng)函數(shù)中插入SDK數(shù)據(jù)搜集代碼,即可實(shí)現(xiàn)對(duì)該類(lèi)型View的監(jiān)控。例如,在Button的點(diǎn)擊事件響應(yīng)函數(shù)onClick中插入SDK數(shù)據(jù)搜集代碼后,當(dāng)Button被點(diǎn)擊,便會(huì)執(zhí)行到onClick中的SDK數(shù)據(jù)搜集代碼,從而實(shí)現(xiàn)Button點(diǎn)擊事件的自動(dòng)搜集。
目標(biāo)事件響應(yīng)函數(shù)(方法):
- onClick(Landroid/view/View;)V
- onClick(Landroid/content/DialogInterface;I)V
- onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V
- onItemSelected(Landroid/widget/AdapterView;Landroid/view/View;IJ)V
- onGroupClick(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z
- onChildClick(Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)Z
- onRatingChanged(Landroid/widget/RatingBar;FZ)V
- onStopTrackingTouch(Landroid/widget/SeekBar;)V
- onCheckedChanged(Landroid/widget/CompoundButton;Z)V
- onCheckedChanged(Landroid/widget/RadioGroup;I)V
- ...
具體實(shí)現(xiàn):
- 對(duì)app中指定包進(jìn)行掃描,篩選出實(shí)現(xiàn)了目標(biāo)接口的類(lèi),在目標(biāo)方法中添加數(shù)據(jù)采集代碼。
例如,篩選出實(shí)現(xiàn)了
android/view/View$OnClickListener接口的類(lèi),然后在onClick(Landroid/view/View;)V方法中注入采集數(shù)據(jù)的代碼。
目標(biāo)效果:
public class MainActivity extends AppCompatActivity implements OnClickListener,
android.content.DialogInterface.OnClickListener,
OnItemClickListener,
OnItemSelectedListener,
OnRatingBarChangeListener,
OnSeekBarChangeListener,
OnCheckedChangeListener,
android.widget.RadioGroup.OnCheckedChangeListener,
OnGroupClickListener, OnChildClickListener {
public void onClick(View var1) {
PluginAgent.onClick(var1);
}
public void onClick(DialogInterface var1, int var2) {
PluginAgent.onClick(this, var1, var2);
}
public void onItemClick(AdapterView<?> var1, View var2, int var3, long var4) {
PluginAgent.onItemClick(this, var1, var2, var3, var4);
}
...
}
Fragment生命周期追蹤
在ViewID優(yōu)化中,我們講到Fragment節(jié)點(diǎn)的優(yōu)化時(shí),提到可通過(guò)重寫(xiě)Fragment的幾個(gè)與生命周期相關(guān)的函數(shù)監(jiān)聽(tīng)Fragment生命周期。這個(gè)過(guò)程除了使用代碼埋點(diǎn),也可借助插件自動(dòng)完成:掃描class文件,定位Fragment的幾個(gè)與生命周期相關(guān)的函數(shù),自動(dòng)插入代碼。
目標(biāo)函數(shù)(方法):
- onResume()V
- onPause()V
- setUserVisibleHint(Z)V
- onHiddenChanged(Z)V
具體實(shí)現(xiàn):
-
對(duì)app中指定包進(jìn)行掃描,篩選出所有父類(lèi)為下列其中之一的子類(lèi)。以下是Fragment及系統(tǒng)內(nèi)置的幾個(gè)常見(jiàn)的Fragment派生類(lèi)。
android/app/Fragment android/app/DialogFragment android/app/ListFragment android/support/v4/app/Fragment android/support/v4/app/DialogFragment android/support/v4/app/ListFragment 對(duì)這些Fragment子類(lèi)的
onResumed,onPaused,onHiddenChanged,setFragmentUserVisibleHint方法的字節(jié)碼進(jìn)行修改,添加數(shù)據(jù)采集代碼。
目標(biāo)效果:
public class BaseFragment extends Fragment {
public BaseFragment() {
}
public void onResume() {
super.onResume();
PluginAgent.onFragmentResume(this);
}
public void onHiddenChanged(boolean var1) {
super.onHiddenChanged(var1);
PluginAgent.onFragmentHiddenChanged(this);
}
public void onPause() {
super.onPause();
PluginAgent.onFragmentPause(this);
}
public void setUserVisibleHint(boolean var1) {
super.setUserVisibleHint(var1);
PluginAgent.setFragmentUserVisibleHint(this, var1);
}
}
2.2.3 代理監(jiān)聽(tīng) vs gradle插件
插件埋點(diǎn)方案,發(fā)生在編譯期,當(dāng)目標(biāo)事件響應(yīng)函數(shù)被執(zhí)行時(shí),才會(huì)觸發(fā)我們插入的代碼主動(dòng)搜集事件。除了消耗一點(diǎn)編譯速度,應(yīng)用運(yùn)行期間基本不受影響。
代理監(jiān)聽(tīng)方案,由于事先并不清楚用戶會(huì)觸發(fā)哪些交互事件,所以需要為所有可交互的View設(shè)置代理,涉及到控件樹(shù)遍歷,因此性能略遜于gradle插件方案。但好在控件樹(shù)遍歷消耗的時(shí)間是毫秒級(jí)的,不會(huì)影響界面交互。
下面總結(jié)一下這兩種方案的優(yōu)缺點(diǎn)。
(1) 代理監(jiān)聽(tīng)方案
缺點(diǎn):
- 遍歷,被動(dòng)等待被觸發(fā)
- 攔截彈窗比較困難
- Fragment生命周期需手動(dòng)攔截
優(yōu)點(diǎn):
- 對(duì)于可點(diǎn)擊但又未設(shè)置點(diǎn)擊監(jiān)聽(tīng)器的View,可設(shè)置監(jiān)聽(tīng)器
(2) gradle插件方案
優(yōu)點(diǎn):
- 無(wú)需遍歷,主動(dòng)觸發(fā)事件
- 主動(dòng)攔截彈窗(待擴(kuò)展)
缺點(diǎn):
- 目前只支持Gradle1.5+構(gòu)建工具
3 總結(jié)與展望
以上就是網(wǎng)易HubbleData在Android端的無(wú)埋點(diǎn)實(shí)踐中總結(jié)的重點(diǎn)難點(diǎn)。還有一些邊邊角角的點(diǎn)就不一一細(xì)述了。
當(dāng)然,我們的無(wú)埋點(diǎn)方案也并不完美,還有一些未解決的問(wèn)題。例如,ViewID的構(gòu)造及優(yōu)化方案并不能適用于所有情況;通過(guò)無(wú)埋點(diǎn)搜集的數(shù)據(jù)也僅限控件的一些固有屬性,并沒(méi)有搜集到更有價(jià)值的業(yè)務(wù)數(shù)據(jù)...
網(wǎng)易HubbleData也將持續(xù)跟進(jìn)業(yè)界先進(jìn)埋點(diǎn)技術(shù),及時(shí)升級(jí)埋點(diǎn)方案。后續(xù)針對(duì)比較有意思的技術(shù)點(diǎn),也會(huì)繼續(xù)整理出來(lái)分享給大家。
如果對(duì)該項(xiàng)目感興趣,可以聯(lián)系 zhangdan_only@163.com ,歡迎一起研究。
預(yù)知更多,請(qǐng)猛戳??
用于Android客戶端無(wú)埋點(diǎn)數(shù)據(jù)采集的Gradle插件
網(wǎng)易HubbleData無(wú)埋點(diǎn)SDK在iOS端的設(shè)計(jì)與實(shí)現(xiàn)