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)的全埋点方案就算完成了。