《Android全埋点解决方案》 —2.3 案例
2.3 案例
下面我们会详细介绍$AppViewScreen事件全埋点方案的实现步骤。
完整的项目源码可以参考以下网址:
https://github.com/wangzhzh/AutoTrackAppViewScreen。
第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.appviewscreen"
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')
}
也可以通过 Project Structure 给模块添加依赖关系,在此不再详细描述。
第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);
}
/**
* 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()
它也是一个静态方法,app 通过该方法可以获取埋点 SDK 的实例对象。
SensorsDataAPI(Application application)
私有的构造函数,也是埋点 SDK 真正的初始化逻辑。在其方法内部通过调用 SDK 的内部私有类SensorsDataPrivate中的方法来注册ActivityLifecycleCallbacks。
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)方法实现可以参考如下源码:
package com.sensorsdata.analytics.android.sdk;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActionBar;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.Keep;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/*public*/ class SensorsDataPrivate {
private static List<Integer> mIgnoredActivities;
static {
mIgnoredActivities = new ArrayList<>();
}
private static final SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"
+ ".SSS", Locale.CHINA);
public static void ignoreAutoTrackActivity(Class<?> activity) {
if (activity == null) {
return;
}
mIgnoredActivities.add(activity.hashCode());
}
public static void removeIgnoredActivity(Class<?> activity) {
if (activity == null) {
return;
}
if (mIgnoredActivities.contains(activity.hashCode())) {
mIgnoredActivities.remove(activity.hashCode());
}
}
public static void mergeJSONObject(final JSONObject source, JSONObject dest)
throws JSONException {
Iterator<String> superPropertiesIterator = source.keys();
while (superPropertiesIterator.hasNext()) {
String key = superPropertiesIterator.next();
Object value = source.get(key);
if (value instanceof Date) {
synchronized (mDateFormat) {
dest.put(key, mDateFormat.format((Date) value));
}
} else {
dest.put(key, value);
}
}
}
@TargetApi(11)
private static String getToolbarTitle(Activity activity) {
try {
ActionBar actionBar = activity.getActionBar();
if (actionBar != null) {
if (!TextUtils.isEmpty(actionBar.getTitle())) {
return actionBar.getTitle().toString();
}
} else {
if (activity instanceof AppCompatActivity) {
AppCompatActivity appCompatActivity = (AppCompatActivity) activity;
android.support.v7.app.ActionBar supportActionBar = appCompat-Activity.getSupportActionBar();
if (supportActionBar != null) {
if (!TextUtils.isEmpty(supportActionBar.getTitle())) {
return supportActionBar.getTitle().toString();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取 Activity 的 title
*
* @param activity Activity
* @return String 当前页面 title
*/
@SuppressWarnings("all")
private static String getActivityTitle(Activity activity) {
String activityTitle = null;
if (activity == null) {
return null;
}
try {
activityTitle = activity.getTitle().toString();
if (Build.VERSION.SDK_INT >= 11) {
String toolbarTitle = getToolbarTitle(activity);
if (!TextUtils.isEmpty(toolbarTitle)) {
activityTitle = toolbarTitle;
}
}
if (TextUtils.isEmpty(activityTitle)) {
PackageManager packageManager = activity.getPackageManager();
if (packageManager != null) {
ActivityInfo activityInfo = packageManager.getActivityInfo (activity.getComponentName(), 0);
if (activityInfo != null) {
activityTitle = activityInfo.loadLabel(packageManager).toString();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return activityTitle;
}
/**
* Track 页面浏览事件
*
* @param activity Activity
*/
@Keep
private static void trackAppViewScreen(Activity activity) {
try {
if (activity == null) {
return;
}
if (mIgnoredActivities.contains(activity.getClass().hashCode())) {
return;
}
JSONObject properties = new JSONObject();
properties.put("$activity", activity.getClass().getCanonicalName());
properties.put("title", getActivityTitle(activity));
SensorsDataAPI.getInstance().track("$AppViewScreen", properties);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 注册 Application.ActivityLifecycleCallbacks
*
* @param application Application
*/
@TargetApi(14)
public static void registerActivityLifecycleCallbacks(Application application) {
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
trackAppViewScreen(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) {
}
});
}
public static Map<String, Object> getDeviceInfo(Context context) {
final Map<String, Object> deviceInfo = new HashMap<>();
{
deviceInfo.put("$lib", "Android");
deviceInfo.put("$lib_version", SensorsDataAPI.SDK_VERSION);
deviceInfo.put("$os", "Android");
deviceInfo.put("$os_version",
Build.VERSION.RELEASE == null ? "UNKNOWN" : Build.VERSION.RELEASE);
deviceInfo
.put("$manufacturer", Build.MANUFACTURER == null ? "UNKNOWN": Build.MANUFACTURER);
if (TextUtils.isEmpty(Build.MODEL)) {
deviceInfo.put("$model", "UNKNOWN");
} else {
deviceInfo.put("$model", Build.MODEL.trim());
}
try {
final PackageManager manager = context.getPackageManager();
final PackageInfo packageInfo = manager.getPackageInfo(context.getPackageName(), 0);
deviceInfo.put("$app_version", packageInfo.versionName);
int labelRes = packageInfo.applicationInfo.labelRes;
deviceInfo.put("$app_name", context.getResources().getString(labelRes));
} catch (final Exception e) {
e.printStackTrace();
}
final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
deviceInfo.put("$screen_height", displayMetrics.heightPixels);
deviceInfo.put("$screen_width", displayMetrics.widthPixels);
return Collections.unmodifiableMap(deviceInfo);
}
}
/**
* 获取 Android ID
*
* @param mContext Context
* @return String
*/
@SuppressLint("HardwareIds")
public static String getAndroidID(Context mContext) {
String androidID = "";
try {
androidID = Settings.Secure.getString(mContext.getContentResolver(), Settings.Secure.ANDROID_ID);
} catch (Exception e) {
e.printStackTrace();
}
return androidID;
}
private static void addIndentBlank(StringBuilder sb, int indent) {
try {
for (int i = 0; i < indent; i++) {
sb.append('\t');
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static String formatJson(String jsonStr) {
try {
if (null == jsonStr || "".equals(jsonStr)) {
return "";
}
StringBuilder sb = new StringBuilder();
char last;
char current = '\0';
int indent = 0;
boolean isInQuotationMarks = false;
for (int i = 0; i < jsonStr.length(); i++) {
last = current;
current = jsonStr.charAt(i);
switch (current) {
case '"':
if (last != '\\') {
isInQuotationMarks = !isInQuotationMarks;
}
sb.append(current);
break;
case '{':
case '[':
sb.append(current);
if (!isInQuotationMarks) {
sb.append('\n');
indent++;
addIndentBlank(sb, indent);
}
break;
case '}':
case ']':
if (!isInQuotationMarks) {
sb.append('\n');
indent--;
addIndentBlank(sb, indent);
}
sb.append(current);
break;
case ',':
sb.append(current);
if (last != '\\' && !isInQuotationMarks) {
sb.append('\n');
addIndentBlank(sb, indent);
}
break;
default:
sb.append(current);
}
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
}
第5步:注册 ActivityLifecycleCallbacks回调
我们是通过调用 SDK 的内部私有类SensorsDataPrivate的registerActivityLifecycleCallbacks(Application application)方法来注册ActivityLifecycleCallbacks的。
/**
* 注册 Application.ActivityLifecycleCallbacks
*
* @param application Application
*/
@TargetApi(14)
public static void registerActivityLifecycleCallbacks(Application application) {
application.registerActivityLifecycleCallbacks(new Application.Activity-LifecycleCallbacks() {
@Override
public void onActivityCreated(final Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(final Activity activity) {
trackAppViewScreen(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) {
}
});
}
需要我们注意的是,只有 API 14+ 才能注册ActivityLifecycleCallbacks回调。
在ActivityLifecycleCallbacks的onActivityResumed(final Activity activity)回调方法中,我们通过调用SensorsDataPrivate的trackAppViewScreen(Activity activity)方法来触发页面浏览事件($AppViewScreen)。
trackAppViewScreen(Activity activity)方法的内部实现逻辑比较简单,可以参考如下:
/**
* Track 页面浏览事件
*
* @param activity Activity
*/
@Keep
private static void trackAppViewScreen(Activity activity) {
try {
JSONObject properties = new JSONObject();
properties.put("$activity", activity.getClass().getCanonicalName());
SensorsDataAPI.getInstance().track("$AppViewScreen", properties);
} catch (Exception e) {
e.printStackTrace();
}
}
在此示例中,我们添加了一个$activity 属性,代表当前 Activity 的名称,我们使用包名+类名的形式表示。然后又定义了事件名称为“$AppViewScreen”,最后调用Sensors-DataAPI的 track 方法来触发页面浏览事件。
第6步:初始化埋点 SDK
需要在应用程序自定义的 Application类中初始化埋点 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);
}
}
第7步:声明自定义的 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>
运行 demo并启动一个 Activity,可以看到如下打印的事件信息,参考图2-1。
图2-1 页面浏览事件详细信息
上面的事件名称叫“$AppViewScreen”,代表的是页面浏览事件,它有一个自定义属性,叫“$activity”,代表当前正在显示的 Activity 名称(包名+类名)。
至此,页面浏览事件($AppViewScreen)的全埋点方案就算完成了。
- 点赞
- 收藏
- 关注作者
评论(0)