大数据时代互联网产品针对用户数据采集和分析是十分重要的一环,作为一个Android开发者一直以来对于埋点(特别是可视化埋点)十分感兴趣。最近了解【易观】数据统计开源了其Sdk源码在GitHub,通过理解其源码多少可以学到一部分关于埋点的技术原理。文末附易观开源SDK官方链接,在此我们只研究Android技术~


一、初始化SDK逻辑

在Application中调用init方法,传入上下文(context) 和 配置参数(config)

/*** 初始化方舟SDK相关API*/
private void initAnalsysy() {...AnalysysConfig config = new AnalysysConfig();// 设置key(目前使用电商demo的key)config.setAppKey(APP_KEY);...// 初始化AnalysysAgent.init(this, config);
}

调用到AgentProcess类中的init方法

public void init(final Context context, final AnalysysConfig config) {...//初始化工具类,注入上下文对象AnalysysUtil.init(context);//监听crash事件CrashHandler.getInstance().setCallback(new CrashHandler.CrashCallBack() {@Overridepublic void onAppCrash(Throwable e) {//上报crash事件CrashHandler.getInstance().reportException(context, e, CrashHandler.CrashType.crash_auto);}});//监听生命周期事件ActivityLifecycleUtils.initLifecycle();//添加生命周期事件回调ActivityLifecycleUtils.addCallback(new AutomaticAcquisition());//初始化AThreadPool,设置key、channel、url等config参数AThreadPool.initHighPriorityExecutor(new Callable() {@Overridepublic Object call() throws Exception {...}});
}

这里主要看一下AutomaticAcquisition类,它本质上是一个Application.ActivityLifecycleCallbacks 的实现类,回调每个activity生命周期事件,主动上报了页面、应用启动、应用结束等信息。

二、采集热图

热图数据的采集逻辑,算是初始化过程中比较重要的一个环节。

// 热图数据采集(默认关闭)
config.setAutoHeatMap(true);

当我们开启了热图数据采集,在AutomaticAcquisition的onActivityCreated中会初始化一个视图树监听器

private void initHeatMap(final WeakReference<Activity> wa) {layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {@Overridepublic void onGlobalLayout() {...final Activity activity = wa.get();if (activity != null) {...//页面宽高分辨率等信息HeatMap.getInstance().initPageInfo(activity);....//hookHeatMap.getInstance().hookDecorViewClick(activity.getWindow().getDecorView());}}};
}

这里重点看一下hookDecorViewClick方法的实现

/**** 递归调用解析view* @param decorView 根节点view*/
public void hookDecorViewClick(View decorView) throws Exception {if (decorView instanceof ViewGroup) {hookViewClick(decorView);int count = ((ViewGroup) decorView).getChildCount();for (int i = 0; i < count; i++) {if (((ViewGroup) decorView).getChildAt(i) instanceof ViewGroup) {hookDecorViewClick(((ViewGroup) decorView).getChildAt(i));} else {hookViewClick(((ViewGroup) decorView).getChildAt(i));}}} else {hookViewClick(decorView);}
}/*** 反射给View注册监听*/
private void hookViewClick(View view) throws Exception {...Class viewClass = Class.forName("android.view.View");Method getListenerInfoMethod = viewClass.getDeclaredMethod("getListenerInfo");if (!getListenerInfoMethod.isAccessible()) {getListenerInfoMethod.setAccessible(true);}Object listenerInfoObject = getListenerInfoMethod.invoke(view);Class mListenerInfoClass = Class.forName("android.view.View$ListenerInfo");Field mOnClickListenerField = mListenerInfoClass.getDeclaredField("mOnTouchListener");mOnClickListenerField.setAccessible(true);//这里通过一系列反射操作拿到了view的mOnTouchListener对象Object touchListenerObj = mOnClickListenerField.get(listenerInfoObject);if (!(touchListenerObj instanceof HookTouchListener)) {HookTouchListener touchListenerProxy = new HookTouchListener((View.OnTouchListener) touchListenerObj);//设置成代理类(HookTouchListener)对象mOnClickListenerField.set(listenerInfoObject, touchListenerProxy);}
}

代理类HookTouchListener中

private class HookTouchListener implements View.OnTouchListener {private View.OnTouchListener onTouchListener;private HookTouchListener(View.OnTouchListener onTouchListener) {this.onTouchListener = onTouchListener;}@Overridepublic boolean onTouch(final View v, final MotionEvent event) {...if (event.getAction() == MotionEvent.ACTION_DOWN) {try {// 黑白名单判断if (isTackHeatMap(v)) {//上报热图信息setCoordinate(v, event);}} catch (Throwable ignore) {ExceptionUtil.exceptionThrow(ignore);}}...}
}private void setCoordinate(final View v, final MotionEvent event) {final float rawX = event.getRawX();final float rawY = event.getRawY();if (isTouch(rawX, rawY)) {x = event.getX();y = event.getY();final long currentTime = System.currentTimeMillis();AThreadPool.asyncLowPriorityExecutor(new Runnable() {@Overridepublic void run() {try {...final String path = PathGeneral.getInstance().general(v);boolean isAddPath = setPath(path);if (isAddPath) {rx = rawX;ry = rawY;//添加点击控件坐标setClickCoordinate();//添加点击控件类型及内容setClickContent(v);//添加页面数据clickInfo.putAll(pageInfo);//上报控件信息AgentProcess.getInstance().pageTouchInfo(clickInfo,currentTime);}} catch (Throwable ignore) {ExceptionUtil.exceptionThrow(ignore);}}});}
}

最后在onActivityResumed生命周期中绑定监听器

rootView.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);

当我们点击一个View时一旦触发ontouch事件,就会自动上报这个View的热图数据。

三、可视化埋点之创建Socket连接

可视化埋点从操作上来看,首先,管理网站跟我们的应用建立一个连接,获取应用页面信息,添加埋点事件配置,然后将埋点事件下发到应用。应用接收到下发的埋点配置数据后,匹配并设置埋点事件、在触发事件时主动上报事件。

易观中,App应用作为Socket客户端与管理网站建立链接,具体实现步骤如下:

// 设置 WebSocket 连接 Url
AnalysysAgent.setVisitorDebugURL(this, SOCKET_URL);/*** AnalysysAgent* 设置可视化websocket服务器地址*/
public static void setVisitorDebugURL(Context context, String url) {AgentProcess.getInstance().setVisitorDebugURL(url);
}/*** 设置可视化websocket服务器地址*/
public void setVisitorDebugURL(final String url) {AThreadPool.asyncHighPriorityExecutor(new Runnable() {@Overridepublic void run() {//设置链接地址,//LifeCycleConfig读取了LifeCycleConfig.json//LifeCycleConfig.visual.optString(START)映射到VisualAgent.setVisitorDebugURL方法setVisualUrl(context,LifeCycleConfig.visual.optString(START), url);}});
}
//通过反射设置url
private void setVisualUrl(Context context, String path, String url) {int index = path.lastIndexOf(".");CommonUtils.reflexStaticMethod(path.substring(0, index),path.substring(index + 1),new Class[]{Context.class, String.class},context, url);
}
/*** 设置可视化websocket服务器地址*/
public static void setVisitorDebugURL(Context context, String url) {...//初始化可视化埋点initVisual(context);...
}/*** 初始化可视化埋点功能*/
private static void initVisual(final Context context) {//创建VisualManager实例VisualManager.getInstance(context);
}/*** VisualManager构造函数*/
private VisualManager(Context context) {//创建ViewCrawler 对象viewCrawler = new ViewCrawler(context);viewCrawler.startUpdates();
}

在ViewCrawler中创建了一个传感器监听SensorHelper,监听摇一摇和屏幕翻转事件,在其onFlipGesture回调中发送消息,尝试调用connectToEditor建立socket连接。(关于socket连接这里就不分析了,接下来我们主要关注其通过这个连接要做什么事情)

四、可视化埋点之数据交互

通过上一步建立了socket连接后,会生成一个EditorConnection对象,这里面有一个EditorClient持有了socket客户端对象。这里主要看下接受socket消息的方法,其中定义了三个事件。

@Override
public void onMessage(String message) {...final JSONObject messageJson = new JSONObject(message);final String type = messageJson.getString("type");...if ("device_info_request".equals(type)) {//请求设备的基本信息mService.sendDeviceInfo();} else if ("snapshot_request".equals(type)) {//请求快照信息// (具体为获取现在屏幕截图和详细组件信息)mService.sendSnapshot(messageJson);} else if ("event_binding_request".equals(type)) {//可视化埋点绑定信息mService.bindEvents(messageJson);}...
}

那么来看下接收到Socket服务下发的消息后,客户端怎么处理的呢?以event_binding_request事件为例,也就是在网页上添加了埋点配置,发送给应用,应用绑定配置的过程

首先,EditorConnection的mService是个Editor对象,接收到socket消息后,通过Editor包装成一个message对象发送给了ViewCrawlerHandler

private class Editor implements EditorConnection.Editor {...@Overridepublic void sendDeviceInfo() {final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_SEND_DEVICE_INFO);mMessageThreadHandler.sendMessage(msg);}...
}

在handleMessage方法中接收到MESSAGE_HANDLE_EDITOR_BINDINGS_RECEIVED事件后,调用handleEditorBindingsReceived方法解析并封装数据,再调用applyEventBindings方法绑定埋点事件。

private void handleEditorBindingsReceived(JSONObject message) {...//解析、封装、缓存数据...//绑定埋点事件applyEventBindings();
}private void applyEventBindings() {final List<EgPair<String, BaseViewVisitor>> newVisitors = new ArrayList<>();...{for (EgPair<String, JSONObject> changeInfo : mEditorEventBindings.values(){...//生成BaseViewVisitorfinal BaseViewVisitor visitor = mProtocol.readEventBinding(changeInfo.second, mDynamicEventTracker);newVisitors.add(new EgPair<>(changeInfo.first,visitor)); ...        }}final Map<String, List<BaseViewVisitor>> editMap = new HashMap<>();...//mEditState.setEdits(editMap);
}public void setEdits(Map<String, List<BaseViewVisitor>> newEdits) {...applyEditsOnUiThread();...
}private void applyEditsOnUiThread() {if (Thread.currentThread() == mUiThreadHandler.getLooper().getThread()) {applyIntendedEdits();} else {mUiThreadHandler.post(new Runnable() {@Overridepublic void run() {applyIntendedEdits();}});}
}private void applyIntendedEdits() {...applyChangesFromList(rootView, specificChanges);...
}private void applyChangesFromList(View rootView, List<BaseViewVisitor> changes) {synchronized (mCurrentEdits) {final int size = changes.size();for (int i = 0; i < size; i++) {final BaseViewVisitor visitor = changes.get(i);final EditBinding binding = new EditBinding(rootView, visitor, mUiThreadHandler);mCurrentEdits.add(binding);}}
}

通过上述一系列传递调用,最终生成了EditBinding对象,我们分析这个类,看看事件具体是怎么绑定到View上的

private static class EditBinding implements ViewTreeObserver.OnGlobalLayoutListener, Runnable {...public EditBinding(View viewRoot, BaseViewVisitor edit, Handler uiThreadHandler) {...run();}...@Overridepublic void run() {...//此处理回最终的事件回调,这个mEdit就是之前解析数据生成的BaseViewVisitormEdit.visit(viewRoot);...    }...
}public void visit(View rootView) {//这里就是根据事件path找到View的方法了mPathfinder.findTargetsInRoot(rootView, mPath, this);
}public void findTargetsInRoot(View givenRootView, List<PathElement> path,Accumulator accumulator) {...//这里是根据path具体寻找viewfinal View rootView = findPrefixedMatch(rootPathElement, givenRootView, indexKey);...if (null != rootView) {//找到view后需要绑定事件了findTargetsInMatchedView(rootView, childPath, accumulator);}
}private void findTargetsInMatchedView(View alreadyMatched, List<PathElement> remainingPath,Accumulator accumulator) {if (remainingPath.isEmpty()) {//已经找到view了,那么直接设置埋点事件就可以了accumulator.accumulate(alreadyMatched);return;}//还没有找到对应的view,就继续找...
}

通过上面几步,找到了埋点事件对应的View,那么accumulator是怎么埋点的?首先,我们需要知道这里的accumulator对象是前面通过mProtocol.readEventBinding生成的,埋点事件不同,生成的visitor也是不一样的,我们以点击事件为例,生成的就是AddAccessibilityEventVisitor对象了,那么我们看一下accumulate究竟做了什么?

@Overridepublic void accumulate(View found) {final View.AccessibilityDelegate realDelegate = getOldDelegate(found);...//这里创建了一个AccessibilityDelegate的代理final TrackingAccessibilityDelegate newDelegate = new TrackingAccessibilityDelegate(realDelegate);//设置自定义的代理found.setAccessibilityDelegate(newDelegate);...
}private class TrackingAccessibilityDelegate extends View.AccessibilityDelegate {private View.AccessibilityDelegate mRealDelegate;...@Overridepublic void sendAccessibilityEvent(View host, int eventType) {//当view触发事件的时候,判断是不是我们的埋点事件if (eventType == mEventType) {//命中埋点事件fireEvent(mPreviousText, host);}if (null != mRealDelegate) {mRealDelegate.sendAccessibilityEvent(host, eventType);}}
}private static abstract class BaseEventTriggeringVisitor extends BaseViewVisitor {...protected void fireEvent(String previousText, View found) {//通过listener回调事件mListener.onEvent(found, mEventName, previousText, mMatchText, mDebounce);}...
}class DynamicEventTracker implements BaseViewVisitor.OnEventListener {...@Overridepublic void onEvent(View v, String eventName, String previousText, String matchText,boolean debounce) {// 触发可视化埋点事件的回调...EventSender.sendEventToSocketServer(v, eventName);//终于看到上报事件了...AnalysysAgent.track(v.getContext(), eventName);...}
}

看到这里有没有一种豁然开朗的感觉呢,简单来说可视化下发一个埋点配置(事件id、事件类型、事件路径等等信息),接收到下发的数据后,首先根据路径信息找到匹配的view,设置代理拦截View的sendAccessibilityEvent方法,这样就完成了配置。当view命中埋点事件时,通过自定义的listener触发数据上报给服务器,这样就完成了一个完整的埋点流程。那么这里为什么不像上面采集热图一样hook onTouch时间呢?

class EditProtocol {...public BaseViewVisitor readEventBinding(JSONObject source,BaseViewVisitor.OnEventListener listener)throws BadInstructionsException {if ("click".equals(eventType)) {return new BaseViewVisitor.AddAccessibilityEventVisitor(path,AccessibilityEvent.TYPE_VIEW_CLICKED,eventID,matchType,listener);} else if ("selected".equals(eventType)) {...} else if ("text_changed".equals(eventType)) {...} else if ("detected".equals(eventType)) {...} else {throw new BadInstructionsException("can't track event type \"" +eventType + "\"");}...
}

看readEventBinding方法就可以理解了,我们不单单要支持点击,我们需要支持更多类型的埋点事件。

五、可视化埋点之用户获取埋点配置

最后,我们可视化埋点都配置好了,那么用户app怎么获取到我们设置的埋点配置,又是怎么让埋点生效呢?

// 初始化时 设置配置下发 Url
AnalysysAgent.setVisitorConfigURL(this, CONFIG_URL);/*** AnalysysAgent* 设置线上请求埋点配置的服务器地址*/
public static void setVisitorConfigURL(Context context, String url) {AgentProcess.getInstance().setVisitorConfigURL(url);
}/*** AgentProcess* 设置线上请求埋点配置的服务器地址*/
public void setVisitorConfigURL(final String url) {AThreadPool.asyncHighPriorityExecutor(new Runnable() {@Overridepublic void run() {...//读取LifeCycleConfig配置,反射调用VisualAgent.setVisitorConfigURL方法setVisualUrl(context,LifeCycleConfig.visualConfig.optString(START),url);...}});
}/*** VisualAgent* 设置线上请求埋点配置的服务器地址*/
public static void setVisitorConfigURL(Context context, String url) {...initConfig(context);...
}/*** VisualAgent* 初始化可视化*/
public static synchronized void initConfig(Context context) {StrategyGet.getInstance(context).getVisualBindingConfig();...
}/*** StrategyGet* http请求是否有可视化埋点协议,并应用*/
public void getVisualBindingConfig() {...new Thread(new Runnable() {@Overridepublic void run() {new StrategyTask(mContext, url).run();}}).start();...
}public class StrategyTask implements Runnable {...@Overridepublic void run() {...//应用埋点配置VisualManager.getInstance(mContext).applyEventBindingEvents(jsonObject.getJSONArray("data"));...}
}/*** VisualManager* http请求是否有可视化埋点协议,并应用*/
public void applyEventBindingEvents(JSONArray bindingEvents) {viewCrawler.setEventBindings(bindingEvents);
}/*** viewCrawler* 保存当前与历史绑定事件Presistent*/
public void setEventBindings(JSONArray bindings) {if (bindings != null) {final Message msg =
mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_EVENT_BINDINGS_RECEIVED);msg.obj = bindings;mMessageThreadHandler.sendMessage(msg);}
}

初始化的时候,当我们配置了对应的url之后,通过反射调用到下载配置的任务,获取到配置后通过message发送给ViewCrawler,这之后的逻辑就跟可视化编辑器下发配置后,解析、匹配、代理、监听、上报数据一致了。

六、总结

埋点大致可以分为,代码埋点、全埋点、可视化埋点。在这里只是简单跟踪和了解一下可视化埋点的大致思路。其实无论是哪种埋点,都需要上报数据,这里面也有很多的细节,例如:

1、缓存策略

易观中使用contentProvider 统筹了以下三种存储方式:

① 数据库:待上传的事件数据
② sp: 存储一些配置信息(url、key、channel等),状态值(PV值、上个页面结束时间、应用启动时间)
③ 内存:网络上传策略和最大缓存数量

2、上传策略

① 服务器策略处理
     * 网络不通返回false,
     * 判断服务器debug模式为1或2时,实时上传.
     * 策略为 0:智能发送

判断db内事件条数是否大于设置条数,间隔时间是否大于设置的间隔时间
     * 策略为 1:实时发送,
     * 策略为 2:间隔发送,

② 用户策略处理
     * 判断用户debug模式为1或2时,实时上传.
     * 判断db内事件条数是否大于设置条数
     * 当前时间减去上次发送时间是否大于设置的间隔时间

③  默认策略
     * 实时上传

3、白名单和黑名单

这块主要适用于全埋点


官方链接:

易观开源SDK-开发者_易观方舟|智能用户运营易观方舟提供:体验版(免费试用30天)、商业版(云端托管,适用中小团队)、企业版(私有化部署,适用于中大型团队),拨打:4006 - 010 – 231选择合适方案。https://www.analysysdata.com/developer-sdk.htmlhttps://github.com/analysys/ans-android-sdkhttps://github.com/analysys/ans-android-sdk

从Android源码出发理解【易观】埋点相关推荐

  1. 【Android】Java回调原理并结合Android源码进行理解

    回调机制是一种常见的设计模式,它把工作流内的某个功能按照约定的接口暴露给外部使用者,为外部使用者提供数据,或要求外部使用者提供数据. 之前对于回调一直是一知半解,而且总是停留在C++的函数指针的理解之 ...

  2. android 浏览器源码分析,从源码出发深入理解 Android Service

    原标题:从源码出发深入理解 Android Service 原文链接: 建议在浏览器上打开,删除了大量代码细节,:) 本文是 Android 系统学习系列文章中的第三章节的内容,介绍了 Android ...

  3. android 点击事件消费,Android View事件分发和消费源码简单理解

    Android View事件分发和消费源码简单理解 前言: 开发过程中觉得View事件这块是特别烧脑的,看了好久,才自认为看明白.中间上网查了下singwhatiwanna粉丝的读书笔记,有种茅塞顿开 ...

  4. Android源码分析-全面理解Context

    前言 Context在android中的作用不言而喻,当我们访问当前应用的资源,启动一个新的activity的时候都需要提供Context,而这个Context到底是什么呢,这个问题好像很好回答又好像 ...

  5. 结合源码深入理解Android Crash处理流程

    应用程序crash在开发过程中还是很常见的,本文主要是从源码的角度去跟踪下Android对于crash的处理流程.App crash的全称:Application crash.而Crash又分为:na ...

  6. Android学习之Activity源码的理解(一)

    一.Activity为Android系统中四大组件之一,是Android程序的呈现层,并通过界面与用户进行交互,因此理解Activity源码是有必要的. 二.之前我写过一篇文章:http://blog ...

  7. 《深入理解Android内核设计思想(第2版)(上下册)》之Android源码下载及编译

    本文摘自人民邮电出版社异步社区<深入理解Android内核设计思想(第2版)(上下册)> 购书地址:http://item.jd.com/12212640.html 试读地址:http:/ ...

  8. 《深入理解Android内核设计思想(第2版)(上下册)》之Android源码下载及编译...

    本文摘自人民邮电出版社异步社区<深入理解Android内核设计思想(第2版)(上下册)> 购书地址:item.jd.com/12212640.ht- 试读地址:www.epubit.com ...

  9. Android源码篇-深入理解粘性广播(1)

    文章目录 广播 基本概念 发送广播 plantuml sendStickyBroadcast 声明 流程图 流程中关键代码 userid uid appid 多用户 数据结构 stickyBroadc ...

最新文章

  1. 物联网的编年史1974-2025 你都知道多少?
  2. 其他算法-Dijkstra
  3. windows安装spark工具记录
  4. 什么是死锁(deadlock)?
  5. 【数据结构与算法】链表倒序输出算法
  6. [MFC] CDialog::DoModal()函数用法
  7. 通过PEB遍历进程模块(x64/wow4)
  8. Linux终端显示图像
  9. 中缀表达式、前缀表达式、后缀表达式
  10. color ui的使用
  11. 模糊集合及运算1.4
  12. Intellij IDEA 设置字体的大小
  13. web前端面试题【html+css+js+框架】
  14. 常见的74系列集成电路
  15. numpy_absolute函数
  16. 计算机网络ip地址在哪,w7的ip地址在哪?小编教你怎么查看
  17. VMware安装优麒麟20.04LTS
  18. Win10 升级安装全攻略
  19. 推荐几款渗透测试常用的脚本(记得收藏)
  20. 圆形断面正常水深莫洛图

热门文章

  1. Android 锁屏无法继续定位问题,安卓消息分发机制
  2. 738.单调递增的数字。贪心算法
  3. 39-【什么叫规矩 什么叫体统】内置算法-遍历、搬运、查找
  4. 国外优秀设计网站推荐
  5. [转帖]05年考研数学辅导书推荐
  6. 二十七、 影子价格(对偶问题)
  7. 易表软件强大但高估用户水平
  8. [论文阅读] The Case for Learned Index Structures
  9. poi word操作之XWPFTable合并单元格
  10. android自定义大转盘,android 代码绘制转盘抽奖的实现