背景
很多時(shí)候,因?yàn)楫a(chǎn)品都會(huì)要獲取用戶的行為,需要客戶端進(jìn)行相關(guān)的埋點(diǎn)。埋點(diǎn)主要分為兩種:
- 侵入式埋點(diǎn)
在每個(gè)需要埋點(diǎn)的地方手動(dòng)添加代碼
- 優(yōu)點(diǎn):埋點(diǎn)準(zhǔn)確,可以精確描述不同組件之間的關(guān)聯(lián)
- 缺點(diǎn):代碼耦合度高,后期難以維護(hù),不需要的埋點(diǎn)需要手動(dòng)刪除
- 無(wú)痕埋點(diǎn)
通過(guò)全局監(jiān)聽(tīng)或AOP技術(shù)給所有的View添加埋點(diǎn)
- 優(yōu)點(diǎn):代碼耦合度低,靈活度高,不同項(xiàng)目可復(fù)用
- 缺點(diǎn):沒(méi)有侵入式埋點(diǎn)精準(zhǔn),無(wú)法描述兩個(gè)組件之間的關(guān)聯(lián)
面試一句話
無(wú)痕埋點(diǎn)就是一種通過(guò)全局監(jiān)聽(tīng)或者AOP技術(shù)省去手動(dòng)埋點(diǎn)的技術(shù),它和代碼耦合度低,靈活度高,適用于組件間關(guān)聯(lián)性不強(qiáng)的業(yè)務(wù)埋點(diǎn)
Code
完整代碼可見(jiàn) -- 代碼路徑
技術(shù)點(diǎn)
無(wú)痕埋點(diǎn)的技術(shù)路徑包括View操作的攔截,上報(bào)信息的設(shè)置與埋入,上報(bào)
- 點(diǎn)擊監(jiān)聽(tīng)
- 滑動(dòng)監(jiān)聽(tīng)
- 埋點(diǎn)信息
- ASpect監(jiān)聽(tīng)
點(diǎn)擊監(jiān)聽(tīng)
實(shí)現(xiàn)方法
通過(guò)給View設(shè)置setAccessibilityDelegate來(lái)獲取sendAccessibilityEvent回調(diào)監(jiān)聽(tīng)View的點(diǎn)擊事件
原理
- 當(dāng)View被點(diǎn)擊時(shí)候,最后都會(huì)走到performClick()方法
public boolean performClick() { // We still need to call this method to handle the cases where performClick() was called // externally, instead of through performClickInternal() notifyAutofillManagerOnClick(); final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } // 關(guān)鍵方法 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; } - 再看sendAccessibilityEvent()方法,可以看到只要view的mAccessibilityDelegate為非空的,就會(huì)觸發(fā)sendAccessibilityEvent()回調(diào)
public void sendAccessibilityEvent(int eventType) { if (mAccessibilityDelegate != null) { mAccessibilityDelegate.sendAccessibilityEvent(this, eventType); } else { sendAccessibilityEventInternal(eventType); } } - 所以我們只需要給View設(shè)置AccessibilityDelegate,就能對(duì)點(diǎn)擊事件進(jìn)行監(jiān)聽(tīng),host代表被監(jiān)聽(tīng)的View,eventType代表事件類(lèi)型,比如點(diǎn)擊、輸入
view.setAccessibilityDelegate(new View.AccessibilityDelegate(){ @Override public void sendAccessibilityEvent(View host, int eventType) { super.sendAccessibilityEvent(host, eventType); Log.d(TAG, "sendAccessibilityEvent: "); } }); - 對(duì)于層級(jí)很深的ViewGroup,我們可以通過(guò)遍歷的方式,對(duì)他層級(jí)內(nèi)的View和ViewGroup添加監(jiān)聽(tīng),為了避免動(dòng)態(tài)添加的View沒(méi)有添加監(jiān)聽(tīng),我們需要給ViewGroup添加childView的監(jiān)聽(tīng)
/** * 設(shè)置Activity頁(yè)面中View的事件監(jiān)聽(tīng) * * @param activity */ public void setTracker(Activity activity) { // 找到根路徑的View View contentView = activity.findViewById(android.R.id.content); if (contentView != null) { setViewTracker(contentView, null); } } /** * 設(shè)置Fragment頁(yè)面中View的事件監(jiān)聽(tīng) * * @param fragment */ public void setTracker(Fragment fragment) { View contentView = fragment.getView(); if (contentView != null) { setViewTracker(contentView, fragment); } } /** * 設(shè)置View上的事件監(jiān)聽(tīng) * * @param view */ public void setTracker(View view) { if (view != null) { setViewTracker(view, null); } } /** * 判斷view是否需要埋點(diǎn),目前默認(rèn)只要可以點(diǎn)擊的都是true * * @param view * @return */ private boolean needTracker(View view) { return true; } /** * 對(duì)每個(gè)View添加埋點(diǎn)的監(jiān)聽(tīng) * * @param view * @param fragment */ private void setViewTracker(View view, Fragment fragment) { if (needTracker(view)) { if (fragment != null) { view.setTag(FRAGMENT_TAG_KEY, fragment); } view.setAccessibilityDelegate(this); } if (view instanceof ViewGroup) { int childCount = ((ViewGroup) view).getChildCount(); for (int i = 0; i < childCount; i++) { setViewTracker(((ViewGroup) view).getChildAt(i), fragment); } ((ViewGroup) view).setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { @Override public void onChildViewAdded(View parent, View child) { setTracker(parent); } @Override public void onChildViewRemoved(View parent, View child) { setTracker(parent); } }); if (needTracker(view)) { view.setAccessibilityDelegate(this); viewScrollListener.setScrollListener(view); } } }