一、用户路径

用户路径分析为八大重要分析模型之一,可以追踪用户从某个开始行为事件直到结束事件的行为路径,是一种监测用户流向,从而统计产品使用深度的分析方法,帮助业务人员了解用户行为分布情况,对海量用户的行为习惯形成宏观了解。

用户路径的作用

用户路径可以帮助使用者洞察用户看似平常的行为背后真正的思想,从而摆脱“大海捞针”式的用户行为数据查询。
使用者既可以有的放矢,验证自身假设,有针对性地解决问题;也可以日常监测用户的行为路径,及时发现用户的核心关注点及干扰选项,引导用户持续挖掘产品及服务的价值。

二、用户路径的采集

一个用户的完整行为路径会包含多个行为事件,以电商为例,用户从打开 App 到支付成功要经过首页浏览、搜索商品、加入购物车、提交订单、支付订单等过程。而在用户真实的选购过程是一个交缠反复的过程,例如提交订单后,用户可能会返回首页继续搜索商品,也可能去取消订单,每一个路径背后都有不同的动机。通常一个完整的用户路径在会包含 App 启动、若干个页面浏览和 App 退出等事件。在神策 Android SDK 中是通过 Application.ActivityLifecycleCallbacks 监听实现 App 启动、App 退出和页面浏览三个行为事件。

2.1 基础概念

要想了解神策的用户路径采集原理,首先我们要了解下 session 和补发机制。

session 机制

在 Android App ,由于用户会很频繁的切换应用,就会造成应用的启动和退出事件过于频繁,且会打断用户正常浏览序列。

例如在上图这种情况,就会有 2 个完整的用户路径:

打开应用 → 浏览页面1 → 关闭/切换应用  → 打开应用 → 浏览页面2 → 关闭/切换应用

而页面浏览1和页面浏览2就被切割成2个独立的用户路径。

所以神策 Android SDK 为了应对应用切换、多进程和强杀等场景,加入了 session 机制(默认30秒,可动态设置)用户打开 App 距上次退出 App 少于设置的 session 时间,则不会触发应用退出和启动事件,如下图:

这时用户路径就被变成为:

打开应用 → 浏览页面1 →  浏览页面2 →  关闭/切换应用

补发机制

如果在退出 App 到后台 30 秒内,进程还没有被杀掉,那么此时会触发退出事件并尝试上报,如果进程被杀掉了,那么退出事件会在下一次启动时补发。

了解完 session 机制和补发机制后,我们再从代码层面来具体讲讲用户路径事件的采集逻辑。

2.2 App 启动事件

当用户首次启动 App 时,如果满足我们的 session 时长机制则会触发 App 启动事件。接下来我们看下 SensorsDataActivityLifecycleCallbacks 中的 onActivityStrated 方法 的实现。

应用启动
@Override
public void onActivityStarted(Activity activity) {
    try {
       ...
        //step 1 读取 Activity 启动的个数
        if (isMultiProcess) {//是否多进程
            startActivityCount = mDbAdapter.getActivityCount();
            mDbAdapter.commitActivityCount(++startActivityCount);
        else {
            ++startActivityCount;
        }
        // 如果是第一个页面
        if (startActivityCount == 1) {
            ...
            boolean sessionTimeOut = isSessionTimeOut();
            //step 2 是否满足 session 时长间隔
            if (sessionTimeOut) {
                ...
                try {
                    if (mSensorsDataInstance.isAutoTrackEnabled() && !mSensorsDataInstance.isAutoTrackEventTypeIgnored(SensorsDataAPI.AutoTrackEventType.APP_START)) {
                        if (firstStart) {
                            mFirstStart.commit(false);
                        }
                        JSONObject properties = new JSONObject();
                        properties.put("$resume_from_background", resumeFromBackground);
                        properties.put("$is_first_time", firstStart);
                        SensorsDataUtils.mergeJSONObject(activityProperty, properties);
                        //step 3 发送 $AppStart 事件
                        mSensorsDataInstance.track("$AppStart", properties); 
                    }
                } catch (Exception e) {
                    SALog.i(TAG, e);
                }
                ...
            }
        }
}

在 onActivityStarted 方法中的关键步骤如下:

step 1. 将 startActivityCount 自增(startActivityCount++),不同的是当多进程时通过数据库来存取 startActivityCount;

step 2. 判断本次启动是否满足 session 时长间隔筛选,如果满足则判断用户是否开启 App 启动事件采集;

step 3. 采集 App 启动事件。

2.3 App 退出事件

当用户退出 App 或将 App 退到后台超过 session 时间时则会采集 App 退出事件。神策 Android SDK 在 ActivityLifecycleCallbacks 中的 onActivityStopped(Activity activity) 回调中进行 App 退出事件的采集。

$AppEnd
@Override
public void onActivityStopped(Activity activity){
    startTimerCount--;
    /*
     * 如果当前是最后一个页面
     */
    if (startActivityCount <= 0) {
        generateAppEndData();
        // 发送一个延迟 session 时间的消息,用于采集 App 退出事件
        handler.sendMessageDelayed(generateMessage(true), sessionTime);
    }  
}
/**
 * 构建 Message 对象
 *
 * @param resetState 是否重置状态
 * @return Message
 */
private Message generateMessage(boolean resetState) {
    Message message = Message.obtain(handler);
    message.what = MESSAGE_END;
    Bundle bundle = new Bundle();
    bundle.putLong(APP_END_TIME, DbAdapter.getInstance().getAppPausedTime());
    bundle.putString(APP_END_DATA, DbAdapter.getInstance().getAppEndData());
    bundle.putBoolean(APP_RESET_STATE, resetState);
    message.setData(bundle);
    return message;
}
handler = new Handler(handlerThread.getLooper()) {
    @Override
    public void handleMessage(Message msg) {
        if (msg != null) {
            Bundle bundle = msg.getData();
            long endTime = bundle.getLong(APP_END_TIME);
            String endData = bundle.getString(APP_END_DATA);
            boolean resetState = bundle.getBoolean(APP_RESET_STATE);
            // 如果是正常的退到后台,需要重置标记位
            if (resetState) {
                resetState();
            else {// 如果是补发则需要添加打点间隔,防止 $AppEnd 在 AppCrash 事件序列之前
                endTime = endTime + TIME_INTERVAL;
            }
            // 采集 App 退出事件
            trackAppEnd(endTime, endData);
        }
    }
}

在 onActivityStopped(Activity activity) 回调中判断当前是否是最后一个页面,如果当前是最后一个页面,则触发延迟 session 时长的 App 退出事件的消息。如果用户在 session 时长内重新进入到前台,则会在 onActivityStarted(Activity activity) 回调中取消 Handler 的消息,即本次没有触发 App 退出事件。如果在后台 session 时长间隔内把 App 进程杀死,则会在下次启动时进行补发。

@Override
public void onActivityStarted(Activity activity) {
    try {
        activityProperty = AopUtil.buildTitleAndScreenName(activity);
        SensorsDataUtils.mergeJSONObject(activityProperty, endDataProperty);
        if (isMultiProcess) {
            startActivityCount = mDbAdapter.getActivityCount();
            mDbAdapter.commitActivityCount(++startActivityCount);
        else {
            ++startActivityCount;
        }
        // 如果是第一个页面
        if (startActivityCount == 1) {
            handler.removeMessages(MESSAGE_END);
            boolean sessionTimeOut = isSessionTimeOut();
            if (sessionTimeOut) {
                // 超时尝试补发 $AppEnd
                handler.sendMessage(generateMessage(false));
            }
        }
    } catch (Exception e) {
        SALog.printStackTrace(e);
    }
}

这样,当用户强杀应用后,下次启动时会通过补发 $AppEnd 事件,从而使整个用户路径完整。在 App 退出事件的采集会遇到很多特殊的因素,更多的细节处理可以参照我们开源项目的完整代码。

2.3 页面浏览($AppViewScreen)

2.3.1 Activity 页面浏览事件采集

神策 Android SDK 中页面浏览事件包含 Activity 和 Fragment 的页面浏览事件,对于 Activity 页面,通过 ActivityLifecycleCallbacks 中的 onActivityResumed(Activity activity) 回调进行监听,具体实现如下:

@Override
public void onActivityResumed(final Activity activity) {
    try {
        if (mSensorsDataInstance.isAutoTrackEnabled() && !mSensorsDataInstance.isActivityAutoTrackAppViewScreenIgnored(activity.getClass())
                && !mSensorsDataInstance.isAutoTrackEventTypeIgnored(SensorsDataAPI.AutoTrackEventType.APP_VIEW_SCREEN)) {
            JSONObject properties = new JSONObject();
            SensorsDataUtils.mergeJSONObject(activityProperty, properties);
            if (activity instanceof ScreenAutoTracker) {
                ScreenAutoTracker screenAutoTracker = (ScreenAutoTracker) activity;
                JSONObject otherProperties = screenAutoTracker.getTrackProperties();
                if (otherProperties != null) {
                    SensorsDataUtils.mergeJSONObject(otherProperties, properties);
                }
            }
            mSensorsDataInstance.trackViewScreen(SensorsDataUtils.getScreenUrl(activity), properties);
        }
    } catch (Exception e) {
        SALog.printStackTrace(e);
    }
}

在 onActivityResumed 中调用 trackViewScreen 上报 $AppViewScreen 事件,其中 ScreenAutoTracker 接口是用来让用户自定义页面浏览的 url 和一些自定义属性的,这里就不细讲了。

2.3.2 Fragment 页面浏览事件采集

Fragment 本身是没有生命周期的监听的,后期在 Andorid Support 25.1.0 和 AndroidX 中的 FragmentManager 增加了 FragmentLifecycleCallbacks 用来监听 Fragment 的生命周期,但由于神策 SDK 不依赖与 Support 库和 AndroidX 库,所以无法使用 FragmentLifecycleCallbacks 来监听 Fragment 的生命周期,为此神策通过全埋点插件在编译时期插入代码来实现 Fragment 的页面浏览采集,我们下面来看看具体的采集原理。

下图是反编译后的源码,这里使用 ASM 在编译期间通过在 Fragment 系统生命周期方法中分别插入了对应的神策 SDK 方法。

从中可以看到神策全埋点插件 Hook 的 Fragment 生命周期方法有:

  • onViewCreated()
  • onResume()
  • setUserVisibleHint()
  • onHiddenChanged()

下面分别介绍对每个生命周期插入的代码。

在 Fragment 的 onViewCreated 生命周期方法中插入方法 onFragmentViewCreated(Object object, View rootView, Bundle bundle),onFragmentViewCreated 方法中主要是遍历 View 并给 View 设置 TAG 标记,这里主要是为了点击事件所做的处理,跟页面浏览事件关系不大。

public static void onFragmentViewCreated(Object object, View rootView, Bundle bundle) {
    try {
        if (!isFragment(object)) {
            return;
        }
        //Fragment名称
        String fragmentName = object.getClass().getName();
        rootView.setTag(R.id.sensors_analytics_tag_view_fragment_name, fragmentName);
        if (rootView instanceof ViewGroup) {
            traverseView(fragmentName, (ViewGroup) rootView);
        }
    } catch (Exception e) {
        SALog.printStackTrace(e);
    }
}

在 Fragment 的 onResume 方法中通过插入 trackFragmentResume(Object object) 方法完成 Fragment 的页面浏览事件的采集。

public static void trackFragmentResume(Object object) {
    if (SensorsDataAPI.sharedInstance().isAutoTrackEventTypeIgnored(SensorsDataAPI.AutoTrackEventType.APP_VIEW_SCREEN)) {
        return;
    }
    if (!SensorsDataAPI.sharedInstance().isTrackFragmentAppViewScreenEnabled()) {
        return;
    }
    if (!isFragment(object)) {
        return;
    }
    try {
        Method getParentFragmentMethod = object.getClass().getMethod("getParentFragment");
        if (getParentFragmentMethod != null) {
            Object parentFragment = getParentFragmentMethod.invoke(object);
            if (parentFragment == null) {
                if (!fragmentIsHidden(object) && fragmentGetUserVisibleHint(object)) {
                    trackFragmentAppViewScreen(object);
                }
            else {
                if (!fragmentIsHidden(object) && fragmentGetUserVisibleHint(object) && !fragmentIsHidden(parentFragment) && fragmentGetUserVisibleHint(parentFragment)) {
                    trackFragmentAppViewScreen(object);
                }
            }
        }
    } catch (Exception e) {
        //ignored
    }
}

在 Fragment 的 onHiddenChanged 方法中通过插入 trackOnHiddenChanged(Object object, boolean hidden) 方法完成 Fragment 的页面浏览事件的采集。

public static void trackOnHiddenChanged(Object object, boolean hidden) {
       if (SensorsDataAPI.sharedInstance().isAutoTrackEventTypeIgnored(SensorsDataAPI.AutoTrackEventType.APP_VIEW_SCREEN)) {
           return;
       }
       if (!SensorsDataAPI.sharedInstance().isTrackFragmentAppViewScreenEnabled()) {
           return;
       }
       if (!isFragment(object)) {
           return;
       }
       Object parentFragment = null;
       try {
           Method getParentFragmentMethod = object.getClass().getMethod("getParentFragment");
           if (getParentFragmentMethod != null) {
               parentFragment = getParentFragmentMethod.invoke(object);
           }
       } catch (Exception e) {
           //ignored
       }
       if (parentFragment == null) {
           if (!hidden) {
               if (fragmentIsResumed(object)) {
                   if (fragmentGetUserVisibleHint(object)) {
                       trackFragmentAppViewScreen(object);
                   }
               }
           }
       else {
           if (!hidden && !fragmentIsHidden(parentFragment)) {
               if (fragmentIsResumed(object) && fragmentIsResumed(parentFragment)) {
                   if (fragmentGetUserVisibleHint(object) && fragmentGetUserVisibleHint(parentFragment)) {
                       trackFragmentAppViewScreen(object);
                   }
               }
           }
       }
   }

在 Fragment 的 setUserVisibleHint 方法中通过插入 trackFragmentSetUserVisibleHint(Object object, boolean isVisibleToUser) 方法完成 Fragment 的页面浏览事件的采集。

public static void trackFragmentSetUserVisibleHint(Object object, boolean isVisibleToUser) {
    if (SensorsDataAPI.sharedInstance().isAutoTrackEventTypeIgnored(SensorsDataAPI.AutoTrackEventType.APP_VIEW_SCREEN)) {
        return;
    }
    if (!SensorsDataAPI.sharedInstance().isTrackFragmentAppViewScreenEnabled()) {
        return;
    }
    if (!isFragment(object)) {
        return;
    }
    Object parentFragment = null;
    try {
        Method getParentFragmentMethod = object.getClass().getMethod("getParentFragment");
        if (getParentFragmentMethod != null) {
            parentFragment = getParentFragmentMethod.invoke(object);
        }
    } catch (Exception e) {
        //ignored
    }
    if (parentFragment == null) {
        if (isVisibleToUser) {
            if (fragmentIsResumed(object)) {
                if (!fragmentIsHidden(object)) {
                    trackFragmentAppViewScreen(object);
                }
            }
        }
    else {
        if (isVisibleToUser && fragmentGetUserVisibleHint(parentFragment)) {
            if (fragmentIsResumed(object) && fragmentIsResumed(parentFragment)) {
                if (!fragmentIsHidden(object) && !fragmentIsHidden(parentFragment)) {
                    trackFragmentAppViewScreen(object);
                }
            }
        }
    }
}

这几个方法是根据 Fragment 不同状态,通过一系列的判断,最终调用 trackFragmentAppViewScreen 方法来采集页面浏览事件。

以上,就对 Android 中常见的页面浏览的方式完成了采集。

三、总结

这篇文章主要是为了能够让大家对于 Sensors Data Android SDK 在用户路径采集方面有大致的了解,大家如果有什么好的想法,或者发现我们的这个项目有 bug,欢迎大家去 GitHub 上提 issues 或者直接 Pull Requests,我们会第一时间处理,也希望我们 SDK 能在大家的一起努力下,做得更加完善。
如果大家觉得我们这个项目还可以的话,点上一颗 star 吧。
Sensors Data Android SDK 项目地址:https://github.com/sensorsdata/sa-sdk-android

Sensors Data Android 埋点插件项目地址:https://github.com/sensorsdata/sa-sdk-android-plugin2

四、交流合作

本文著作权归神策数据开源社区所有。商业转载请联系我们获得授权;非商业转载请注明出处,并附上神策数据开源社区公众号二维码。
你还可以扫描二维码,加入社区交流群,与大家一同讨论。
也欢迎关注我们的公众号,博客更新尽在掌握。