从Android源码出发理解【易观】埋点

大数据时代互联网产品针对用户数据采集和分析是十分重要的一环,作为一个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() {
        @Override
        public 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() {
        @Override
        public Object call() throws Exception {
            ...
        }
    });
}

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

二、采集热图

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

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

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

private void initHeatMap(final WeakReference wa) {
    layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            ...
            final Activity activity = wa.get();
            if (activity != null) {
                ...
                //页面宽高分辨率等信息
                HeatMap.getInstance().initPageInfo(activity);
                ....
                //hook
                HeatMap.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;
    }

    @Override
    public 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() {
            @Override
            public 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() {
      @Override
      public 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 {
    ...
    @Override
    public 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> newVisitors = new ArrayList<>();
    ...
    {
        for (EgPair changeInfo : mEditorEventBindings.values(){
            ...
            //生成BaseViewVisitor
            final BaseViewVisitor visitor = mProtocol.readEventBinding(changeInfo.second, mDynamicEventTracker);
            newVisitors.add(new EgPair<>(changeInfo.first,visitor)); 
            ...        
        }
    }
    final Map> editMap = new HashMap<>();
    ...
    //
    mEditState.setEdits(editMap);   
}

public void setEdits(Map> newEdits) {
    ...
    applyEditsOnUiThread();
    ...
}

private void applyEditsOnUiThread() {
    if (Thread.currentThread() == mUiThreadHandler.getLooper().getThread()) {
        applyIntendedEdits();
    } else {
        mUiThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                applyIntendedEdits();
            }
        });
    }
}

private void applyIntendedEdits() {
    ...
    applyChangesFromList(rootView, specificChanges);
    ...
}

private void applyChangesFromList(View rootView, List 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();
    }
    ...
    @Override
    public void run() {
        ...
        //此处理回最终的事件回调,这个mEdit就是之前解析数据生成的BaseViewVisitor
        mEdit.visit(viewRoot);
        ...    
    }
    ...
}

public void visit(View rootView) {
    //这里就是根据事件path找到View的方法了
    mPathfinder.findTargetsInRoot(rootView, mPath, this);
}

public void findTargetsInRoot(View givenRootView, List path,
                                  Accumulator accumulator) {
    ...
    //这里是根据path具体寻找view
    final View rootView = findPrefixedMatch(rootPathElement, givenRootView, indexKey);
    ...
    if (null != rootView) {
        //找到view后需要绑定事件了
        findTargetsInMatchedView(rootView, childPath, accumulator);
    }
}

private void findTargetsInMatchedView(View alreadyMatched, List remainingPath,Accumulator accumulator) {
    if (remainingPath.isEmpty()) {
        //已经找到view了,那么直接设置埋点事件就可以了
        accumulator.accumulate(alreadyMatched);
        return;
    }
    //还没有找到对应的view,就继续找
    ...
}

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

@Override
 public 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;
    ...
    
    @Override
    public 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 {
    ...
    @Override
    public 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() {
      @Override
      public 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() {
        @Override
        public void run() {
            new StrategyTask(mContext, url).run();
        }
    }).start();
    ...
}

public class StrategyTask implements Runnable {
    ...
    @Override
    public 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源码分析,android)