Android 全埋点解决方案 (7):$AppViewScreen 全埋点方案 2.3

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

Android全埋点解决方案(7):$AppViewScreen全埋点方案 2.3

(案例)

内容简介
这是一本实战为导向的、翔实的 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

下面我们会详细介绍 $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();
}
}

在此示例中,我们添加了一个 activityActivity使+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。

Android全埋点解决方案(7):$AppViewScreen全埋点方案 2.3

图 2-1 页面浏览事件详细信息

上面的事件名称叫“AppViewScreenactivity”,代表当前正在显示的 Activity 名称(包名 + 类名)。

至此,页面浏览事件 ($AppViewScreen) 的全埋点方案就算完成了。

Android全埋点解决方案(7):$AppViewScreen全埋点方案 2.3

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

评论

发布