網(wǎng)易HubbleData之Android無(wú)埋點(diǎn)實(shí)踐

版權(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)用getChildPositiongetChildAdapterPosition獲取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)

最后編輯于
?著作權(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)容