背景
很多時候,因?yàn)楫a(chǎn)品都會要獲取用戶的行為,需要客戶端進(jìn)行相關(guān)的埋點(diǎn)凭需。埋點(diǎn)主要分為兩種:
- 侵入式埋點(diǎn)
在每個需要埋點(diǎn)的地方手動添加代碼
- 優(yōu)點(diǎn):埋點(diǎn)準(zhǔn)確,可以精確描述不同組件之間的關(guān)聯(lián)
- 缺點(diǎn):代碼耦合度高肝匆,后期難以維護(hù)粒蜈,不需要的埋點(diǎn)需要手動刪除
- 無痕埋點(diǎn)
通過全局監(jiān)聽或AOP技術(shù)給所有的View添加埋點(diǎn)
- 優(yōu)點(diǎn):代碼耦合度低,靈活度高术唬,不同項(xiàng)目可復(fù)用
- 缺點(diǎn):沒有侵入式埋點(diǎn)精準(zhǔn)薪伏,無法描述兩個組件之間的關(guān)聯(lián)
面試一句話
無痕埋點(diǎn)就是一種通過全局監(jiān)聽或者AOP技術(shù)省去手動埋點(diǎn)的技術(shù),它和代碼耦合度低粗仓,靈活度高嫁怀,適用于組件間關(guān)聯(lián)性不強(qiáng)的業(yè)務(wù)埋點(diǎn)
Code
完整代碼可見 -- 代碼路徑
技術(shù)點(diǎn)
無痕埋點(diǎn)的技術(shù)路徑包括View操作的攔截,上報(bào)信息的設(shè)置與埋入借浊,上報(bào)
- 點(diǎn)擊監(jiān)聽
- 滑動監(jiān)聽
- 埋點(diǎn)信息
- ASpect監(jiān)聽
點(diǎn)擊監(jiān)聽
實(shí)現(xiàn)方法
通過給View設(shè)置setAccessibilityDelegate來獲取sendAccessibilityEvent回調(diào)監(jiān)聽View的點(diǎn)擊事件
原理
- 當(dāng)View被點(diǎn)擊時候塘淑,最后都會走到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為非空的,就會觸發(fā)sendAccessibilityEvent()回調(diào)
public void sendAccessibilityEvent(int eventType) { if (mAccessibilityDelegate != null) { mAccessibilityDelegate.sendAccessibilityEvent(this, eventType); } else { sendAccessibilityEventInternal(eventType); } }
- 所以我們只需要給View設(shè)置AccessibilityDelegate蚂斤,就能對點(diǎn)擊事件進(jìn)行監(jiān)聽,host代表被監(jiān)聽的View存捺,eventType代表事件類型,比如點(diǎn)擊、輸入
view.setAccessibilityDelegate(new View.AccessibilityDelegate(){ @Override public void sendAccessibilityEvent(View host, int eventType) { super.sendAccessibilityEvent(host, eventType); Log.d(TAG, "sendAccessibilityEvent: "); } });
- 對于層級很深的ViewGroup捌治,我們可以通過遍歷的方式岗钩,對他層級內(nèi)的View和ViewGroup添加監(jiān)聽,為了避免動態(tài)添加的View沒有添加監(jiān)聽肖油,我們需要給ViewGroup添加childView的監(jiān)聽
/** * 設(shè)置Activity頁面中View的事件監(jiān)聽 * * @param activity */ public void setTracker(Activity activity) { // 找到根路徑的View View contentView = activity.findViewById(android.R.id.content); if (contentView != null) { setViewTracker(contentView, null); } } /** * 設(shè)置Fragment頁面中View的事件監(jiān)聽 * * @param fragment */ public void setTracker(Fragment fragment) { View contentView = fragment.getView(); if (contentView != null) { setViewTracker(contentView, fragment); } } /** * 設(shè)置View上的事件監(jiān)聽 * * @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; } /** * 對每個View添加埋點(diǎn)的監(jiān)聽 * * @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); } } }