《Android全埋点解决方案》 —3.2 案例
3.2 案例
针对上面介绍的原理,接下来我们将详细介绍如何实现$AppStart和$AppEnd 事件的全埋点方案。
完整的项目源码可以参考: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,并且没有触发过$AppEnd 事件(应用程序发生崩溃或者应用程序被强杀等场景),则补发$AppEnd 事件。如果触发了$AppEnd 事件,说明是一个新的 Session 开始了,需要触发$AppStart 事件。
在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>
至此,$AppStart 和$AppEnd 事件的全埋点方案就算完成了。
- 点赞
- 收藏
- 关注作者
评论(0)