在我們展開討論之前,首先要明白什么是用戶行為記錄?
我的理解是 用戶行為記錄是針對使用我們軟件的用戶的信息統計,這些信息主要是 用戶的操作信息 以及 其他一些相關感興趣的日志信息,這些信息通常由用戶的操作而產生,經我們包裝處理后上傳到服務器,以供數據分析員使用分析。不要小看這些信息,分析信息可以讓我們了解軟件的用戶群,平台信息,以及用戶的操作習慣等,現在已經有越來越多的開發者和公司重試並利用這一塊。
ok,既然我們是討論解決方案的,閑話之后直入主題,怎么做?
1.用戶行為記錄信息的組成 一條完整有價值的用戶行為記錄應該包含哪些內容呢?無疑,用戶本身的點擊代表的操作是主要信息,比如用戶在支付頁面點擊了支付,這樣的一個行為能提取出“支付頁”和“支付”兩個有價值的信息,此外,產生這個操作的時間戳信息,用戶的機器信息(手機型號等),SDK版本號以及用戶的網絡類型信息等。
2.用戶行為記錄從哪來?到哪去? 每一條用戶行為記錄都是由用戶的操作產生的,這些信息最終會被上傳到服務端保存分析。我們要做的工作就是將每條有價值的用戶行為記錄信息保存並上傳至服務器。當然,出於應用耗電量以及程序效率的考慮,我們不可能在每次用戶產生操作記錄時,都發送服務將信息上傳到服務器。所以我們可以采用 緩存--文件--上傳服務器的策略。
明白了上面這些信息,解決方案就出來了。

1 /** 2 * 用戶行為記錄 3 * 4 * @author derson2388 5 * 6 */ 7 public class UserActionManager { 8 9 private static UserActionManager manager; 10 11 /** 12 * 工作線程消息隊列處理 13 */ 14 private Handler mHandler; 15 /** 16 * 日志緩存 17 */ 18 private ArrayList<String> mCache = new ArrayList<String>(); 19 20 /** 21 * 單例訪問,同步上鎖 22 * 23 * @return 24 */ 25 public synchronized UserActionManager getInstance() { 26 if (null == manager) { 27 manager = new UserActionManager(); 28 } 29 return manager; 30 } 31 32 private UserActionManager() { 33 new Thread(new Runnable() { 34 35 @Override 36 public void run() { 37 // 新啟動一個工作線程,啟動Looper后創建handler處理寫入請求 38 Looper.prepare(); 39 mHandler = new Handler(Looper.myLooper()); 40 Looper.loop(); 41 } 42 }).start(); 43 } 44 }
上面的代碼有幾點值得注意:
1.UserActionManager為全局程序提供了一個單例訪問的實例,緩存cache用於緩存用戶行為記錄,當緩存到達一定的閥值時,才把緩存里的日志寫入文件,當文件大小到達一定大小時,才將日志信息通過網絡發送到服務器。
2.構造函數里在新啟的工作線程里啟動了一個新的Looper並以此構造了一個專門處理該工作線程消息隊列的handler,這樣記錄用戶,寫入文件,上傳服務器等都可以通過這個handler分發/處理到工作線程的消息隊列MessageQueue里,避免了主線程ANR的風險。

1 /** 2 * 用戶行為記錄 3 * 4 * @param action 5 */ 6 public synchronized void writeUserCode(String action) { 7 // TODO 在這里對用戶行為信息做格式化組裝 8 mCache.add(action); 9 if (mCache.size() > MAXSIZE_CACHE) { 10 if (null != mHandler) { 11 mHandler.post(new Runnable() { 12 13 @Override 14 public void run() { 15 if (!isSending) { 16 // 寫日志到文件 17 pushToFile(); 18 // 發送日志到服務端 19 sendActionToServer(); 20 } 21 } 22 }); 23 } 24 } 25 }
在監聽到用戶操作的時候調用記錄方法,先將信息存入緩存,當緩存到達閥值時,清空緩存並寫入文件。

1 /** 2 * 寫入文件 3 */ 4 private void pushToFile() { 5 String logs = ""; 6 // 組裝信息 7 for (String str : mCache) { 8 logs += str + "\n"; 9 } 10 // 清空緩存日志 11 mCache.clear(); 12 File file = new File(userActionFilePath); 13 if (!file.exists()) { 14 try { 15 file.createNewFile(); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 BufferedWriter writer = null; 21 try { 22 writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))); 23 writer.write(logs + "\n"); 24 } catch (Exception e) { 25 e.printStackTrace(); 26 } finally { 27 if (null != writer) { 28 try { 29 writer.close(); 30 } catch (IOException e) { 31 // TODO Auto-generated catch block 32 e.printStackTrace(); 33 } 34 } 35 } 36 }

1 private void sendActionToServer() { 2 if (!isSending && null != mHandler) { 3 mHandler.post(new Runnable() { 4 5 @Override 6 public void run() { 7 if (isSending) { 8 return; 9 } 10 File temp = new File(userActionFilePath); 11 if (null != temp) { 12 long length = temp.length(); 13 if (length > MAXSIZE_FILE_CACHE) { 14 // 文件大於閥值,刪除文件,重置重試次數 15 retryCount = 0; 16 return; 17 } 18 if (length > SIZE_FILE_CACHE && retryCount >= 3) { 19 // 文件大小大於設置值並失敗次數大於3次,刪除文件重置重試次數 20 retryCount = 0; 21 return; 22 } 23 // 開始發送服務 24 isSending = true; 25 new Thread(new Runnable() { 26 27 @Override 28 public void run() { 29 if (sendService()) { 30 // 發送服務成功,清空文件內容 31 retryCount = 0; 32 } else { 33 // 發送服務失敗,增加重試次數 34 retryCount++; 35 } 36 // 服務發送完畢,重置標記位 37 isSending = false; 38 } 39 }).start(); 40 } 41 } 42 }); 43 } 44 }
以上代碼在文件大小大於閥值時會刪除了文件,在發送服務次數到達了3次后刪除文件。