神策分析 Android SDK 之用户路径采集

 

神策分析 Android SDK 之用户路径采集_第1张图片

一、用户路径

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

用户路径的作用

用户路径可以帮助使用者洞察用户看似平常的行为背后真正的思想,从而摆脱“大海捞针”式的用户行为数据查询。

使用者既可以有的放矢,验证自身假设,有针对性地解决问题;也可以日常监测用户的行为路径,及时发现用户的核心关注点及干扰选项,引导用户持续挖掘产品及服务的价值。

二、用户路径的采集

一个用户的完整行为路径会包含多个行为事件,以电商为例,用户从打开 App 到支付成功要经过首页浏览、搜索商品、加入购物车、提交订单、支付订单等过程。而在用户真实的选购过程是一个交缠反复的过程,例如提交订单后,用户可能会返回首页继续搜索商品,也可能去取消订单,每一个路径背后都有不同的动机。

通常一个完整的用户路径包含 App 启动、若干个页面浏览和 App 退出等事件。通过 Application.ActivityLifecycleCallbacks 监听,神策 Android SDK 实现 App 启动、页面浏览和App 退出三个行为事件。

(一)基础概念

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

  • session 机制

神策分析 Android SDK 之用户路径采集_第2张图片

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

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

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

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

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

神策分析 Android SDK 之用户路径采集_第3张图片

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

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

  • 补发机制

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

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

(二)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 启动事件。

(三)App 退出事件

当用户退出 App 或将 App 退到后台超过 session 时间时则为采集 App 退出事件。

神策 Android SDK 进行 App 退出事件的采集时,需要在 ActivityLifecycleCallbacks 中的 onActivityStopped(Activity activity) 回调中实施 。

$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 退出事件的采集会遇到很多特殊的因素,更多的细节处理可以参照我们开源项目的完整代码。

(四)页面浏览($AppViewScreen)

1. Activity 页面浏览事件采集

神策 Android SDK 中页面浏览事件包含 Activity 和 Fragment 的页面浏览事件,通过 ActivityLifecycleCallbacks 中的 onActivityResumed(Activity 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. Fragment 页面浏览事件采集

Fragment 本身没有监听生命周期, 后期 FragmentLifecycleCallbacks 监听 Fragment 的生命周期,是 Andorid Support 25.1.0 和 AndroidX 中增加  FragmentManager 起到的重要作用。

神策 SDK 不依赖 Support 库和 AndroidX 库,无法用 FragmentLifecycleCallbacks 监听 Fragment 的生命周期,为此神策通过全埋点插件在编译时期插入代码来实现 Fragment 的页面浏览采集,下面来看看具体的采集原理:

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

神策分析 Android SDK 之用户路径采集_第4张图片

从中可以看到神策全埋点插件 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 能在大家的一起努力下,做得更加完善。

​交流合作

本文著作权归「神策数据开源社区」所有,商业转载请联系我们获得授权;非商业转载请注明出处,并附上神策数据开源社区服务号二维码。

你还可以扫描二维码,加入社区交流群,与大家一同讨论。也欢迎关注我们的公众号,最新内容尽在掌握!

神策分析 Android SDK 之用户路径采集_第5张图片神策分析 Android SDK 之用户路径采集_第6张图片 

神策分析 Android SDK 之用户路径采集_第7张图片 神策分析 Android SDK 之用户路径采集_第8张图片

▼▼▼

更多精彩内容

  • 深入浅出:移动端数据采集埋点 SDK

  • 神策分析 iOS SDK 架构解析

  • 神策分析 Android SDK 架构解析

·END·

神策数据开源社区服务号

汇聚数据采集菁英,推动技术交流与发展

神策分析 Android SDK 之用户路径采集_第9张图片

 

点击链接 神策数据开源社区,进入开源社区官网~

你可能感兴趣的:(开源社区技术交流,大数据那些事儿,技术)