1. 程式人生 > >Android自動化埋點的實踐

Android自動化埋點的實踐

前幾天在app里加上了按鈕點選事件的自動埋點功能,這個功能的實現在面試中問過很多次,得到的答案都不盡如人意,歸根到底是沒有理解“自動”這個需求,自己也思考過一些方案,但是一直沒有一個比較靠譜的實現方式,直到看了這篇文章,才豁然開朗。

思路是基於Android的事件傳遞機制,當手指觸控到螢幕時,當前的activity就接收到了一個按下的事件,這個事件通常會被activity傳遞給自己的子view,否則使用者就點不了螢幕上的按鈕了。因此可以給應用中所有的activity提供一個基類,在基類中對手指按下事件做統一的統計,這樣就解決了在單個頁面上埋點的問題。

既然要統計view的點選事件,那麼首先要找到使用者點選的是哪個view。但是各個頁面的佈局結構千差萬別,怎麼去定位這個target呢?如果你瞭解activity頁面結構的話,這個問題就不是問題了。Activity有著跟html類似的佈局結構,都有一個根佈局,然後在根佈局上再新增各種各樣的view。在Activity中這個根就是DecorView,可以通過activity.getWindow().getDecorView()獲得這個物件。簡單來說,decorview裡面包含兩個子view,一個是titlelayout一個是contentlayout,一般來說titlelayout我們都直接隱藏的,因為會用到自己的titlebar,所以Activity裡顯示的內容就是contentlayout的內容,也就是setContentView()方法設定的layout,通過遍歷我們可以得到這個頁面上的所有view,然後通過view.getLocationOnScreen()可以得到這個view的大小和在螢幕上的位置,點選事件的位置可以通過event.getRawX()和event.getRawY()獲得,這樣我們就可以知道event是落在哪個view上了。

  public boolean eventInView(View view, MotionEvent event) {
        if (view.getVisibility() == View.INVISIBLE || view.getVisibility() == View.GONE) {
            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();
        if (clickX > x && clickX < (x + width) &&
                clickY > y && clickY < (y + height)) {
            return true;
        }
        return false;
    }

然而,事情並沒有那麼簡單。頁面佈局通常會進行巢狀,一個button可能是巢狀在一個父layout裡,這樣使用上面的方法判斷下來的話,就有兩個view獲取到這個event了,實際情況可能是三個或者更多,這當然是個錯誤。怎麼解決呢?思考一下我們是怎麼來做佈局的吧,如果現在有一個relativelayout,裡面放了一個button,我們只給button註冊點選事件,那relativelayout就不應該被統計到,此時只有button有onclick事件,relativelayout是沒有的。如果反過來,點選relativelayout有事件,而點選button沒有的話,就是relativelayout有onclick事件而button沒有。因此,在得到event落在哪些view裡以後,需要進行一個判斷,哪個view有onclick事件,就認為哪個view實際被點選了。基於android的事件傳遞機制,我們應該從外到裡從上到下進行遍歷,只要外層view有點選事件,我們就可以結束遍歷了。

怎麼知道一個view是否有點選事件呢,android並沒有提供類似於view.getOnClickListener()的api,這個問題只能通過反射來解決,具體實現依sdk版本不同而不同。實現如下:

 public View.OnClickListener getOnClickListener(View view) {
        if (view == null) {
            return null;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            return getOnClickListenerV14(view);
        } else {
            return getOnClickListenerV(view);
        }
    }

    //Used for APIs lower than ICS (API 14)
    private View.OnClickListener getOnClickListenerV(View view) {
        View.OnClickListener retrievedListener = null;
        String viewStr = "android.view.View";
        Field field;

        try {
            field = Class.forName(viewStr).getDeclaredField("mOnClickListener");
            retrievedListener = (View.OnClickListener) field.get(view);
        } catch (NoSuchFieldException ex) {
            Log.e("Reflection", "No Such Field.");
        } catch (IllegalAccessException ex) {
            Log.e("Reflection", "Illegal Access.");
        } catch (ClassNotFoundException ex) {
            Log.e("Reflection", "Class Not Found.");
        }

        return retrievedListener;
    }

    //Used for new ListenerInfo class structure used beginning with API 14 (ICS)
    private View.OnClickListener getOnClickListenerV14(View view) {
        View.OnClickListener retrievedListener = null;
        String viewStr = "android.view.View";
        String lInfoStr = "android.view.View$ListenerInfo";

        try {
            Field listenerField = Class.forName(viewStr).getDeclaredField("mListenerInfo");
            Object listenerInfo = null;

            if (listenerField != null) {
                listenerField.setAccessible(true);
                listenerInfo = listenerField.get(view);
            }

            Field clickListenerField = Class.forName(lInfoStr).getDeclaredField("mOnClickListener");

            if (clickListenerField != null && listenerInfo != null) {
                retrievedListener = (View.OnClickListener) clickListenerField.get(listenerInfo);
            }
        } catch (NoSuchFieldException ex) {
            Log.e("Reflection", "No Such Field.");
        } catch (IllegalAccessException ex) {
            Log.e("Reflection", "Illegal Access.");
        } catch (ClassNotFoundException ex) {
            Log.e("Reflection", "Class Not Found.");
        }

        return retrievedListener;
    }
好了,思路基本上就是這樣,最後一個問題,我們應該記些啥內容?既然是自動埋點,肯定只能記一些通用的資訊,有特殊需求的還是要手動去加。因此,我只記錄了當前頁面的類名和當前view的id:

Log.d(TAG,this.getClass().getSimpleName() + "-" + getResources().getResourceEntryName(view.getId()));