前言:
上一篇文章 主要介绍了埋点的基本概念以及几种埋点技术实现方式的原理和差异,本篇文章是自动化埋点技术探索的第一篇,主要介绍的是页面浏览事件、APP在前台还是在后台 这两类事件在无埋点技术的理论分析和实践
无埋点技术 - 处理页面浏览事件
AppViewScreen 事件,即页面浏览事件。在 Android 中,页面浏览,其实就是指切换不同的 Activity。生命周期对于一个 Activity的意义非凡,那什么是生命周期?生命周期通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过程 。通过对 Activity 的生命周期了解可知,页面的浏览事件其实就是指的Activity中的onResume()。因为Activity执行了onResume()就表示 已经出现在前台并开始活动,那么如何监听页面的浏览事件?因为埋点是基于可点击事件来执行内部逻辑,因此只有首先找到当前正在运行的Activity才可以进行后续的操作。
问题:
那有没有一种方案,可以对 Activity 的所有生命周期事件进行集中处理(或者叫监听)?
解决思路:
ActivityLifecycleCallbacks 是 Application 的一个内部接口,从 API 14 开始提供的。Application 通过此接口提供了一套回调方法,用于让开发者可以对 Activity 的所有生命周期事件进行集中处理,首先看一下ActivityLifecycleCallbacks 的内部源码
public interface ActivityLifecycleCallbacks {
void onActivityCreated(Activity activity, Bundle savedInstanceState);
void onActivityStarted(Activity activity);
void onActivityResumed(Activity activity);
void onActivityPaused(Activity activity);
void onActivityStopped(Activity activity);
void onActivitySaveInstanceState(Activity activity, Bundle outState);
void onActivityDestroyed(Activity activity);
}
ActivityLifecycleCallbacks 的简单使用如下:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
this.registerActivityLifecycleCallbacks(new StuActivityLifecycle());
}
private class StuActivityLifecycle implements ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
}
}
你可能会问,为什么Activity生命周期方法只要调用了,就会触发Application内部ActivityLifecycleCallbacks对应的方法 ?
解决问题最好的方式就是去探索Activity以及Application的系统源码。举例,以Activity的onCreate(),下面是它的源码:
@MainThread
@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
if (mLastNonConfigurationInstances != null) {
mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders);
}
if (mActivityInfo.parentActivityName != null) {
if (mActionBar == null) {
mEnableDefaultActionBarUp = true;
} else {
mActionBar.setDefaultDisplayHomeAsUpEnabled(true);
}
}
if (savedInstanceState != null) {
mAutoFillResetNeeded = savedInstanceState.getBoolean(AUTOFILL_RESET_NEEDED, false);
mLastAutofillId = savedInstanceState.getInt(LAST_AUTOFILL_ID,
View.LAST_APP_AUTOFILL_ID);
if (mAutoFillResetNeeded) {
getAutofillManager().onCreate(savedInstanceState);
}
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.fragments : null);
}
mFragments.dispatchCreate();
getApplication().dispatchActivityCreated(this, savedInstanceState);
if (mVoiceInteractor != null) {
mVoiceInteractor.attachActivity(this);
}
mRestoredFromBundle = savedInstanceState != null;
mCalled = true;
}
由于这里主要是分析Activity与Application之间的关系,那么,快速定位到这样一行代码: getApplication().dispatchActivityCreated(this, savedInstanceState);
可以看到,这里调用了Application的dispatchActivityCreated方法,dispatchActivityCreated源码如下:
void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) {
Object[] callbacks = collectActivityLifecycleCallbacks();
if (callbacks != null) {
for (int i=0; i
这里出现了collectActivityLifecycleCallbacks(),那Application内部的collectActivityLifecycleCallbacks这个方法又是做什么的,继续点进源码:
private Object[] collectActivityLifecycleCallbacks() {
Object[] callbacks = null;
synchronized (mActivityLifecycleCallbacks) {
if (mActivityLifecycleCallbacks.size() > 0) {
callbacks = mActivityLifecycleCallbacks.toArray();
}
}
return callbacks;
}
原来,这里做了一个赋值数组的操作,而且进行了同步代码块的操作,同步代码块的目的是为了解决并发操作可能造成的异常(因为监听的是所有的Activity)。源码出现的 mActivityLifecycleCallbacks,实际上是Application内部定义的一个私有成员变量:
private ArrayList mActivityLifecycleCallbacks =
new ArrayList();
大家都知道,ArrayList 是 java 集合框架中比较常用的数据结构。它继承自 AbstractList,实现了 List 接口。底层是基于数组实现容量大小动态变化,允许 null 的存在。那么,这里定义的ArrayList 又是如何进行添加和删除的?
这是Application内部的一段代码,一目了然:
public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {
synchronized (mActivityLifecycleCallbacks) {
mActivityLifecycleCallbacks.add(callback);
}
}
public void unregisterActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) {
synchronized (mActivityLifecycleCallbacks) {
mActivityLifecycleCallbacks.remove(callback);
}
}
综上所述,由于Activity每一个生命周期都对应 ActivityLifecycleCallbacks 接口中的一个方法,如上面提到的 onActivityCreated 回调是在 Activity 的 onCreate 方法中调用 getApplication().dispatchActivityCreated(this, savedInstanceState) 完成对 Activity 生命周期跟踪监听;另外,Application 也是可以 register 多个ActivityLifecycleCallbacks 这也是源码提到的
值得一提的是,ActivityLifecycleCallbacks 还有以下功能:
- 应用新开进程假重启处理(低内存回收、修改权限)
- 管理 Activity 页面栈
- 获取当前 Activity 页面
- 判断应用前后台
- 保存恢复状态值 savedInstanceState
- 页面分析统计埋点
解决方案:
那么,针对页面浏览事件在无埋点技术上的实现,就可以有以下步骤:
- 在应用程序自定义的 Application 对象的 onCreate() 方法中初始化埋点 SDK,并传入当前的 Application 对象。
- SDK 拿到 Application 对象之后,通过registerActivityLifecycleCallback 方法注册 Application.ActivityLifecycleCall-backs。这样 SDK 就能对 App 中所有的 Activity 的生命周期事件进行集中处理(监控)了。
- 在注册的 Application.ActivityLifecycleCallbacks 的 onActivityRe-
sumed 回调方法中,就可以拿到当前正在显示的 Activity 对象,接着调用
SDK 的相关接口,来触发页面浏览事件
无埋点技术 - 处理AppStart、AppEnd事件
所谓的AppStart、AppEnd事件实际上就是判断当前 App 是处于前台还是处于后台。而 Android 系统本身没有给 App 提供相关的接口来判断这些状态。
问题:
一般来说,判断当前 App 是处于前台还是后台首先必须要面对2个问题:
- App 有多个进程该如何判断?
- App 崩溃或者被强杀该如何判断?
解决思路:
ContentProvider (内容提供者)属于 Android四大组件之一,它的主要作用是进程间进行数据交互 & 共享,即跨进程通信。ContentProvider的底层是采用 Binder机制,设计成Binder机制的作用前面也提到了就是解决跨进程的数据共享问题。另外,Android系统也提供了 ContentProvider 的数据回调监听 ,即 ContentObserver,这样的设计,让跨进程间的数据通信更加完善。
对于 App 崩溃或者应用进程被强杀的场景,神策数据技术团队给出的解决办法是引入了Session 的概念。注意:此Session非彼Session,关于Session的官方概念,可以参考笔者的另外一篇文章:Cookie、Session、Token那点事儿 神策数据团队认为:对于App,当一个页面退出,如果 30s 之内没有打开新的页面,就认为 App 处于后台;当一个页面位于显示状态,如果与上一个页面退出时间的间隔超过 30s,就认为 App 重新处于前台了。
针对神策数据技术团队这种通过时间戳来计算判断的方式,可以解决大多数情况的数据采集,但还是会有个别采集不到的情况,具体的原因就是30s的设置。举个反例,如果设置25s、35s、40s 采集是否会更加精准?因为页面的内容不同、用户的客观条件(硬件、知识架构、大脑灵敏度)不同等等,可能更加科学实现的一种方式是针对内容的不同,动态去下发不同的设置时间,当然这是我个人的一些想法。还有一种可能,30s的设置是他们在统计了大量的用户行为、进行数据判断计算后得到的一个算术平均值,30s相较于其他值可能是较好的设置。
解决方案:
综上,针对App处于前台还是后台的事件在无埋点技术上的实现,就可以有以下步骤:
首先注册 ActivityLifecycleCallbacks 回调,来监听应用程序内所有 Activity 的生命周期。处理业务时涉及到标记位的保存以及跨进程间的数据通信, 采用ContentProvider + SharedPreferences 的方式实现进程间数据共享,同时注册 ContentObserver 来监听跨进程间的数据通信。
在页面退出的时候也就是 onPause(),启动一个倒计时 30s 定时器,如果 30s 之内没有新的页面显示,则触发 AppEnd 事件;如果有新的页面进来,存储一个标记位来标记新页面进来。需要注意的是,由于Activity 之间可能是跨进程的,所以标记位需要实现进程间的共享,也就是通过 ContentProvider + SharedPreferences 进行存储。
接着,通过 ContentObserver 监听到新页面进来的标记位改变,然后取消定时器。如果 30s 之内没有新的页面进来(如用户按 Home 键 / 返回键退出 App、App 崩溃、App 被强杀),我们会下次启动的时候补发一个 AppEnd 事件。
说完了应用在后台的埋点处理在谈谈应用在前台的埋点处理:
- 页面启动的时候也就是 onStart(),首先进行逻辑判断,判断与上个页面的退出时间间隔是否超过了 30s,如果没有超过 30s,则无需补发 AppEnd 事件,直接触发 AppScreen(页面浏览) 事件。接下来判断是否已触发 AppEnd 事件的标记位,如果标记位为 true,则触发 AppStart 事件,反之不触发;如果超过了 30s,逻辑判断是否已经触发了 AppEnd 事件,如果没有, 则先触发 AppEnd 事件,然后再触发 AppStart 和 AppScreen 事件。
文章部分内容选自:神策数据用户行为洞察研究院《安卓全埋点技术白皮书》,感谢技术分享!
如果这篇文章对您有开发or学习上的些许帮助,希望各位看官留下宝贵的star,谢谢。
Ps:著作权归作者所有,转载请注明作者, 商业转载请联系作者获得授权,非商业转载请注明出处(开头或结尾请添加转载出处,添加原文url地址),文章请勿滥用,也希望大家尊重笔者的劳动成果!