Android 全埋点解决方案 (12):AppStartAppEnd 全埋点方案 3.2

阅读数:8 2019 年 11 月 30 日 22:55

Android全埋点解决方案(12):$AppStart、$AppEnd全埋点方案 3.2

(案例)

内容简介
这是一本实战为导向的、翔实的 Android 全埋点技术与解决方案手册,是国内知名大数据公司神策数据在该领域多年实践经验的总结。由神策数据合肥研发中心负责人亲自执笔,他在 Android 领域有近 10 年研发经验,开发和维护着知名的商用开源 Android & iOS 数据埋点 SDK。
本书详细阐述了 Android 全埋点的 8 种解决方案,涵盖各种场景,从 0 到 1 详解技术原理和实现步骤,并且提供完整的源代码,各级研发工程师均可借此实现全埋点数据采集,为市场解开全埋点的神秘面纱。
8 种 Android 全埋点解决方案包括:
AppClick 全埋点方案 1:代理 View.OnClickListener、
AppClick 全埋点方案 2:代理 Window.Callback
AppClick 全埋点方案 3:代理 View.AccessibilityDelegate
AppClick 全埋点方案 4:透明层
AppClick 全埋点方案 5:AspectJ
AppClick 全埋点方案 6:ASM
AppClick 全埋点方案 7:Javassist
AppClick 全埋点方案 8:AST

针对上面介绍的原理,接下来我们将详细介绍如何实现 AppStartAppEnd 事件的全埋点方案。

完整的项目源码可以参考: https://github.com/wangzhzh/AutoTrackAppStartAppEnd

第 1 步:新建一个项目(Project)

在新建的项目中,会自动包含一个主 module,即:app。

第 2 步:创建 sdk module

新建一个 Android Library module,名称叫 sdk,这个模块就是我们的埋点 SDK 模块。

第 3 步:添加依赖关系

app module 需要依赖 sdk module。可以通过修改 app/build.gradle 文件,在其 dependencies 节点中添加依赖关系:

复制代码
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.sensorsdata.analytics.android.app.startend"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation project(':sdk')
}

第 4 步:编写埋点 SDK

在 sdk module 中我们新建一个埋点 SDK 的主类,即 SensorsDataAPI.java,完整的源码可以参考如下:

复制代码
package com.sensorsdata.analytics.android.sdk;
import android.app.Application;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.json.JSONObject;
import java.util.Map;
/**
* Created by 王灼洲 on 2018/7/22
*/
@Keep
public class SensorsDataAPI {
private final String TAG = this.getClass().getSimpleName();
public static final String SDK_VERSION = "1.0.0";
private static SensorsDataAPI INSTANCE;
private static final Object mLock = new Object();
private static Map<String, Object> mDeviceInfo;
private String mDeviceId;
@Keep
@SuppressWarnings("UnusedReturnValue")
public static SensorsDataAPI init(Application application) {
synchronized (mLock) {
if (null == INSTANCE) {
INSTANCE = new SensorsDataAPI(application);
}
return INSTANCE;
}
}
@Keep
public static SensorsDataAPI getInstance() {
return INSTANCE;
}
private SensorsDataAPI(Application application) {
mDeviceId = SensorsDataPrivate.getAndroidID(application.getApplicationContext());
mDeviceInfo = SensorsDataPrivate.getDeviceInfo(application.getApplicationContext());
SensorsDataPrivate.registerActivityLifecycleCallbacks(application);
SensorsDataPrivate.registerActivityStateObserver(application);
}
/**
* track 事件
*
* @param eventName String 事件名称
* @param properties JSONObject 事件自定义属性
*/
public void track(@NonNull String eventName, @Nullable JSONObject properties) {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("event", eventName);
jsonObject.put("device_id", mDeviceId);
JSONObject sendProperties = new JSONObject(mDeviceInfo);
if (properties != null) {
SensorsDataPrivate.mergeJSONObject(properties, sendProperties);
}
jsonObject.put("properties", sendProperties);
jsonObject.put("time", System.currentTimeMillis());
Log.i(TAG, SensorsDataPrivate.formatJson(jsonObject.toString()));
} catch (Exception e) {
e.printStackTrace();
}
}
}

目前这个主类比较简单,主要包含如下几个方法。

  • init(Application application)

这是一个静态方法,是埋点 SDK 的初始化函数,它有一个 Application 类型的参数,内部实现使用到了单例设计模式,然后调用私有构造函数初始化埋点 SDK。app module 就是调用这个方法来初始化我们埋点 SDK 的。

  • getInstance()

这也是一个静态方法,通过该方法可以获取埋点 SDK 的实例对象。

  • SensorsDataAPI(Application application)

私有的构造函数,也是埋点 SDK 真正的初始化逻辑。在其方法内部通过调用 SDK 的内部私有类 SensorsDataPrivate 中的方法来注册 ActivityLifecycleCallbacks,并给 Content-Provider 注册一个 ContentObserver。

  • track(@NonNull final String eventName, @Nullable JSONObject properties)

对外公开的 track 事件接口。通过调用该方法可以触发事件,第一个参数 eventName 代表事件的名称,第二个参数 properties 代表事件的属性。本书为了简化,触发事件仅通过 Log.i 打印了事件的 JSON 信息。

关于 SensorsDataPrivate 类中的 getAndroidID(Context context)、getDeviceInfo(Context context)、mergeJSONObject(final JSONObject source, JSONObject dest)、formatJson(String jsonStr) 等方法实现可以参考工程的源码。

第 5 步:注册 ActivityLifecycleCallbacks 回调

我们是通过调用埋点 SDK 的内部私有类 SensorsDataPrivate 的 registerActivityLifecycleCallbacks(Application application) 方法来注册 ActivityLifecycleCallbacks 的。

复制代码
/**
* 注册 Application.ActivityLifecycleCallbacks
*
* @param application Application
*/
@TargetApi(14)
public static void registerActivityLifecycleCallbacks(Application application) {
mDatabaseHelper = new DatabaseHelper(application.getApplicationContext(), application.getPackageName());
countDownTimer = new CountDownTimer(SESSION_INTERVAL_TIME, 10 * 1000) {
@Override
public void onTick(long l) {
}
@Override
public void onFinish() {
trackAppEnd(mCurrentActivity.get());
}
};
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycle-Callbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
mDatabaseHelper.commitAppStart(true);
double timeDiff = System.currentTimeMillis() - mDatabaseHelper.getAppPausedTime();
if (timeDiff > 30 * 1000) {
if (!mDatabaseHelper.getAppEndEventState()) {
trackAppEnd(activity);
}
}
if (mDatabaseHelper.getAppEndEventState()) {
mDatabaseHelper.commitAppEndEventState(false);
trackAppStart(activity);
}
}
@Override
public void onActivityResumed(Activity activity) {
trackAppViewScreen(activity);
}
@Override
public void onActivityPaused(Activity activity) {
mCurrentActivity = new WeakReference<>(activity);
countDownTimer.start();
mDatabaseHelper.commitAppPausedTime(System.currentTimeMillis());
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
}

首先初始化一个 SensorsDatabaseHelper 对象,这个主要是用来操作 ContentProvider 的,然后再初始化一个 30s 的计时器 CountDownTimer 对象,当计时器 finish 的时候,会触发 $AppEnd 事件。最后注册 Application.ActivityLifecycleCallbacks 回调。

在 Application.ActivityLifecycleCallbacks 的 onActivityStarted(Activity activity) 回调方法中,首先修改 AppStart 的标记位,这样之前注册的 ContentObserver 就能收到通知并取消掉 CountDownTimer 计时器。然后判断一下当前页面与上个页面退出时间的间隔是否超出了 30s,如果超出了 30s,并且没有触发过 AppEndAppEnd 事件。如果触发了 AppEndSessionAppStart 事件。

在 onActivityResumed(Activity activity) 回调方法中,会直接触发 $AppViewScreen 页面浏览事件。

在 onActivityPaused(Activity activity) 回调方法中,启动 CountDownTimer 计时器,并且保存当前页面退出时的时间戳。

第 6 步:定义 SensorsDatabaseHelper

复制代码
package com.sensorsdata.analytics.android.sdk;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
/*public*/ class SensorsDatabaseHelper {
private static final String SensorsDataContentProvider = ".SensorsData-ContentProvider/";
private ContentResolver mContentResolver;
private Uri mAppStart;
private Uri mAppEndState;
private Uri mAppPausedTime;
public static final String APP_STARTED = "$app_started";
public static final String APP_END_STATE = "$app_end_state";
public static final String APP_PAUSED_TIME = "$app_paused_time";
SensorsDatabaseHelper(Context context, String packageName) {
mContentResolver = context.getContentResolver();
mAppStart = Uri.parse("content://" + packageName + SensorsDataContentProvider + SensorsDataTable.APP_STARTED.getName());
mAppEndState = Uri.parse("content://" + packageName + SensorsDataContentProvider + SensorsDataTable.APP_END_STATE.getName());
mAppPausedTime = Uri.parse("content://" + packageName + SensorsDataContentProvider + SensorsDataTable.APP_PAUSED_TIME.getName());
}
/**
* Add the AppStart state to the SharedPreferences
*
* @param appStart the ActivityState
*/
public void commitAppStart(boolean appStart) {
ContentValues contentValues = new ContentValues();
contentValues.put(APP_STARTED, appStart);
mContentResolver.insert(mAppStart, contentValues);
}
/**
* Add the Activity paused time to the SharedPreferences
*
* @param pausedTime Activity paused time
*/
public void commitAppPausedTime(long pausedTime) {
ContentValues contentValues = new ContentValues();
contentValues.put(APP_PAUSED_TIME, pausedTime);
mContentResolver.insert(mAppPausedTime, contentValues);
}
/**
* Return the time of Activity paused
*
* @return Activity paused time
*/
public long getAppPausedTime() {
long pausedTime = 0;
Cursor cursor = mContentResolver.query(mAppPausedTime, new String[]{APP_
PAUSED_TIME}, null, null, null);
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToNext()) {
pausedTime = cursor.getLong(0);
}
}
if (cursor != null) {
cursor.close();
}
return pausedTime;
}
/**
* Add the Activity End to the SharedPreferences
*
* @param appEndState the Activity end state
*/
public void commitAppEndEventState(boolean appEndState) {
ContentValues contentValues = new ContentValues();
contentValues.put(APP_END_STATE, appEndState);
mContentResolver.insert(mAppEndState, contentValues);
}
/**
* Return the state of $AppEnd
*
* @return Activity End state
*/
public boolean getAppEndEventState() {
boolean state = true;
Cursor cursor = mContentResolver.query(mAppEndState, new String[]{APP_
END_STATE}, null, null, null);
if (cursor != null && cursor.getCount() > 0) {
while (cursor.moveToNext()) {
state = cursor.getInt(0) > 0;
}
}
if (cursor != null) {
cursor.close();
}
return state;
}
public Uri getAppStartUri() {
return mAppStart;
}
}

这个工具类主要是用来操作 ContentProvider 用来保存相关的数据和标记位。

第 7 步:定义 SensorsDataContentProvider

复制代码
package com.sensorsdata.analytics.android.sdk;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class SensorsDataContentProvider extends ContentProvider {
private final static int APP_START = 1;
private final static int APP_END_STATE = 2;
private final static int APP_PAUSED_TIME = 3;
private static SharedPreferences sharedPreferences;
private static SharedPreferences.Editor mEditor;
private static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private ContentResolver mContentResolver;
@Override
public boolean onCreate() {
if (getContext() != null) {
String packName = getContext().getPackageName();
uriMatcher.addURI(packName + ".SensorsDataContentProvider", Sensors-DataTable.APP_STARTED.getName(), APP_START);
uriMatcher.addURI(packName + ".SensorsDataContentProvider", Sensors-DataTable.APP_END_STATE.getName(), APP_END_STATE);
uriMatcher.addURI(packName + ".SensorsDataContentProvider", Sensors-DataTable.APP_PAUSED_TIME.getName(), APP_PAUSED_TIME);
sharedPreferences = getContext().getSharedPreferences("com.sensorsdata. analytics.android.sdk.SensorsDataAPI", Context.MODE_PRIVATE);
mEditor = sharedPreferences.edit();
mEditor.apply();
mContentResolver = getContext().getContentResolver();
}
return false;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
if (contentValues == null) {
return uri;
}
int code = uriMatcher.match(uri);
switch (code) {
case APP_START:
boolean appStart = contentValues.getAsBoolean(SensorsDatabaseHelper.APP_STARTED);
mEditor.putBoolean(SensorsDatabaseHelper.APP_STARTED, appStart);
mContentResolver.notifyChange(uri, null);
break;
case APP_END_STATE:
boolean appEnd = contentValues.getAsBoolean(SensorsDatabaseHelper.APP_END_STATE);
mEditor.putBoolean(SensorsDatabaseHelper.APP_END_STATE, appEnd);
break;
case APP_PAUSED_TIME:
long pausedTime = contentValues.getAsLong(SensorsDatabaseHelper.APP_PAUSED_TIME);
mEditor.putLong(SensorsDatabaseHelper.APP_PAUSED_TIME, pausedTime);
break;
}
mEditor.commit();
return uri;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
int code = uriMatcher.match(uri);
MatrixCursor matrixCursor = null;
switch (code) {
case APP_START:
int appStart = sharedPreferences.getBoolean(SensorsDatabaseHelper.APP_STARTED, true) ? 1 : 0;
matrixCursor = new MatrixCursor(new String[]{SensorsDatabase-Helper.APP_STARTED});
matrixCursor.addRow(new Object[]{appStart});
break;
case APP_END_STATE:
int appEnd = sharedPreferences.getBoolean(SensorsDatabaseHelper.APP_END_STATE, true) ? 1 : 0;
matrixCursor = new MatrixCursor(new String[]{SensorsDatabase-Helper.APP_END_STATE});
matrixCursor.addRow(new Object[]{appEnd});
break;
case APP_PAUSED_TIME:
long pausedTime = sharedPreferences.getLong(SensorsDatabase-Helper.APP_PAUSED_TIME, 0);
matrixCursor = new MatrixCursor(new String[]{SensorsDatabase-Helper.APP_PAUSED_TIME});
matrixCursor.addRow(new Object[]{pausedTime});
break;
}
return matrixCursor;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
return 0;
}
}

实现了一个 ContentProvider,通过操作 SharedPreferences 来保存数据,可以解决多进程间共享数据的问题,同时也能做到快速读写,提升效率。

SensorsDataTable 的定义如下:

复制代码
package com.sensorsdata.analytics.android.sdk;
/*public*/ enum SensorsDataTable {
APP_STARTED("app_started"),
APP_PAUSED_TIME("app_paused_time"),
APP_END_STATE("app_end_state");
SensorsDataTable(String name) {
this.name = name;
}
public String getName() {
return name;
}
private String name;
}

第 8 步:初始化埋点 SDK

需要在应用程序自定义的 Application (比如叫 MyApplication)类中初始化 SDK,一般建议在 onCreate() 方法中进行初始化。

复制代码
package com.sensorsdata.analytics.android.app;
import android.app.Application;
import com.sensorsdata.analytics.android.sdk.SensorsDataAPI;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initSensorsDataAPI(this);
}
/**
* 初始化埋点 SDK
*
* @param application Application
*/
private void initSensorsDataAPI(Application application) {
SensorsDataAPI.init(application);
}
}

第 9 步:声明自定义的 Application

以上面定义的 MyApplication 为例,需要在 AndroidManifest.xml 文件的 application 节点中声明 MyApplication。

复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sensorsdata.analytics.android.app">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

至此,AppStartAppEnd 事件的全埋点方案就算完成了。

Android全埋点解决方案(12):$AppStart、$AppEnd全埋点方案 3.2

购书地址 https://item.jd.com/12574672.html?dist=jd

评论

发布