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

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

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_activityString埋点View所在的Activity
event_nameString事件名称
event_typeString事件类型,如click
trigger_idint未知
deployedboolean未知
pathPathfinder.PathElement[]埋点View在ViewTree中的找寻路径,格式见”path数据格式”

path数据格式

参数名类型说明
prefixString是否深度优先遍历
view_classString埋点View的class名称
indexint选择第index+1个匹配View作为返回结果
idint埋点View的id。如果该属性未给出,将通过sa_id_name查找
sa_id_nameString埋点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 alive
mEdit.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设置代理监听:

@Override
public 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的混合值作为唯一标识。后续异步更新这个事件时,通过唯一标识在缓存中查找。

@Override
public 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()中连接服务端,即握手过程。握手成功后,响应服务端需要的数据:

@Override
public 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
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值