Android全埋点-页面浏览事件

全埋点

全埋点也叫无埋点,自动埋点。是指预先自动收集用户的所有行为数据。然后就可以根据收集的数据从中筛选出所需的行为数据进行分析。

采集的事件类型

全埋点采集的事件主要包括以下4种类型:

AppStart事件

指应用程序的启动,它包括冷启动和热启动。

AppEnd事件

指应用程序退出,它包括了应用程序的正常退出、Home键进入后台、应用程序被强杀以及应用程序崩溃。

AppViewScreen事件

指应用程序页面浏览,对于Android应用程序来说就是指Activity或Fragment的切换。

AppClick事件

指应用程序控件的点击事件,也就是View的点击事件。

AppViewScreen全埋点方案

页面浏览事件就是指切换不同的Activity或Fragment,对于一个Activity来说它的onResume()方法执行,就代表该页面已经显示出来了,即该页面被浏览。所以我们只需要自动在onResume()方法中实现处理AppViewScreen事件相关的代码,即可解决AppViewScreen事件全埋点

Application.ActivityLifecycleCallbacks

ActivityLifecycleCallbacksApplication的一个内部接口,是从API14(Android4.0)开始提供的。Application类通过此接口提供了一系列的回调方法,用于让开发者可以对Activity的所有生命周期事件进行集中的处理。我们可以通过** Application**提供的registerActivityLifecycleCallbacks方法来注册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);
}

ActivityonResume()方法为例,如果我们注册了该接口,Android系统会先回调ActivityLifecycleCallbacksonActivityResumed(Activity activity)方法,然后再执行Activity本身的onResume()方法。(注意:一个Application是可以注册多个ActivityLifecycleCallbacks回调的)

除了使用该方法,我们可能还会想到定义一个BaseActivity,然后让其他的Activity继承这个BaseActivity。但是这样会有一个问题。如果我们在应用中集成了一些第三方库,并且我们用到了三方库中的Activity,此时我们是无法监控该页面的浏览事件的。因为我们无法让它去继承我们自定义的BaseActivity。所以我们采用Application.ActivityLifecycleCallbacks,不过该方案需要API14+,不过现在主流的机型都已经满足了。

代码实战

1.定义一个工具类SensorsDataAPI,用来采集相应的事件。

//全埋点工具类
class SensorsDataAPI private constructor(application: Application) {

    //设备ID
    private var mDeviceId: String = SensorsDataPrivate.getAndroidID(application.applicationContext)
    //设备信息集合,eg:厂商,版本,分辨率等
    private var mDeviceInfo: Map<String, Any> = SensorsDataPrivate.getDeviceInfo(application.applicationContext)

    companion object {
        //事件采集工具的版本号
        const val SDK_VERSION = "1.0.0"
        @Volatile
        var instance: SensorsDataAPI? = null
        private val TAG = this.javaClass.simpleName

        /**
         * 初始化埋点
         * @param application:Application
         */
        fun init(application: Application) = instance ?: synchronized(this) {
            //双检查带参数单例
            instance ?: SensorsDataAPI(application).also { instance = it }
        }
    }

    init {
        //注册registerActivityLifecycleCallbacks
        SensorsDataPrivate.registerActivityLifecycleCallbacks(application)
    }

    /**
     * 记录事件
     * @param eventName:事件名字
     * @param properties:自定义采集的属性
     */
    fun track(@NonNull eventName: String, @Nullable properties: JSONObject?) {
        try {
            val jsonObject = JSONObject()
            jsonObject.put("event", eventName)
            jsonObject.put("device_id", mDeviceId)
            val sendProperties = JSONObject(mDeviceInfo)
            if (properties != null) {
                SensorsDataPrivate.mergeJSONObject(properties, sendProperties)
            }
            jsonObject.put("properties", sendProperties)
            jsonObject.put("time", System.currentTimeMillis())
            Log.e(TAG, jsonObject.toString())
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * 不采集设置的页面的浏览事件
     * @param activity :指定的Activity
     */
    fun ignoreAutoTrackActivity(activity: Class<*>) {
        SensorsDataPrivate.ignoreAutoTrackActivity(activity)
    }

    /**
     * 恢复指定页面的浏览事件的采集
     * @param activity
     */
    fun removeIgnoredActivity(activity: Class<*>) {
        SensorsDataPrivate.removeIgnoredActivity(activity)
    }
    
}

SensorsDataPrivate

class SensorsDataPrivate {

    companion object {

        private val mDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.CHINA)
        private val mIgnoredActivities = ArrayList<String>()

        /**
         * 记录页面浏览事件
         * @param activity
         */
        fun trackAppViewScreen(activity: Activity) {
            try {
                if (mIgnoredActivities.contains(activity.javaClass.canonicalName)) {
                    return
                }
                val properties = JSONObject()
                properties.put("activity", activity.javaClass.canonicalName)
                SensorsDataAPI.instance?.track("AppViewScreen", properties)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

        /**
         * 获取androidID
         * @param context
         * @return String
         */
        @SuppressLint("HardwareIds")
        fun getAndroidID(context: Context): String {
            var androidID = ""
            try {
                androidID = Settings.Secure.getString(
                    context.contentResolver,
                    Settings.Secure.ANDROID_ID
                )
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return androidID
        }

        /**
         * 获取设备相关信息
         * @param context
         * @return Map
         */
        fun getDeviceInfo(context: Context): Map<String, Any> {
            val deviceInfo = HashMap<String, Any>()
            deviceInfo["lib"] = "Android"
            deviceInfo["lib_version"] = SensorsDataAPI.SDK_VERSION
            deviceInfo["os"] = "Android"
            deviceInfo["os_version"] = Build.VERSION.RELEASE ?: "UNKNOWN"
            deviceInfo["manufacturer"] = Build.MANUFACTURER ?: "UNKNOWN"
            deviceInfo["model"] = if (TextUtils.isEmpty(Build.MODEL)) "UNKNOWN" else Build.MODEL.trim()
            try {
                val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
                deviceInfo["app_version"] = packageInfo.versionName
                deviceInfo["app_name"] = context.resources.getString(packageInfo.applicationInfo.labelRes)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            val displayMetrics = context.resources.displayMetrics
            deviceInfo["screen_width"] = displayMetrics.widthPixels
            deviceInfo["screen_height"] = displayMetrics.heightPixels
            //返回值有序只读的map
            return Collections.unmodifiableMap(deviceInfo)
        }

        /**
         * 注册Application.registerActivityLifecycleCallbacks
         * @param application
         */
        fun registerActivityLifecycleCallbacks(application: Application) {
            application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
                override fun onActivityPaused(activity: Activity?) { }

                override fun onActivityResumed(activity: Activity?) {
                    if (activity != null) {
                        trackAppViewScreen(activity)
                    }
                }

                override fun onActivityStarted(activity: Activity?) { }

                override fun onActivityDestroyed(activity: Activity?) {}

                override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { }

                override fun onActivityStopped(activity: Activity?) { }

                override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { }

            })
        }

        /**
         * 将自定义属性合并到公共属性中
         * @param source 自定义的
         * @param dest : 公共的
         */
        fun mergeJSONObject(source: JSONObject, dest: JSONObject) {
            val propertiesIterator = source.keys()
            while (propertiesIterator.hasNext()) {
                val key = propertiesIterator.next()
                val value = source.get(key)
                if (value is Date) {
                    synchronized(mDateFormat) {
                        dest.put(key, mDateFormat.format(value))
                    }
                } else {
                    dest.put(key, value)
                }
            }
        }

        /**
         * 忽略记录指定的Activity
         * @param activity
         */
        fun ignoreAutoTrackActivity(activity: Class<*>) {
            mIgnoredActivities.add(activity.canonicalName)
        }

        /**
         * 移除忽略的Activity
         * @param activity
         */
        fun removeIgnoredActivity(activity: Class<*>) {
            if (mIgnoredActivities.contains(activity.canonicalName)) {
                mIgnoredActivities.remove(activity.canonicalName)
            }
        }
    }
}

2.在Application中初始化埋点工具类。

SensorsDataAPI.init(this)

注意:别忘记在AndroidManifest.xml中指定Application

通过上面的代码设置,我们在启动页面的时候就会自动采集设置的数据,这里只是以日志的形式把采集的数据打印出来了。

Log.e(TAG, jsonObject.toString())
{
    "event": "AppViewScreen",
    "device_id": "9e3077550b446ff0",
    "properties": {
        "app_name": "My Application",
        "screen_width": 1080,
        "screen_height": 1794,
        "lib": "Android",
        "os": "Android",
        "app_version": "1.0",
        "os_version": "9",
        "model": "Android SDK built for x86",
        "lib_version": "1.0.0",
        "manufacturer": "Google",
        "activity": "com.example.myapplication.MainActivity"
    },
    "time": 1563692439349
}

SensorsDataAPI中还有两个方法:ignoreAutoTrackActivityremoveIgnoredActivity在上面的示例中没有用到。通过这两个方法,可以忽略和恢复事件的采集。

比如:Android6.0+有些权限需要动态申请,不管用户选择了允许还是禁止,系统都会再次回调当前ActivityonResume()方法。这样就会导致再一次触发页面浏览事件,所以,在需要申请权限的页面需要把当前的Activity忽略掉,等到权限申请结束后再恢复采集。

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    SensorsDataAPI.instance?.ignoreAutoTrackActivity(MainActivity::class.java)
    when (requestCode) {
        //权限申请结果处理 
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

override fun onStop() {
    super.onStop()
    SensorsDataAPI.instance?.removeIgnoredActivity(MainActivity::class.java)
}

页面浏览事件相对于其他事件的埋点是比较容易的,主要是通过ApplicationActivityLifecycleCallbacks来监控Activity的生命周期相关方法的回调,从而来实现埋点的逻辑。

Kotlin实战