《Android全埋点解决方案》 —3.2 案例

举报
华章计算机 发表于 2019/12/19 11:55:19 2019/12/19
【摘要】 本节书摘来自华章计算机《Android全埋点解决方案》 一书中第3章,第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 事件的全埋点方案就算完成了。


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。