Android-自動化埋點

原文:http://www.imwillsun.com/pages/2014/01/10/auto-monitor-on-android.html

當我們開發一款Android應用上線後,希望能收集一些用戶操作的行爲數據,比如用戶在某個頁面點擊了多少次,在某個控件被點擊了多少次,在某個頁面停留了多少時間等。這些數據收集起來可以交給數據分析師,他們可以統計出應用的PV或UV;或者統計應用中哪些頁面最受歡迎,哪些控件點擊率最低,從而來改進應用。對於控件被點擊多少次,一般做法是在控件點擊事件中加入幾行log代碼,然後將此次的點擊記錄下來,最終發送到服務端,頁面的點擊也是類似,需要在頁面生命週期的開始加入log代碼。這種插入log代碼記錄操作行爲的方式定義爲埋點。但麻煩的是,如果業務邏輯複雜,頁面衆多,控件衆多,那就要在許多地方插入這些log代碼。這是一件多麼重複的事情呀!

那有沒有可能自動化去埋點呢?就是將界面的打開、關閉以及控件點擊的log記錄放到統一的地方去處理,而不用在許多業務邏輯中加入log代碼。這塊統一的監控代碼需要做到如下的事情:

1.可以監控到界面打開或者關閉,並將這種操作記錄到log中
2.當界面上的有控件被點擊的時候,可以監控到哪個界面哪個控件被點擊了,並將這些操作信息記錄到log中
3.要能實現埋點的定製,即對需要埋點的控件或者界面才記錄它們的操作log

下面分析一下自動化埋點的思路。
自動化埋點的思路

首先對Android的Activity和UI做個基本的瞭解,然後再提供一些自動化埋點的思路。

1.Activity的生命週期

學習Android,Activity的生命週期是必修課。Activity的生命週期分爲onCreate,onStart,onResume,onPause,onStop和onDestroy,一個界面的展示和消失都會要經過這幾個階段,所以如果監控了Activity的生命週期,就可以監控一個界面的打開、關閉以及用戶在界面上停留的時間。實現這種監控方式,可以通過創造一個界面基類,讓所有業務界面去繼承它,然後基類中重寫所有Activity的生命週期方法,見如下代碼。至於BaseActivity重寫方法裏做什麼樣的攔截處理,這個會在下面說到。

public BaseActivity extends Activity{
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//這裏對Activity的生命週期進行攔截,其他的方法也是
}
}
public BusinessActivity extends Activity{
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); //一定要調用super的方法
//這裏寫一些業務代碼

}
}

2.Android的UI佈局

Activity中的UI佈局是層層嵌套的,很類似HTML的佈局。在一個Activity中,“根”view是PhoneWindow$DecorView,可通過this.getWindow().getDecorView()獲取到這個對象。通過實例說明一下,在一個空白的Activity界面中添加一個Button按鈕,下圖是利用hierarchyviewer截到的UI佈局圖。

圖中的DecorView類似於HTML中的標籤,裏面嵌套的第一層Linearlayout類似於標籤。Linearlayout中有兩個子view,都是FrameLayout佈局,它們類似於

標籤。第一個FrameLayout是應用的titlebar,TextView是titlebar上的文字;第二個FrameLayout中的內容就是Activity中的佈局。可以看到最外層的採用RelativeLayout佈局,裏面有個Button按鈕。
先做下簡單瞭解,下面會說到它跟自動化埋點的關係。

3.Android事件傳遞機制

Activity中的UI佈局是層層嵌套的,如果點擊一個界面上的控件,點擊事件的傳遞是由父視圖向子視圖傳遞,然後再傳到具體的控件中,這個跟HTML中的點擊事件冒泡一樣。我們簡單看一下Android的事件傳遞機制是怎麼實現的。

在View中關於事件響應的方法有兩個:

public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)

而ViewGroup除了這個方法還有一個方法:

public boolean onInterceptTouchEvent(MotionEvent event)

這些方法的返回值都是布爾型,返回的true或false決定了一個事件的響應能不能向下傳遞。我們來看一下這幾個方法的作用。

dispatchTouchEvent方法用於事件的分發,在Activity界面中所有的點擊事件都要經過這個方法的分發。返回true表示不分發事件,由當前的View來消費事件響應;返回false則繼續往下分發,如果是ViewGroup則分發給onInterceptTouchEvent進行判斷是否攔截該事件。
onTouchEvent方法用於事件的處理,返回true表示消費處理當前事件,返回false則不處理,交給子控件進行繼續分發。這個方法主要在普通控件中,比如Button。
onInterceptTouchEvent是ViewGroup中才有的方法,View中沒有,它的作用是負責事件的攔截,返回true的時候表示攔截當前事件,不繼續往下分發,交給自身的onTouchEvent進行處理。返回false則不攔截,繼續往下傳。這是ViewGroup特有的方法,因爲ViewGroup中可能還有子View,而在Android中View中是不能再包含子View的。

除了上述的事件,Android提供了一個OnTouchListener的監聽器,當事件傳遞到控件的時候,如果控件註冊了這個監聽器,則會執行監聽器中的onTouch方法。同時,如果它返回true,則事件也是不繼續向下傳遞了。

public boolean onTouch(View v, MotionEvent event)

上述的事件傳遞可以通過舉一個例子說明,假設一個界面上有一個Button按鈕,當我們touch down這個Button的時候,DOWN事件的傳遞如下:

Activity->dispatchTouchEvent
Button->dispatchTouchEvent
Button->onTouch
Button->onTouchEvent

這裏的每一步返回false,事件就不會向下傳遞。當我們touch up這個Button的時候,UP事件的傳遞如下:

Activity->dispatchTouchEvent
Button->dispatchTouchEvent
Button->onTouch
Button->onTouchEvent
Button->click

可以看到,一個Button的click事件要經過上面幾個過程。如果要監聽一個Button的click事件,有一種思路是我們可以創建一個基類BaseButton繼承自Button,在回調OnClickListener的地方加入攔截代碼。但是麻煩的是,點擊控件不一定是Button,可能是其他TextView或者Layout之類的,Android中控件很多,我們要造很多控件基類,這樣應用中充滿的控件都必須是我們自己創建的控件,這樣的設計是相當龐雜的。

那麼我們考慮另外一種思路:讓創建的BaseActivity基類重寫Activity的dispatchTouchEvent方法,當touch button時,可以獲取到按下(DOWN)和擡起(UP)時產生的MotionEvent對象。這個MotionEvent對象有兩個方法,getRawX()和getRawY(),通過這兩個方法我們可以獲取到“點擊位置”在界面中的座標。同時,上文中提到,Activity的UI是層層嵌套的,通過“根”view可以層層遍歷其下的子view以及所有子View上的控件,這些View和控件在屏幕中的座標和寬高我們是可以獲取到的。好了,這樣就可以搜索所有的子View或者控件的佈局區域是否包含“點擊位置”,從而來判斷哪個View或控件被點擊。具體判斷可以通過如下代碼實現。

public boolean isInView(View view,MotionEvent event){
int clickX = event.getRawX();
int clickY = event.getRawY();
//如下的view表示Activity中的子View或者控件
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; //這個條件成立,則判斷這個view被點擊了
}
return false;
}

自動化埋點的實現

綜上我們可以整理一下自動化埋點的思路。對於自動化埋點第一個功能,可以通過創建基類BaseActivity重寫Activity的所有的生命週期。對於自動化埋點的第二個功能,實現方式是,通過重寫Activity的dispatchTouchEvent方法,點擊事件發生時,通過MotionEvent對象獲取點擊位置座標,然後遍歷Activity界面中所有的View(控件也都是View),判斷哪個View區域包含點擊位置,從而判斷哪個View被點擊了。另外有個問題,當攔截到這些操作信息,如何將它放到一個統一的地方去處理呢?可以採用廣播的方式,將相關數據發送出去,然後在一個BroadcastReceiver中統一處理埋點的log生成。看如下代碼:

public BaseActivity extends Activity{
//其他的Activity生命週期重寫類似
protected void onStart() {
super.onStart();
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
Intent intent = new Intent(ACTIVITY_START);
intent.putExtra(ACTIVITY_START, event);
broadcastManager.sendBroadcast(intent);
}
protected boolean dispatchTouchEvent(MotionEvent ev) {
if (event.getAction() == MotionEvent.ACTION_UP) {
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
Intent intent = new Intent(VIEW_CLICK);
intent.putExtra(VIEW_CLICK, event);
broadcastManager.sendBroadcast(intent);
}
}
}

public class AutoMonitorReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(action == VIEW_CLICK){
MotionEvent event = intent.getParcelableExtra(VIEW_CLICK);
//1.遞歸遍歷Activity(就是Context)中的所有View,找出被點擊的View
View clickView = searchClickView(view, event);
//2.生成log記錄下來
writeLog();
}else if(action == ACTIVITY_START){
//可以知道某個界面被打開了,然後記錄此次操作行爲
writeLog();
}
}
private View searchClickView(View view, MotionEvent event) {
View clickView = null;
if (isInView(view, event) &&
view.getVisibility() == View.VISIBLE) { //這裏一定要判斷View是可見的
if (view instanceof ViewGroup) { //遇到一些Layout之類的ViewGroup,繼續遍歷它下面的子View
ViewGroup group = (ViewGroup) view;
for (int i = group.getChildCount() - 1; i >= 0; i–) {
View chilView = group.getChildAt(i);
clickView = searchClickView(chilView, event);
if (clickView != null) {
return clickView;
}
}
}
clickView = view;
}
return clickView;
}
}

又一個問題,代碼中的writeLog方法到底要記錄哪些數據作爲log信息呢?log信息中最重要的是能讓開發者看出來哪個界面被打開或者哪個控件被點擊。對於界面,可以記錄其類名;對於控件,一般沒有確定的名稱,那麼可以記錄下來這個控件在界面中的路徑。比如上文中介紹Android UI佈局的實例,如果要定位記錄那個Button,則可以記錄它所在界面的類名和Button的佈局路徑作爲它的標識。那個Button的路徑可以表示爲DecorView>LinearLayout[0]>FrameLayout[1]>RelativeLayout[0]>Button[0],由於LinearLayout有多個子View,因此可以在子View中加入編號來區分。這樣就解決了log信息的記錄問題,log信息的格式大致要有如下幾個字段:

monitor_type | ui_name | view_ui_path

比如是控件點擊埋點log,則可以記錄爲

VIEW_CLICK | MainActivity | DecorView>LinearLayout[0]>FrameLayout[1]>RelativeLayout[0]>Button[0]

比如是MainActivity界面打開的埋點log,則可以記錄爲

ACTIVITY_START | MainActivity | NULL

這樣我們分析這些日誌信息,就可以統計出一個應用中各個頁面被打開過多少次,某個界面的控件被點擊過多少次。

對於自動化埋點的第三個功能,實現埋點的定製,這個比較好實現。如果我們需要對那些界面或者控件進行埋點,我們可以定製一個埋點列表。這個列表在應用啓動的時候被下載到用戶的手機上,然後AutoMonitorReceiver需要多做一點事,就是將廣播發送過來的埋點信息與埋點列表進行比對,看是不是需要埋的點,如果是,就將其記錄;如果不是,就不做處理。假設我們用json存儲這個埋點列表,大致的結構如下:

{
“version”: “app_1.1.1”,
“view_id”:”MainActivity”: {
“DecorView>LinearLayout[0]>FrameLayout[1]>RelativeLayout[0]>Button[0]”:”button_0”
“DecorView>LinearLayout[0]>FrameLayout[1]>RelativeLayout[0]>Button[1]”:”button_1”
}
}

當然這個埋點列表如何生成,這個需要開發者自己寫代碼去處理。
自動化埋點存在的問題和難點

本文提供的自動化埋點的思路是,通過攔截屏幕點擊事件來搜索被點擊的控件,然後將其界面類名和控件的UI路徑記錄下來作爲log信息。將控件的UI路徑和所在的界面類名作爲一個控件的標識,有時候會出現一些問題,比如:

1.Android版本不同會造成控件的UI路徑不同。比如Android2.2與Android4.1版本下,獲取到上文中Button的UI路徑分別爲

//android2.2
DecorView>FrameLayout[1]>RelativeLayout[0]>Button[0]
//android4.1
DecorView>LinearLayout[0]>FrameLayout[1]>RelativeLayout[0]>Button[0]

這個問題可以通過將控件的UI路徑縮短來解決,比如就只用FrameLayout[1]>RelativeLayout[0]>Button[0]路徑來標識Button控件。

2.對一些隱藏控件、彈出窗口或者浮動窗口不好處理。比如,在上文中的Button同樣位置存在另外一個Button,不過是隱藏的,有時出現,有時不出現。當一個Button被點擊,單純依靠DecorView>FrameLayout[1]>RelativeLayout[0]>Button[0]路徑不能判斷出是哪個Button被點擊。解決這個問題,有時做一些特殊處理可以解決,比如擴展log的字段,多加入一些控件的信息,比如Button上的文字等。但是有時,開發應用的控件佈局千變萬化,一些控件確實不能通過UI路徑進行唯一標識,這種就沒法自動化埋點了。只能通過手動埋點來來補充了。

3.文中提到爲了實現埋點的定製,需要開發者自己寫代碼生成一個埋點列表,這個也是比較麻煩的。要遍歷一個應用中所有界面和界面中控件的UI路徑,這個比較容易,但是取出自己想要埋點的控件UI路徑,這個可能需要人工去查看比對。另外,一些大型應用開發的時候,界面隨時發生着變化,一些控件的佈局在隨時發生變化。每次發佈應用的時候,都需要掃描一下應用控件信息,以及重新找一下埋點控件的UI路徑,這個是相當麻煩的。如何實現這部分的自動化,也是一個難題。
參考文章

InfoQ: Android事件傳遞機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章