关于前端:Android-SDK-启动退出方案演进

8次阅读

共计 9508 个字符,预计需要花费 24 分钟才能阅读完成。

1. 前言

在经营剖析中,DAU(Daily Active User)、UV(Unique Visitor)和用户应用时长是最常见的三个指标。对于一个 App 来说,三个指标的含意如下:
DAU:日沉闷用户数;
UV:独立访客;
用户应用时长:App 应用时长。
依据下面的形容可知,DAU 和 UV 的统计分析与 App 启动事件非亲非故,用户应用时长则须要通过 App 退出事件进行剖析。
在神策剖析中,统计上述三个指标的形式如下:
DAU:通过查问 App 每日启动的独立用户数来统计;
UV:通过查问 App 启动总的用户数来统计;
用户应用时长:通过查问 App 退出事件均匀时长来统计。
神策 Android SDK 曾经开源四年多了,这段时间内曾经陆续公布了近 160 个版本,从最后的仅反对代码埋点到当初反对全埋点、可视化全埋点等性能。其中,全埋点的启动事件和退出事件采集也通过了一直地演进,作者作为方案设计的参与者整顿了整个过程。上面逐渐向大家进行介绍,心愿对大家有所帮忙,更心愿失去一些领导意见。

2. 基本原理

因为在 Android 零碎中没有凋谢的 API 接口监听 App 的启动与退出,所以无奈间接从零碎层面准确地判断 App 的启动与退出。在 Android 中 App 的页面承载是基于 Activity,因而能够尝试从 Activity 启动个数的维度来判断 App 的启动和退出。总的来说,当监测到第一个 Activity 关上的时候标记为 App 启动;监测到最初一个页面敞开的时候标记为 App 退出。
在 Android 零碎中,能够通过在 Application 中注册 Application.ActivityLifecycleCallbacks 回调来统计 Activity 的切换,从而达到对 App 启动和退出事件的埋点采集,代码如下:
ActivityLifecycleCallbacks
public interface ActivityLifecycleCallbacks {

void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState);

void onActivityStarted(@NonNull Activity activity);

void onActivityResumed(@NonNull Activity activity);

void onActivityPaused(@NonNull Activity activity);

void onActivityStopped(@NonNull Activity activity);

void onActivityDestroyed(@NonNull Activity activity);

}

代码释义如下:
onActivityCreated:当 Activity 调用 super.onCreate() 时触发;
onActivityStarted:当 Activity 调用 super.onStart() 时触发;
onActivityResumed:当 Activity 调用 super.onResume() 时触发;
onActivityPaused:当 Activity 调用 super.onPause() 时触发;
onActivityStopped:当 Activity 调用 super.onStop() 时触发;
onActivityDestroyed:当 Activity 调用 super.onDestroy() 时触发。

3. 计划演进

3.1. 初期版本 1.0

3.1.1. 原理简介

在初期版本中,通过监测 Activity 的 onStart 和 onStop 生命周期函数来判断 App 启动和退出:
在 SDK 初始化时注册 Application.ActivityLifecycleCallbacks 监听,同时外部保护一个 Activity 的计数器;
当触发 onActivityStarted 回调时,如果 Activity 计数器个数为 0,则示意 App 关上的第一个页面(即 App 启动)。此时,触发 App 启动事件,同时计数器加 1;
当触发 onActivityStopped 回调时,示意一个 Activity 页面不可见,则 Activity 计数器减 1。如果 Activity 计数器个数为 0,则示意 App 最初一个页面不可见(即 App 退出)。此时,触发 App 退出事件。
外围流程如图 3-1 所示:

图 3-1 初期版本的流程图
3.1.2. 具体实现

针对初期版本的启动退出事件采集的外围逻辑,代码示例如下:
外围逻辑实现
public class MyApplication extends Application {

@Override
public void onCreate() {super.onCreate();
    registerActivityLifecycleCallbacks(new SensorsDataActivityLifecycleCallbacks());
}

static class SensorsDataActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
    private static final String TAG = "SA.LifecycleCallbacks";
    private int mActivityCount;

    @Override
    public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {}

    @Override
    public void onActivityStarted(@NonNull Activity activity) {if (mActivityCount == 0) {Log.d(TAG, "App 启动");
        }
        mActivityCount++;
    }

    @Override
    public void onActivityResumed(@NonNull Activity activity) {}

    @Override
    public void onActivityPaused(@NonNull Activity activity) {}

    @Override
    public void onActivityStopped(@NonNull Activity activity) {
        mActivityCount--;
        if (mActivityCount == 0) {Log.d(TAG, "App 退出");
        }
    }

    @Override
    public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}

    @Override
    public void onActivityDestroyed(@NonNull Activity activity) {}}

}

3.1.3. 优缺点

长处:
初期版本的实现较为简单;
较好地满足了晚期的客户需要,可能比拟精确地采集 App 启动事件和 App 退出事件。
毛病:
当 App 内呈现多过程页面时,如果进行跨过程的页面切换会触发 App 退出,造成用户行为序列中断,对用户行为序列剖析造成很大的困扰。

3.2. 中期版本 2.0

3.2.1. 原理简介

随着客户的增多,App 应用的场景越来越简单,初期版本曾经无奈满足一些场景的需要,影响剖析性能的体验。次要面临以下场景:
场景 1:App 前后台切换
在初期版本中,当 Activity 进入后盾后,计数器个数为 0。此时,会触发 App 退出事件,再次进入 App 时则会触发 App 启动事件。如果频繁进行前后台切换,则会采集很多 App 启动事件和退出事件,这种场景下的 App 启动事件对 DAU 的理论剖析没有意义。例如:用户在某个页面须要输出验证码,临时切换 App 到后盾查看短信中的验证码,再次关上 App 时会触发一对 App 启动和退出事件,这种场景下触发 App 启动和退出事件会将用户的逻辑行为序列中断,不利于理论的用户行为序列剖析。
场景 2:App 跳转到第三方页面再返回
在一些场景下,App 启动和退出会将用户的理论行为序列进行中断,不利于理论的用户行为序列剖析。例如:在一些商家服务的 App 中,用户实现订单后,点击领取按钮跳转到第三方的领取平台进行领取,领取实现后点击返回键从新进入 App 中的场景。初期版本的解决会触发一对 App 启动和退出事件,这种场景下的启动和退出就将用户的理论行为序列进行了中断。
场景 3:App 外部存在多过程的页面
当 App 内呈现一个 Activity 页面在独自的过程时,因为初期版本中的页面计数器是不反对多过程间共享,导致同一个 App 内的多过程页面跳转,会触发 App 启动和退出事件,造成谬误的 App 启动和退出事件采集。
场景 4:App 异样解体或强杀
App 中的异样解体或强杀是很常见的一种场景,然而在初期版本中是无奈解决的,导致在这种场景下会无奈采集到 App 的退出事件。

针对上述四种场景,咱们给出了上面的解决方案:
针对场景 1 和场景 2,咱们引入 Session 时长的概念。以默认的 Session 时长距离 30s 来说,对于一个 App 而言,当它的一个页面退出了,如果在 30s 之内没有新的页面关上,咱们就认为 App 进入后盾了,此时会触发 App 退出事件;当它的一个页面显示进去了,如果与上一个页面退出工夫的距离超过了 30s,咱们就认为这个应用程序从新处于前台了,此时会触发 App 启动事件;
针对场景 3,咱们引入一个标记位,用于标识是否触发 App 退出事件。当 App 退到后盾 30s 后,如果 App 退出事件标记位为 false,则触发 App 退出事件,同时重置 App 退出事件标记为 true。对于跨过程标记位的读取,咱们采纳 Android 零碎的 “ContentProvider + SharedPreferences” 的形式来进行跨过程的数据共享,保障不同的过程间读取的 App 退出事件标记位的准确性;
针对场景 4,对于 App 的异样解体,通过引入 Thread.UncaughtExceptionHandler 自定义异样处理器来监听,在自定义的 uncaughtException 办法中实现对 App 退出的采集。因为无奈捕获 Native 端的一些解体或强杀场景,所以采纳定时打点的性能进行 App 退出事件的信息保留。当下次关上 App 时,如果检测到 App 标记位为 false,则进行补发 App 退出事件,同时重置 App 退出事件标记为 true。
外围流程如图 3-2 所示:

图 3-2 中期版本的流程图
通过中期版本的流程图能够看到,针对 App 启动事件和退出事件的采集原理有较大的改变,显著比初期版本简单很多。同时,波及到更多的细节解决。例如:标记位存储失败、事件触发的工夫戳保留等。

3.2.2. 具体实现

针对中期版本采集的外围逻辑,代码实现如下:
外围逻辑实现

package com.sensorsdata.analytics.android.demo; import android.app.Activity;import android.app.Application;import android.content.ContentResolver;import android.content.ContentValues;import android.content.Context;import android.database.Cursor;import android.net.Uri;import android.os.Bundle;import android.os.CountDownTimer; import java.lang.ref.WeakReference; public class MyApplication extends Application {@Override public void onCreate() {super.onCreate(); registerActivityLifecycleCallbacks(new SensorsDataActivityLifecycleCallbacks()); } static class SensorsDataActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {private static SensorsDatabaseHelper mDatabaseHelper; private static CountDownTimer countDownTimer; private final static int SESSION_INTERVAL_TIME = 30 * 1000; @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {} @Override public void onActivityStarted(@NonNull Activity activity) {mDatabaseHelper.commitAppStart(true); double timeDiff = System.currentTimeMillis() – mDatabaseHelper.getAppPausedTime(); if (timeDiff > SESSION_INTERVAL_TIME) {if (!mDatabaseHelper.getAppEndEventState()) {trackAppEnd(); } } if (mDatabaseHelper.getAppEndEventState()) {mDatabaseHelper.commitAppEndEventState(false); trackAppStart();} } @Override public void onActivityResumed(@NonNull Activity activity) {// 此处要开启定时打点,用于记录 App 退出事件信息 timer(); } @Override public void onActivityPaused(@NonNull Activity activity) {countDownTimer.start(); cancelTimer(); mDatabaseHelper.commitAppPausedTime(System.currentTimeMillis()); } @Override public void onActivityStopped(@NonNull Activity activity) {} @Override public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} @Override public void onActivityDestroyed(@NonNull Activity activity) {} // 触发 App 启动事件 private void trackAppStart() {} // 触发 App 退出事件 private void trackAppEnd() {} // 定时打点记录 App 退出信息 private void timer() {} // 勾销定时打点 private void cancelTimer() {}}}

3.2.3. 优缺点

长处:
实现了简单场景中采集 App 启动和退出事件;
App 启动和退出事件的采集更加精确。
毛病:
为了解决更多的场景,实现原理上也更加简单;
对于频繁的存储标记位以及定时打点,这些业务解决都有很多的性能耗费。

3.3. 稳固版本 3.0

3.3.1. 原理简介

中期版本的外围目标是为了解决更简单的场景,用来补救初期版本的有余。随着用户的一直增多,对于 SDK 的性能提出了日益严格的要求,因而针对中期版本须要做一些优化。
中期版本中裸露的性能耗费点:
onActivityStarted 触发时,执行启动工夫戳 AppStartTime 的频繁读取、App 退出事件标记位的频繁读取、Session 时长的频繁判断;
onActivityResumed 触发时,执行打点定时器的从新开启;
onActivityPaused 触发时,执行打点定时器的敞开。
在一个 App 中,当波及到频繁的 Activity 切换时,会屡次执行下面的生命周期函数。外部频繁的存取时间戳,会造成较大的资源耗费。那如何能力防止频繁的读取呢?顺着这个问题的排查思路,最终采纳初期版本的计数器联合中期版本的跨过程通信来解决频繁的性能耗费。总的来说,就是“只在第一个页面时,才执行 App 启动的逻辑判断。只在最初一个页面时,才执行 App 退出的逻辑判断”。
外围流程如图 3-3 所示:

图 3-3 稳固版本的流程图
在稳固版本中 App 退出事件会存在一个尝试补发的流程:
如果本次启动工夫戳与上次退出工夫戳之差超过 session 时长时,则尝试补发;
如果读取本地缓存的 App 退出信息为空则不进行补发,如果不为空则补发。
失常流程下,App 退出事件触发后会革除本地打点 App 退出信息。如果 App 退出事件曾经触发,尝试补发时会读取到为空的 App 退出信息,就不会触发 App 退出事件。因而,该过程称之为尝试补发。通过稳固版本的流程图能够看到,相比拟中期版本流程上有了很大的优化,也更容易了解,性能耗费也比中期版本低很多。

3.3.2. 具体实现

针对稳固版本的外围逻辑,代码实现如下:
外围逻辑实现

public class MyApplication extends Application {@Override public void onCreate() {super.onCreate(); registerActivityLifecycleCallbacks(new SensorsDataActivityLifecycleCallbacks()); } static class SensorsDataActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {private static SensorsDatabaseHelper mDatabaseHelper; private int startActivityCount; private int startTimerCount; private final static int SESSION_INTERVAL_TIME = 30 1000; @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {} @Override public void onActivityStarted(@NonNull Activity activity) {// 读取 Activity 启动个数 startActivityCount = mDatabaseHelper.getActivityCount(); mDatabaseHelper.commitActivityCount(++startActivityCount); // 如果是第一个页面 if (startActivityCount == 1) {boolean sessionTimeOut = isSessionTimeOut(); if (sessionTimeOut) {// 如果超过 session 时长,尝试补发 trackAppEnd(); } if (sessionTimeOut) {// 触发 App 启动事件 trackAppStart(); } } // 如果是第一个页面 if (startTimerCount++ == 0) {/ 在启动的时候开启打点,退出时进行打点,在此处能够避免两点: 1. App 在 onResume 之前 Crash,导致只有启动没有退出; 2. 多过程的状况下只会开启一个打点器;/ timer();} } @Override public void onActivityResumed(@NonNull Activity activity) {} @Override public void onActivityPaused(@NonNull Activity activity) {} @Override public void onActivityStopped(@NonNull Activity activity) {// 进行计时器,针对跨过程的状况,要进行以后过程的打点器 startTimerCount–; if (startTimerCount == 0) {cancelTimer(); } startActivityCount = mDatabaseHelper.getActivityCount(); startActivityCount = startActivityCount > 0 ? –startActivityCount : 0; mDatabaseHelper.commitActivityCount(startActivityCount); if (startActivityCount <= 0) {// 如果是最初一个页面,则发动倒计时触发 App 退出 //TODO 倒计时的逻辑,倒计时完结后触发 App 退出 trackAppEnd(); } } @Override public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} @Override public void onActivityDestroyed(@NonNull Activity activity) {} // 触发 App 启动事件 private void trackAppStart() {} // 触发 App 退出事件 private void trackAppEnd() {} // 定时打点记录 App 退出信息 private void timer() {} // 勾销定时打点 private void cancelTimer() {} private boolean isSessionTimeOut() {long currentTime = Math.max(System.currentTimeMillis(), 946656000000L); return Math.abs(currentTime – mDatabaseHelper.getAppEndTime()) > SESSION_INTERVAL_TIME; } }}

下面示例代码只是对外围逻辑的大抵实现,更具体实现可参考神策 Android SDK。

3.3.3. 优缺点

长处:
升高了频繁的标记位存取,进步了效率;
App 启动和退出事件的采集更加精确。
毛病:
依然有局部业务在主线程中实现;
尽管对于计划进行了优化,然而实现原理还是较为简单。

4. 总结

神策 Android SDK 实现的 App 启动和退出的采集计划,是随着对理论场景的了解加深而一直产生扭转。目前应用的是稳固版本,通过大量客户的验证后曾经趋于稳定,对于 App 启动和退出的相干问题也比拟少。后续咱们对于稳固版本的优化是将 App 启动和退出的相干逻辑从主线程中剥离进去,进一步升高对主线程的影响。
Android 零碎中对于 App 启动和退出的准确检测是一个值得钻研的课题,欢送大家参加进来一起交换。

文章起源:神策技术社区

正文完
 0