Android-自動化埋點
當我們開發一款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事件傳遞機制