关于Android全埋点方案
本文主要参考了《Android全埋点解决方案---王灼洲》一书
简介:
业务需求推送技术革新。
对于线上问题排查和解决一直都是程序的痛点,大数据的生态建设对于推动企业数字化转型的价值也是非常大的,于是便调研并接入Android的埋点方案,下面进入正题;
全埋点,也叫无埋点、无码埋点、无痕埋点、自动埋点。全埋点是指无须Android应用程序开发工程师写代码或者只写少量的代码,就能预先自动收集用户的所有行为数据,然后就可以根据实际的业务分析需求从中筛选出所需行为数据并进行分析。
全埋点采集的事件目前主要包括以下四种(事件名称前面的$符号,是指该事件是预置事件,与之对应的是自定义事件)。·$AppStart事件是指应用程序启动,同时包括冷启动和热启动场景。热启动也就是指应用程序从后台恢复的情况。
·$AppEnd事件是指应用程序退出,包括应用程序的正常退出、按Home键进入后台、应用程序被强杀、应用程序崩溃等场景。·$AppViewScreen事件是指应用程序页面浏览,对于Android应用程序来说,就是指切换Activity或Fragment。
·$AppClick事件是指应用程序控件点击,也即View被点击,比如点击Button、ListView等。
在采集的这四种事件当中,最重要并且采集难度最大的是$AppClick事件。所以,全埋点的解决方案基本上也都是围绕着如何采集$AppClick事件来进行的。
先来了解一下前三种的实现方式:
页面浏览埋点方案
在Android系统中,页面浏览其实就是指切换不同的Activity或Fragment(本书暂时只讨论切换Activity的情况)。对于一个Activity,它的哪个生命周期执行了,代表该页面显示出来了呢?通过对Activity生命周期的了解可知,其实就是onResume(Activity activity)的回调方法。所以,当一个Activity执行到onResume(Activityactivity)生命周期时,也就代表该页面已经显示出来了,即该页面被浏览了。我们只要自动地在onResume里触发$AppViewScreen事件,即可解决页面浏览事件的全埋点。
技术方案
ActivityLifecycleCallbacks是Application的一个内部接口,是从API 14(即Android 4.0)开始提供的。Application类通过此接口提供了一系列的回调方法,用于让开发者可以对Activity的所有生命周期事件进行集中处理(或称监控)。我们可以通过Application类提供的registerActivityLifecycleCallback(ActivityLifecycleCallbacks callback)方法来注册ActivityLifecycleCallbacks回调。
我们下面先看看Application.ActivityLifecycleCallbacks都提供了哪些回调方法。Application.ActivityLifecycleCallbacks接口定义如下:
public interface ActivityLifecycleCallbacks { @Override public void onActivityCreated(final Activity activity, Bundle bundle){} @Override public void onActivityStarted(Activity activity) {} @Override public void onActivityResumed(final Activity 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) {} }
我们可以很直观的看到,这个接口定义的方法跟Activity的生命周期基本如出一辙。如果我们注册了Activity-LifecycleCallbacks回调,Android系统会先回调ActivityLifecycleCallbacks的onActivityResumed(Activity activity)方法,然后再执行Activity本身的onResume函数(请注意这个调用顺序,因为不同的生命周期的执行顺序略有差异)。通过registerActivityLifecycleCallback方法名中的“register”字样可以知道,一个Application是可以注册多个ActivityLifecycleCallbacks回调的,我们用register方法的内部实现也可以证实这一点。
public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) { synchronized (mActivityLifecycleCallbacks) { mActivityLifecycleCallbacks.add(callback); } }
内部定义了一个list用来保存所有已注册的ActivityLifecycleCallbacks。
原理概述
实现Activity的页面浏览事件,大家首先想到的是定义一个BaseActivity,然后让其他Activity继承这个BaseActivity。这种方法理论上是可行的,但不是最优选择,有些特殊的场景是无法适应的。比如,你在应用程序里集成了一个第三方的库(比如IM相关的),而这个库里恰巧也包含Activity,此时你是无法让这个第三方的库也去继承你的BaseActivity(最起码驱使第三方服务商去做这件事的难度比较大)。所以,为了实现全埋点中的页面浏览事件,最优的方案还是基于我们上面讲的Application.ActivityLifecycleCallbacks。不过,使用Application.ActivityLifecycleCallbacks机制实现全埋点的页面浏览事件,也有一个明显的缺点,就是注册Application.ActivityLifecycleCallbacks回调要求API 14+。不过问题也不大,现在大多数项目都已经在这个之上了。
在应用程序自定义的Application类的onCreate()方法中初始化埋点SDK,并传入当前的Application对象。埋点SDK拿到Application对象之后,通过调用Application的registerActivityLifecycleCallback(ActivityLifecycleCallbacks callback)方法注册Application.ActivityLifecycleCallbacks回调。这样埋点SDK就能对当前应用程序中所有的Activity的生命周期事件进行集中处理(监控)了。
流程
第1步:新建一个项目(Project)在新建的项目中,会自动包含一个主module,即:app。
第2步:创建sdk module新建一个Android Library module,名称叫sdk,这个模块就是我们的埋点SDK模块。
第3步:添加依赖关系app module需要依赖sdk module。可以通过修改app/build.gradle文件,在其dependencies节点中添加依赖关系:
第4步:编写埋点SDK在sdk module中我们新建一个埋点SDK的主类,即SensorsDataAPI.java,完整的源码参考如下:
/** * @author zhangsicong * @version SensorsDataAPI.java, v 0.1 2020-05-11 12:20 PM zhangsicong * @description: TODO */@Keeppublic 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 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(); } }}
/** * @author zhangsicong * @version SensorsDataPrivate.java, v 0.1 2020-05-13 10:59 AM zhangsicong * @description: TODO */class SensorsDataPrivate { private static List mIgnoredActivities; static { mIgnoredActivities = new ArrayList<>(); } private static final SimpleDateFormat mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" + ".SSS", Locale.CHINA); public static void mergeJSONObject(final JSONObject source, JSONObject dest) throws JSONException { Iterator 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); } } } public static Map getDeviceInfo(Context context) { final Map 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); } } @SuppressWarnings("SameParameterValue") private static ViewGroup getRootViewFromActivity(Activity activity, boolean decorView) { if (decorView) { return (ViewGroup) activity.getWindow().getDecorView(); } else { return activity.findViewById(android.R.id.content); } } /** * 注册 Application.ActivityLifecycleCallbacks * * @param application Application */ @TargetApi(14) public static void registerActivityLifecycleCallbacks(Application application) { application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { @Override public void onActivityCreated(final Activity activity, Bundle bundle) { } @Override public void onActivityStarted(Activity activity) {} @Override public void onActivityResumed(final Activity activity) { final ViewGroup rootView = getRootViewFromActivity(activity, true); 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) {} }); } /** * Track 页面浏览事件 * * @param activity Activity */ @Keep private static void trackAppViewScreen(Activity activity) { try { if (activity == null) { return; } if (mIgnoredActivities.contains(activity.getClass().getCanonicalName())) { 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(); } } /** * 获取 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; } @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; androidx.appcompat.app.ActionBar supportActionBar = appCompatActivity.getSupportActionBar(); if (supportActionBar != null) { if (!TextUtils.isEmpty(supportActionBar.getTitle())) { return supportActionBar.getTitle().toString(); } } } } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 获取 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; } /** * 获取 View 所属 Activity * * @param view View * @return Activity */ private static Activity getActivityFromView(View view) { Activity activity = null; if (view == null) { return null; } try { Context context = view.getContext(); if (context != null) { if (context instanceof Activity) { activity = (Activity) context; } else if (context instanceof ContextWrapper) { while (!(context instanceof Activity) && context instanceof ContextWrapper) { context = ((ContextWrapper) context).getBaseContext(); } if (context instanceof Activity) { activity = (Activity) context; } } } } catch (Exception e) { e.printStackTrace(); } return activity; } 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 ""; } }}
一大段代码贴完~
从代码我们可以看出,这边主要还是通过Application中的ActivityLifecycleCallbacks接口来监控Activity的生命周期,并在这其中来记录用户的进入和退出。
至此,页面浏览事件($AppViewScreen)的全埋点方案就算完成了。
点击事件埋点方案
技术方案
android.R.id.content对应的视图是一个FrameLayout布局,它目前只有一个子元素,就是我们平时开发的时候,在onCreate方法中通过setContentView设置的View。换句说法就是,当我们在layout文件中设置一个布局文件时,实际上该布局会被一个FrameLayout容器所包含,这个FrameLayout容器的android:id属性值就是android.R.id.content。
原理概述
前面还是通过Application监控Activity的生命周期方法,在Application.ActivityLifecycleCallbacks的onActivityResumed(Activity activity)回调方法中,我们可以拿到当前正在显示的Activity实例,通过activity.findViewById(android.R.id.content)方法就可以拿到整个内容区域对应的View(是一个FrameLayout)。然后,埋点SDK再逐层遍历这个RootView,并判断当前View是否设置了mOnClickListener对象,如果已设置mOnClickListener对象并且mOnClickListener又不是我们自定义的WrapperOnClickListener类型,则通过WrapperOnClickListener代理当前View设置的mOnClickListener。WrapperOnClickListener是我们自定义的一个类,它实现了View.OnClickListener接口,在WrapperOnClickListener的onClick方法里会先调用View的原有mOnClickListener处理逻辑,然后再调用埋点代码,即可实现“插入”埋点代码,从而达到自动埋点的效果。
流程
前面的流程跟上面一样,但是我们需要在SDK的两个类中做一些处理,下面直接上代码。
/** * @author zhangsicong * @version SensorsDataAPI.java, v 0.1 2020-05-11 12:20 PM zhangsicong * @description: TODO */@Keeppublic 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 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 Dialog 的点击 * @param activity Activity * @param dialog Dialog */ public void trackDialog(@NonNull final Activity activity, @NonNull final Dialog dialog) { if (dialog.getWindow() != null) { dialog.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { SensorsDataPrivate.delegateViewsOnClickListener(activity, dialog.getWindow().getDecorView()); } }); } } /** * 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(); } }}
SensorsDataPrivate类的话因为太长了,这边只贴关键部分代码
/** * 注册 Application.ActivityLifecycleCallbacks * * @param application Application */ @TargetApi(14) public static void registerActivityLifecycleCallbacks(Application application) { application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { private ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener; @Override public void onActivityCreated(final Activity activity, Bundle bundle) { final ViewGroup rootView = getRootViewFromActivity(activity, true); onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { delegateViewsOnClickListener(activity, rootView); } }; } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(final Activity activity) { final ViewGroup rootView = getRootViewFromActivity(activity, true); rootView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener); trackAppViewScreen(activity); } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { if (Build.VERSION.SDK_INT >= 16) { final ViewGroup rootView = getRootViewFromActivity(activity, true); rootView.getViewTreeObserver().removeOnGlobalLayoutListener(onGlobalLayoutListener); } } @Override public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { } @Override public void onActivityDestroyed(Activity activity) { } }); }
/** * Delegate view OnClickListener * * @param context Context * @param view View */ @TargetApi(15) @SuppressWarnings("all") protected static void delegateViewsOnClickListener(final Context context, final View view) { if (context == null || view == null) { return; } if (view instanceof AdapterView) { if (view instanceof Spinner) { AdapterView.OnItemSelectedListener onItemSelectedListener = ((Spinner) view).getOnItemSelectedListener(); if (onItemSelectedListener != null && !(onItemSelectedListener instanceof WrapperAdapterViewOnItemSelectedListener)) { ((Spinner) view).setOnItemSelectedListener( new WrapperAdapterViewOnItemSelectedListener(onItemSelectedListener)); } } else if (view instanceof ExpandableListView) { try { Class viewClazz = Class.forName("android.widget.ExpandableListView"); //Child Field mOnChildClickListenerField = viewClazz.getDeclaredField("mOnChildClickListener"); if (!mOnChildClickListenerField.isAccessible()) { mOnChildClickListenerField.setAccessible(true); } ExpandableListView.OnChildClickListener onChildClickListener = (ExpandableListView.OnChildClickListener) mOnChildClickListenerField.get(view); if (onChildClickListener != null && !(onChildClickListener instanceof WrapperOnChildClickListener)) { ((ExpandableListView) view).setOnChildClickListener( new WrapperOnChildClickListener(onChildClickListener)); } //Group Field mOnGroupClickListenerField = viewClazz.getDeclaredField("mOnGroupClickListener"); if (!mOnGroupClickListenerField.isAccessible()) { mOnGroupClickListenerField.setAccessible(true); } ExpandableListView.OnGroupClickListener onGroupClickListener = (ExpandableListView.OnGroupClickListener) mOnGroupClickListenerField.get(view); if (onGroupClickListener != null && !(onGroupClickListener instanceof WrapperOnGroupClickListener)) { ((ExpandableListView) view).setOnGroupClickListener( new WrapperOnGroupClickListener(onGroupClickListener)); } } catch (Exception e) { e.printStackTrace(); } } else if (view instanceof ListView || view instanceof GridView) { AdapterView.OnItemClickListener onItemClickListener = ((AdapterView) view).getOnItemClickListener(); if (onItemClickListener != null && !(onItemClickListener instanceof WrapperAdapterViewOnItemClick)) { ((AdapterView) view).setOnItemClickListener( new WrapperAdapterViewOnItemClick(onItemClickListener)); } } } else { //获取当前 view 设置的 OnClickListener final View.OnClickListener listener = getOnClickListener(view); //判断已设置的 OnClickListener 类型,如果是自定义的 WrapperOnClickListener,说明已经被 hook 过,防止重复 hook if (listener != null && !(listener instanceof WrapperOnClickListener)) { //替换成自定义的 WrapperOnClickListener view.setOnClickListener(new WrapperOnClickListener(listener)); } else if (view instanceof CompoundButton) { final CompoundButton.OnCheckedChangeListener onCheckedChangeListener = getOnCheckedChangeListener(view); if (onCheckedChangeListener != null && !(onCheckedChangeListener instanceof WrapperOnCheckedChangeListener)) { ((CompoundButton) view).setOnCheckedChangeListener( new WrapperOnCheckedChangeListener(onCheckedChangeListener)); } } else if (view instanceof RadioGroup) { final RadioGroup.OnCheckedChangeListener radioOnCheckedChangeListener = getRadioGroupOnCheckedChangeListener(view); if (radioOnCheckedChangeListener != null && !(radioOnCheckedChangeListener instanceof WrapperRadioGroupOnCheckedChangeListener)) { ((RadioGroup) view).setOnCheckedChangeListener( new WrapperRadioGroupOnCheckedChangeListener(radioOnCheckedChangeListener)); } } else if (view instanceof RatingBar) { final RatingBar.OnRatingBarChangeListener onRatingBarChangeListener = ((RatingBar) view).getOnRatingBarChangeListener(); if (onRatingBarChangeListener != null && !(onRatingBarChangeListener instanceof WrapperOnRatingBarChangeListener)) { ((RatingBar) view).setOnRatingBarChangeListener( new WrapperOnRatingBarChangeListener(onRatingBarChangeListener)); } } else if (view instanceof SeekBar) { final SeekBar.OnSeekBarChangeListener onSeekBarChangeListener = getOnSeekBarChangeListener(view); if (onSeekBarChangeListener != null && !(onSeekBarChangeListener instanceof WrapperOnSeekBarChangeListener)) { ((SeekBar) view).setOnSeekBarChangeListener( new WrapperOnSeekBarChangeListener(onSeekBarChangeListener)); } } } //如果 view 是 ViewGroup,需要递归遍历子 View 并 hook if (view instanceof ViewGroup) { final ViewGroup viewGroup = (ViewGroup) view; int childCount = viewGroup.getChildCount(); if (childCount > 0) { for (int i = 0; i < childCount; i++) { View childView = viewGroup.getChildAt(i); //递归 delegateViewsOnClickListener(context, childView); } } } }
从代码我们可以看出,在ActivityLifecycleCallbacks的onActivityCreated方法中获取到我们前面说的rootView,拿到rootView之后通过instanceof判断这个view具体是什么类型的点击时间,然后对于当前控件加上我们自定义的监听事件来去做单独的处理。
最后的最后,当然要把书里的源码贴出来。
页面浏览事件 https://github.com/wangzhzh/AutoTrackAppViewScreen
点击监听事件 https://github.com/wangzhzh/AutoTrackAppClick1
更多相关文章
- 你真的了解android中的SAX解析吗?
- 【自定义控件】android事件分发机制
- Android应用程序插件化研究之DexClassLoader
- Android(安卓)SDK开发指南(翻译)系列一:最佳实践(一)-- 性能设计
- android 事件机制与事件监听(一)
- H5唤起android app,启动关联应用
- MDC Android专场:账户同步备份框架与Web&Native混合开发
- Android(安卓)让人又爱又恨的触摸机制(二)
- Android应用程序的编译流程及使用Ant编译项目的攻略