Android 可視化埋點方案

背景

目前統計打點已是一個產品常見的需求,尤爲在業務模式探索的前期,埋點功能更是必不可少的功能,下面將介紹最簡單的app全埋點方案!前端

什麼是數據埋點

數據埋點是通常項目採用統計UV,PV,Action,Time等一系列的數據信息,對特定用戶行爲或事件進行捕獲、處理和發送的相關技術及其實施過程。android

爲何要數據埋點

產品或運營分析人員,基於埋點數據分析須要,對用戶行爲的每個事件進行埋點佈置,並經過SDK上報埋點的數據結果,進行分析,並進一步優化產品或指導運營。
數據埋點包括哪些
這裏有我以前寫的一篇文章App優質精準的用戶行爲統計和日誌打撈方案
地址:blog.csdn.net/sk719887916…git

數據埋點採集模式

自動埋點
App經過代理,調用Sdk相關API,進行的將數據埋點上報的模式.
無痕埋點
無需經過專門提供代理類,直接由sdk提供相關接口,或者經過編譯工具,預編譯替換代碼等,直接由sdk所有負責採集上報
可視化埋點
可視化埋點指 前端或者app端基於dom 元素和控件所精準自動埋點的上報的方案。github

對比分析:

自動埋點:
缺點:
1 開發人員工做量大,需對業務提供惟一的ID,來區分每個業務,不管是否提供sdk代理,業務開發人員至少須要屢次調用sdk相關API.
2  業務人員和產品溝通成本提升,須要對具體業務制定相關的業務標識,以便於產品分析和統計
優勢:
產品運營工做量少,對照業務映射表,就能分析出還原相關業務場景, 數據比較精細,無需大量的加工和處理。數據庫


無痕埋點
缺點:
1 sdk開發人員需提供一套無痕埋點技術成品,包括能正確獲取PV,UV,ACtion,TIme等多項統計指標。前期技術投入大。
2 數據量大,需後端落地進行大量處理,並由產品進行自我還原業務員場景。 不管採用智能系統平臺,仍是經過原生的數據庫查詢數據,都是一種大量的分析精力。
優勢:
1 開發人員工做量小,無需對業務標識進行惟一區分,由sdk自動進行生成,ID規則由sdk和產品進行約定。減小業務人員的溝通成本和使用步驟。
2 數據量全面,覆蓋面廣,產品可按需進行分析。作到毫無遺漏。
3 支持動態頁面和局部動效的統計。編程


可視化埋點
優勢:
1 相對數據量而言
相比較於無埋點相而言對較低,可是這個可視化元素的識別技術是客戶端或者前端所要實現的,惟一id生成也無需客戶端去自定義規則,這套生成規則由相關產品在自動化工具的狀況下生成配置表,下發到客戶端,再由客戶端按坑就班到相關界面去實現。
2 數據量相對精確
缺點:
1 可視化工具的平臺的搭建,靜態頁面的元素識別都須要額外開發。
2 動態效果可能會遺漏。json

實現方案:

埋點需求可參考我以前的文章:
App優質精準的用戶行爲統計和日誌打撈方案:https://blog.csdn.net/sk719887916/article/details/50931485
App打造自定義的統計SDK:
https://www.jianshu.com/p/cd83e81b78aa
自動埋點實際上也是,提供一個base類,由業務類繼承base類,在base裏面作相關統計api調用,
可參考個人github:github.com/Tamicer/Sky…後端

打點要素

android端打點的四個要素api


1 view展示,點擊,消失
AccessibilityDelegate.
AccessibilityDelegate的使用(API level 14)
AccessibilityDelegate主要用來對view作一個檢測,包括view的點擊,選中,滑動,touch,文本變化及描述等等,能夠用來作一些數據統計或者分析
建立一個自定義的AccessibilityDelegate,實現sendAccessibilityEvent(View host, int eventType)方法,而後經過view.setAccessibilityDelegate(),當該view的相關屬性出現變化時,就會回調到實現的sendAccessibilityEvent方法中,咱們能夠經過AccessibilityEvent.type_xxx來區分eventType是什麼類型,而後作不一樣的處理
給View設置AccessibilityDelegate,而當View 產生了click,long_click 等事件的時候.會在響應原有的Listener方法後.發送消息給
AccessibilityDelegate.而後在sendAccessibilityEvent方法下作打點操做.服務器


2 頁面監聽
ActivityLifecycleCallbacks
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {}
在onResume中對activity.getWindow().getDecorView().getRootView()進行向下遍歷


3 定位View
View 的惟一性.
根據context的className+View自身的 Id的String 值.(String 值能夠反射R文件獲得)+View 的className值肯定.
當前沒有id.向上須要父類的id,若是父類id 沒有則記錄父類className+getChildAt()值而且繼續往上找,直到找到有id的view或者沒有父類爲止


4 上報時機
啓動上報 輪詢上報,網絡變化,先後臺切換,主動上報,推送回撈

核心實現:

以Android做爲列子:
提供自動遍歷元素 並能撲捉點擊的控件的activity, 並能在生命週期統計pv的打開和關閉,調用我開源的SkyMonitoring的對應的api.
複寫dispatchTouchEvent(MotionEvent ev) 事件函數,肯定被點擊的view的相關位置,並生成惟一的ID,企業級app都是從服務器下發對應的ID,對應頁面去調用埋點sdk Api,實現事件行爲TcStatInterface.initEvent(path.viewTree);。
這個path就是view的路徑,頁面的深度路徑,包括打開和關閉sdk在SkyMonitoring中已能自動獲取。
本次demo是id生成規則是按照 :包名+ Activity+ Viewgroup+ Layout+ view + View index + viewID實現的。
業務直接去繼承TamicActivity便可,就能去實現全部可視化view的埋點功能。
代碼以下:

 
 

   public abstract class TamicActivity extends AppCompatActivity {


    private int statusBarHeight;
    View rootView;
    String rootViewTree;
    String bigDataPrefix;
    String bigDataIngorePrefix;
    String bigDataEventPrefix;
    private String TAG  = "LYK";

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        //獲取到根節點的view
        rootView = getWindow().getDecorView();
        //控件在視圖樹上的根路徑
        rootViewTree = getPackageName() + "." + getClass().getSimpleName();
        //前綴名 bigData
        bigDataPrefix = "Tamic_test";
        //前綴名 bigData_
        bigDataIngorePrefix = bigDataPrefix + "";
        //前綴名 bigdata_ignore
        bigDataEventPrefix =  bigDataIngorePrefix +"Igmore";
    }

    @Override
    protected void onResume() {
        super.onResume();

        TcStatInterface.recordPageStart(TamicActivity.this);
    }

    @Override
    protected void onPause() {
        super.onPause();

        TcStatInterface.recordPageEnd();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // APP退出
        TcStatInterface.recordAppEnd();

    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if(ev.getAction() == MotionEvent.ACTION_DOWN){
            ViewPath path = findClickView(ev);
            if(path != null) {
                Log.e(TAG, "path -->" + path.viewTree);
                TcStatInterface.initEvent(path.viewTree);
            }
        }
        return super.dispatchTouchEvent(ev);
    }

    private ViewPath findClickView(MotionEvent ev) {
        Log.e(TAG, "bigdata-->findClickView");
        ViewPath clickView = new ViewPath(rootView, rootViewTree);
        return searchClickView(clickView, ev, 0);
    }


    private ViewPath searchClickView(ViewPath myView, MotionEvent event, int index) {
        ViewPath clickView = null;
        View view = myView.view;
        if (isInView(view, event)) {   
            myView.level++;
            if (myView.level == 2 && !"LinearLayout".equals(view.getClass().getSimpleName())) {
                myView.filterLevelCount++;
            }
            if (myView.level > myView.filterLevelCount) {
                myView.viewTree = myView.viewTree + "." + view.getClass().getSimpleName() + "[" + index + "]";
            }
            Log.i(TAG, "bigdata-->tag = " + view.getTag());
            if (view.getTag() != null) {
                // 主動標記不須要統計時,不進行自動統計
                String tag = view.getTag().toString();
                if (tag.startsWith(bigDataIngorePrefix)) {
                    return null;
                } else if (tag.startsWith(bigDataPrefix)) {
                    if (tag.startsWith(bigDataEventPrefix)) {
                        myView.specifyTag = tag.replace(bigDataEventPrefix, "");
                    }
                    return myView;
                }
            }
            if (view instanceof ViewGroup) {   
                if (view instanceof AbsListView) {
                    Log.i(TAG, "bigdata-->AbsListView ");
                    return null;
                }
                ViewGroup group = (ViewGroup) view;
                int childCount = group.getChildCount();
                if (childCount == 0) {
                    return myView;
                }
                for (int i = childCount - 1; i >= 0; i--) {
                    myView.view = group.getChildAt(i);
                    clickView = searchClickView(myView, event, i);
                    if (clickView != null) {
                        return clickView;
                    }
                }
            } else {
                clickView = myView;
            }
        }
        return clickView;
    }

    private boolean isInView(View view, MotionEvent event) {
        if (view == null || view.getVisibility() != View.VISIBLE) {
            return false;
        }
        int clickX = (int) event.getRawX();
        int clickY = (int) event.getRawY();
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        int x = location[0];
        int y = location[1];
        int width = view.getWidth();
        int height = view.getHeight();
        return clickX > x && clickX < (x + width) && clickY > y && clickY < (y + height);
    }
   }

App項目集成使用,初始化url和相關統計配置字典,這個字典能夠從服務器下發下來,我本次只是經過簡單的本地文件作實踐。

 
 

public class StatAppliation extends Application {

@Override
public void onCreate() {
    super.onCreate();
    // you app id
    int appId = 21212;
    // assets
    String fileName = "my_statconfig.json";
    String url = "https://github.com/Tamicer/TamicAppMonitoring";
    // init statSdk
    TcStatInterface.initialize(this, appId, "you app chanel", fileName);
    TcStatInterface.setUrl(url);
    TcStatInterface.setUploadPolicy(TcStatInterface.UploadPolicy.UPLOAD_POLICY_DEVELOPMENT, TcStatInterface.UPLOAD_TIME_ONE);
 }
}

可視化也能夠經過aop插樁實現,可是實現起來對代碼的***性過高,這裏不作介紹。
aop插樁對碎片化fragment支持比較好。對這塊的介紹可看我之前在公衆號推送的一篇文章:AOP編程之AspectJ實戰實現數據無痕埋點

可參考:
https://www.baidu.com/link?url=FniQOFyj1pd6O5Fz6viRMN3ZgexIKAk7SQ08EgpBU9cHHMszPlm2jRXJ21mkomtY&wd=&eqid=ffc87acf0005fd18000000045a5d98dd

項目地址:

https://github.com/Tamicer/TamicAppMonitoring

相關文章
相關標籤/搜索