Sensors Analytics可视化埋点代码阅读笔记

来源:互联网 发布:待遇 知乎 编辑:程序博客网 时间:2024/05/18 17:43

这是一个代码阅读笔记,而不是实现分析,想要深入学习亲自阅读源码才是最好的。

Sensors Analytics是一款sdk端开源的统计工具,并在各语言各平台上有相应的SDK。本文学习的是Android版本。由于对可视化埋点的实现感兴趣,于是写个笔记记录下阅读过程。

可视化埋点

功能包括两部分:
- 非代码埋点 允许手机连接应用后台管理界面,通过可视化操作设置埋点。
- 无需更新 服务端配置动态配置埋点位置,可配置的埋点必须是view的特定事件,例如click。


主要类

  • ViewCrawler 可视化埋点门面封装,其setEventBindings()用于设置埋点配置。
  • EditState 根据应用的生命周期管理埋点配置。ViewCrawler持有一个EditState实例。
  • ViewVisitor 用于遍历ViewTree。其内部有一个Pathfinder委托处理遍历逻辑,并以List<Pathfinder.PathElement>形式存储查找路径。
  • EditBinding 将埋点配置与ViewVisitor绑定。其自身会监听onGlobalLayout并调用ViewVisitor.visit()开始遍历。
  • Pathfinder 封装真正的ViewTree遍历过程。

初始化

应用启动时加载本地缓存埋点配置,同时向服务端获取埋点配置。无论本地配置还是服务端配置,成功读取后,最终以Map<Activity, Set<EditBinding>>形式保存在EditState

同时,启动时加载应用的R.id.class,通过反射获取id名和id的对应关系。这使得服务端可以通过id名而不是id配置埋点,提高了可读性。


获取服务端配置的埋点

埋点数据格式

参数名 类型 说明 target_activity String 埋点View所在的Activity event_name String 事件名称 event_type String 事件类型,如click trigger_id int 未知 deployed boolean 未知 path Pathfinder.PathElement[] 埋点View在ViewTree中的找寻路径,格式见”path数据格式”

path数据格式

参数名 类型 说明 prefix String 是否深度优先遍历 view_class String 埋点View的class名称 index int 选择第index+1个匹配View作为返回结果 id int 埋点View的id。如果该属性未给出,将通过sa_id_name查找 sa_id_name String 埋点View的id名,如R.id.tv_name

运行过程

EditBinding自身是OnGlobalLayoutListener,实例化时将自身添加到ViewTree观察者:

public EditBinding(View viewRoot, ViewVisitor edit, Handler uiThreadHandler) {    //...    final ViewTreeObserver observer = viewRoot.getViewTreeObserver();    if (observer.isAlive()) {        observer.addOnGlobalLayoutListener(this);    }    run();}

onGlobalLayout()中第一次遍历ViewTree,并且此后每隔5秒遍历ViewTree,寻找埋点View:

if (!mAlive) {    return;}final View viewRoot = mViewRoot.get();if (null == viewRoot || mDying) {    cleanUp();    return;}// ELSE View is alive and we are alivemEdit.visit(viewRoot);mHandler.removeCallbacks(this);mHandler.postDelayed(this, 5000);

mEdit.visit(viewRoot);即为遍历过程,最终调用自身的findTargetsInMatchedView()

private void findTargetsInMatchedView(View alreadyMatched, List<PathElement> remainingPath,                                          Accumulator accumulator) {    if (remainingPath.isEmpty()) {        // 已经匹配了View        accumulator.accumulate(alreadyMatched);        return;    }    //...嵌套匹配逻辑}

findTargetsInMatchedView()根据id、id名(如果有的话)、index、prefix查找View。

index的官方解释:

The index attribute, counting from root to leaf, and first child to last child, selects a particular matching view amongst all possible matches. Indexing starts at zero, like an array
index. So E.index == 2 means “Select the third possible match for this element”.

prefix的官方解释:

The prefix attribute refers to the position of the matched views in the hierarchy, relative to the current position of the path being searched.

prefix有两种值,影响View遍历顺序:ZERO_LENGTH_PREFIXSHORTEST_PREFIX。其中SHORTEST_PREFIX允许深度优先遍历。

以上引用的代码中Accumulator.accumulate()用于处理遍历结果。由于ViewVisitor继承于Accumulator,所以最后alreadyMatched交给ViewVisitor自身处理。accumulate()的实现与该ViewVisitor监听的事件有关。例如,如果监听onClick,则对view设置代理监听:

@Overridepublic void accumulate(View found) {    final View.AccessibilityDelegate realDelegate = getOldDelegate(found);    if (realDelegate instanceof TrackingAccessibilityDelegate) {        final TrackingAccessibilityDelegate currentTracker =                (TrackingAccessibilityDelegate) realDelegate;        if (currentTracker.willFireEvent(getEventName())) {            return; // Don't double track        }    }    if (SensorsDataAPI.ENABLE_LOG) {        Log.i(LOGTAG, String.format("ClickVisitor accumulated. View %s", found.toString()));    }    // We aren't already in the tracking call chain of the view    final TrackingAccessibilityDelegate newDelegate =            new TrackingAccessibilityDelegate(realDelegate);    found.setAccessibilityDelegate(newDelegate);    mWatching.put(found, newDelegate);}

监听到事件后,在DynamicEventTracker中处理事件。如果是click这样的单次事件,则立即发送报告;如果是edited事件,则缓存,延迟3秒发送。需要缓存的事件取eventName、triggerId、view三者hashCode的混合值作为唯一标识。后续异步更新这个事件时,通过唯一标识在缓存中查找。

@Overridepublic void OnEvent(View v, EventInfo eventInfo, boolean debounce) {    final long moment = System.currentTimeMillis();    final JSONObject properties = new JSONObject();    try {        properties.put("$from_vtrack", String.valueOf(eventInfo.mTriggerId));        properties.put("$binding_trigger_id", eventInfo.mTriggerId);        properties.put("$binding_path", eventInfo.mPath);        properties.put("$binding_depolyed", eventInfo.mIsDeployed);    } catch (JSONException e) {        Log.e(LOGTAG, "Can't format properties from view due to JSON issue", e);    }    // 对于Clicked事件,事件发生时即调用track记录事件;对于Edited事件,由于多次Edit时会触发多次Edited,    // 所以我们增加一个计时器,延迟发送Edited事件    if (debounce) {        final Signature eventSignature = new Signature(v, eventInfo);        final UnsentEvent event = new UnsentEvent(eventInfo, properties, moment);        synchronized (mDebouncedEvents) {            final boolean needsRestart = mDebouncedEvents.isEmpty();            mDebouncedEvents.put(eventSignature, event);            if (needsRestart) {                mHandler.postDelayed(mTask, DEBOUNCE_TIME_MILLIS);            }        }    } else {        try {            SensorsDataAPI.sharedInstance(mContext).track(eventInfo.mEventName, properties);        } catch (InvalidDataException e) {            Log.w("Unexpected exception", e);        }    }}

可视化埋点连接

通过enableEditingVTrack()开启可视化埋点。

实现原理:监听ActivityLifecycleCallbacks,每次在onResume()中连接服务端,即握手过程。握手成功后,响应服务端需要的数据:

@Overridepublic void onMessage(String message) {    try {        final JSONObject messageJson = new JSONObject(message);        final String type = messageJson.getString("type");        if (type.equals("device_info_request")) {            mService.sendDeviceInfo(messageJson);        } else if (type.equals("snapshot_request")) {            mService.sendSnapshot(messageJson);        } else if (type.equals("event_binding_request")) {            mService.bindEvents(messageJson);        } else if (type.equals("disconnect")) {            mService.disconnect();        }    } catch (final JSONException e) {        Log.e(LOGTAG, "Bad JSON received:" + message, e);    }}

潜在问题

代码不灵活

大量socket封装用于实现可视化埋点,而一旦用户不需要这个功能,这些代码显得冗余。

数据库操作

这种统计SDK一般都会将信息存在本地数据库,并在合适的时机上传。然而本SDK的数据库实现过于简单,数据库操作不基于事务,容错差。

0 0
原创粉丝点击