在我们展开讨论之前,首先要明白什么是用户行为记录?
我的理解是 用户行为记录是针对使用我们软件的用户的信息统计,这些信息主要是 用户的操作信息 以及 其他一些相关感兴趣的日志信息,这些信息通常由用户的操作而产生,经我们包装处理后上传到服务器,以供数据分析员使用分析。不要小看这些信息,分析信息可以让我们了解软件的用户群,平台信息,以及用户的操作习惯等,现在已经有越来越多的开发者和公司重试并利用这一块。
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次后删除文件。