Android自动化埋点技术探索-1

前言:

上一篇文章 主要介绍了埋点的基本概念以及几种埋点技术实现方式的原理和差异,本篇文章是自动化埋点技术探索的第一篇,主要介绍的是页面浏览事件、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<callbacks.length; i++) {
                ((ActivityLifecycleCallbacks)callbacks[i]).onActivityCreated(activity,
                        savedInstanceState);
            }
        }
    }

这里出现了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<ActivityLifecycleCallbacks> mActivityLifecycleCallbacks =
            new ArrayList<ActivityLifecycleCallbacks>();

大家都知道,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.ActivityLifecycleCallbacksonActivityRe-
    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地址),文章请勿滥用,也希望大家尊重笔者的劳动成果!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容