作者:王灼洲
出版社:机械工业出版社
格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT
Android全埋点解决方案试读:
前言
为什么要写这本书?转眼间,我从事Android研发工作已经有9个年头,作为国内第一批Android研发工作者,我见证了Android的发展历程,也开发和维护着国内第一个商用的开源Android & iOS数据埋点SDK。
我目前就职于神策数据,担任神策数据合肥研发中心负责人。神策数据是一家以重构中国互联网数据根基为使命的公司,十分重视基础数据的采集与建模。随着大数据行业的快速发展,数据采集也变得越来越重要,数据基础夯实与否,取决于数据的采集方式。埋点方式多种多样,按照埋点位置不同,可以分为前端(客户端)埋点与后端(服务器端)埋点。其中全埋点(无埋点)是目前较为流行的前端埋点方式之一。
在服务数百家客户的过程中,我逐渐萌生出写此书的想法,原因有三:
第一,国内企业对全埋点技术需求迫切,但是图书市场仍处空白。
全埋点技术炙手可热,全埋点采用“全部采集,按需选取”的形式,对页面中所有交互元素的用户行为进行采集,通过界面配置来决定哪些数据需要进行分析,也被誉为“最全、最便捷、界面友好、技术门槛低”的数据采集方式。
第二,市面上存在对全埋点概念过度包装的情况,希望本书能够揭开全埋点的神秘面纱。
数据埋点技术在互联网(尤其是移动端)上使用非常普遍,一些数据分析服务厂商将全埋点概念经过包装后,作为核心技术来卖,给人神秘无比的感觉。
第三,给企业带来价值,推动开发者参与大数据行业的生态建设。
神策数据的采集技术一直在不断革新,神策SDK组件统称为OpenSasdk,包括C SDK、C++SDK、CSharp SDK、Java SDK、Python SDK、PHP SDK、Ruby SDK、Golang SDK、Node SDK、APICloud SDK、Android SDK、iOS SDK等,神策数据愿意将一些成熟的技术与国内外开发者交流与分享,并已于2019年1月正式成立供IT开发者的分享、使用与交流技术的开源社区——Sensors Data开源社区,一方面能够更好地服务客户,推动企业的数字化转型;一方面借此造福同行,推动开发者参与数据行业生态建设。
我希望通过此书全面公开Android全埋点技术,从0到1进行详细介绍,尤其是控件点击事件全埋点采集的8种方法,并都提供了完整的项目源码。读者对象
本书适用于初级、中级、高级水平的Android开发工程师、技术经理、技术总监等。如何阅读这本书
本书系统讲解了Android全埋点的解决方案,特别是控件点击事件的全埋点采集,总结并归纳了如下8种解决方案,并且都提供了完整的项目源码。
$AppStart、$AppEnd全埋点方案
·$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勘误和支持
由于作者的水平有限,编写时间仓促,以及技术不断地更新和迭代,书中难免会出现一些错误或者不准确的地方,恳请读者批评指正。为此,特意创建了一个网站:http://book.blendercn.org,读者可以将书中的错误发布在Bug勘误表页面中。同时,如果你遇到任何问题,也可以访问Q & A页面,我将尽量在线上为读者提供满意的解答。书中的全部源文件可以从上面这个网站下载,我会将相应的功能更新及时发布出来。如果你有更多的宝贵意见,也欢迎发送邮件至邮箱congcong009@gmail.com,期待能够得到你们的真挚反馈。致谢
感谢神策数据创始人团队桑文锋、曹犟、付力力、刘耀洲在工作中的指导和帮助。
感谢机械工业出版社华章公司的编辑杨福川老师,在这半年多的时间中始终支持我的写作,你的鼓励和帮助引导我能顺利完成全部书稿。
谨以此书献给大数据行业的关注者和建设者!王灼洲2019年2月 第1章 全埋点概述
全埋点,也叫无埋点、无码埋点、无痕埋点、自动埋点。全埋点是指无须Android应用程序开发工程师写代码或者只写少量的代码,就能预先自动收集用户的所有行为数据,然后就可以根据实际的业务分析需求从中筛选出所需行为数据并进行分析。
全埋点采集的事件目前主要包括以下四种(事件名称前面的$符号,是指该事件是预置事件,与之对应的是自定义事件)。
·$AppStart事件
是指应用程序启动,同时包括冷启动和热启动场景。热启动也就是指应用程序从后台恢复的情况。
·$AppEnd事件
是指应用程序退出,包括应用程序的正常退出、按Home键进入后台、应用程序被强杀、应用程序崩溃等场景。
·$AppViewScreen事件
是指应用程序页面浏览,对于Android应用程序来说,就是指切换Activity或Fragment。
·$AppClick事件
是指应用程序控件点击,也即View被点击,比如点击Button、ListView等。
在采集的这四种事件当中,最重要并且采集难度最大的是$AppClick事件。所以,全埋点的解决方案基本上也都是围绕着如何采集$AppClick事件来进行的。
对于$AppClick事件的全埋点整体解决思路,归根结底,就是要自动找到那个被点击的控件处理逻辑(后文统称原处理逻辑),然后再利用一定的技术原理,对原处理逻辑进行“拦截”,或者在原处理逻辑的执行前面或执行者后面“插入”相应的埋点代码逻辑,从而达到自动埋点的效果。
至于如何做到自动“拦截”控件的原处理逻辑,一般都是参考Android系统的事件处理机制来进行的。关于Android系统的事件处理机制,本书由于篇幅有限,不再详述。
至于如何做到自动“插入”埋点代码逻辑,基本上都是参考编译器对Java代码的整体处理流程来进行的,即:JavaCode --> .java --> .class --> .dex
选择在不同的处理阶段“插入”埋点代码,所采用的技术或者原理也不尽相同,所以全埋点的解决方案也是多种多样的。
面对这么多的全埋点方案,我们究竟该如何做选择呢?
在选择全埋点的解决方案时,我们需要从效率、兼容性、扩展性等方面进行综合考虑。
·效率
全埋点的基本原理,如上所述,其实就是利用某些技术对某些方法(控件被点击时的处理逻辑)进行拦截(或者叫代理)或者“插入”相关埋点代码。比如按钮Button,如果要给它设置点击处理逻辑,需要设置android.view.View.OnClickListener,并重写它的onClick(android.view.View)方法。如果要实现$AppClick事件的全埋点,我们就可以“拦截”onClick(android.view.View)方法,或者在onClick(android.view.View)方法的前面或者后面“插入”相应的埋点逻辑代码。按照“在什么时候去代理或者插入代码”这个条件来区分的话,$AppClick事件的全埋点技术可以大致分为如下两种方式。
·静态代理
所谓静态代理,就是指通过Gradle Plugin在应用程序编译期间“插入”代码或者修改代码(.class文件)。比如AspectJ、ASM、Javassist、AST等方案均属于这种方式。这几种方案,我们在后面会一一进行介绍。
这几种方式处理的时机可以参考图1-1。图1-1 静态代理处理时机
·动态代理
所谓动态代理,就是指在代码运行的时候(Runtime)去进行代理。比如我们比较常见的代理View.OnClickListener、Window.Callback、View.AccessibilityDelegate等方案均属于这种方式。这几种方案,我们也会在后面一一进行介绍。
不同的方案,其处理能力和运行效率各不相同,同时对应用程序的侵入程度以及对应用程序的整体性能的影响也各不相同。从总体上来说,静态代理明显优于动态代理,这是因为静态代理的“动作”是在应用程序的编译阶段处理的,不会对应用程序的整体性能有太大的影响,而动态代理的“动作”是在应用程序运行阶段发生的(也即Runtime),所以会对应用程序的整体性能有一定的影响。
·兼容性
随着Android生态系统的快速发展,不管是Android系统本身,还是与Android应用程序开发相关的组件和技术,都在飞速发展和快速迭代,从而也给我们研发全埋点方案带来一定的难度。比如不同的Android应用程序可以有不同的开发语言(Java、Kotlin)、不同的Java版本(Java7、Java8)、不同的开发IDE(eclipse、Android Studio),更有不同的开发方式(原生开发、H5、混合开发),使用不同的第三方开发框架(React Native、APICloud、Weex)、不同的Gradle版本,以及Lambda、D8、Instant Run、DataBinding、Fragment等新技术的出现,都会给全埋点带来很多兼容性方面的问题。
·扩展性
随着业务的快速发展和对数据分析需求的不断提高,对使用全埋点进行数据采集,也提出了更高的要求。一方面要求可以全部自动采集(采集的范围),同时又要求能有更精细化的采集控制粒度(采集可以自定义)。比如,如何给某个控件添加自定义属性?如果不想采集某个控件的点击事件应该如何控制?如果不想采集某种控件类型(ImageView)的点击事件又该如何处理?如果某个页面(Activity)上所有控件的点击事件都不想采集又该如何处理等。
任何一种全埋点的技术方案,都有优点和缺点,没有一种普适的完美解决方案。我们只需要针对不同的应用场景,选择最合适的数据采集方案即可。能满足实际数据采集需求的方案,才是最优的方案。1.1 Android View类型
在Android系统中,控件(View)的类型非常丰富。分类方式也是多种多样的。我们根据控件设置的监听器(listener)的不同,可以大致将控件分为如下几类。
·Button、CheckedTextView、TextView、ImageButton、ImageView等
为这些控件设置的listener均是android.view.View.OnClickListener。
下面以Button为例:Button button = findViewById(R.id.button);button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //do something }});SeekBar
·SeekBar
SeekBar设置的listener是android.widget.SeekBar.OnSeekBarChangeListener,如:SeekBar seekBar = findViewById(R.id.seekBar);seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { // do something } @Override public void onStartTrackingTouch(SeekBar seekBar) { // do something } @Override public void onStopTrackingTouch(SeekBar seekBar) { // do something }});
·TabHost
TabHost设置的listener是android.widget.TabHost.OnTabChangeListener,如:TabHost tabHost = findViewById(R.id.tabhost);tabHost.setOnTabChangedListener(new TabHost.OnTabChangeListener() { @Override public void onTabChanged(String tabName) { //do something }});
·RatingBar
RatingBar设置的listerner是android.widget.RatingBar.OnRatingBarChangeListener,如:RatingBar ratingBar = findViewById(R.id.ratingBar);ratingBar.setOnRatingBarChangeListener(newRatingBar.OnRatingBarChangeListener() { @Override public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { //do something }});
·CheckBox、SwitchCompat、RadioButton、ToggleButton、RadioGroup等
这些View属于同一种类型,它们都是属于带有“状态”的按钮,它们设置的listener均是CompoundButton.OnCheckedChangeListener。
下面以CheckBox为例:CheckBox checkBox = findViewById(R.id.checkbox);checkBox.setOnCheckedChangeListener(newCompoundButton.OnCheckedChangeListener(){ @Override public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { //do something } });
·Spinner
Spinner设置的listener是android.widget.AdapterView.OnItemSelectedListener,如:Spinner spinner = findViewById(R.id.spinner);spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView > parent, View view, int position, long id) { //do something } @Override public void onNothingSelected(AdapterView > parent) { } });
·MenuItem
主要是通过重写Activity的相关方法(onOptionsItemSelected、onContextItemSelected)来设置listener,如://选项菜单@Overridepublic boolean onOptionsItemSelected(android.view.MenuItem) { //do something}//上下文菜单@Overridepublic boolean onContextItemSelected(android.view.MenuItem) { //do something}
·ListView、GridView
ListView和GridView都是AdapterView的子类,显示的内容都是一个“集合”。它们设置的listener均是android.widget.AdapterView.OnItemClickListener,如:ListView listView = findViewById(R.id.listView);listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){ @Override public void onItemClick(AdapterView > parent, View view, int position, long id) { //do something }});
·ExpandableListView
ExpandableListView也是AdapterView的子类,同时也是ListView的子类。它的点击分为ChildClick和GroupClick两种情况,所以,它设置的listener也是分为两种情况,即:android.widget.ExpandableListView.OnChildClickListener和android.widget.ExpandableList-View.OnGroupClickListener,如:ExpandableListView listview = findViewById(R.id.expandablelistview);//ChildClicklistview.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView expandableListView, View view, int groupPosition, int childPosition, long id) { //do something return true; }});//GroupClicklistview.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() { @Override public boolean onGroupClick(ExpandableListView expandableListView, View view, int childPosition, long id) { //do something return true; }});
·Dialog
Dialog设置的listener分为两种情况。对于常见的普通Dialog,设置的listener是android.content.DialogInterface.OnClickListener,如:AlertDialog.Builder builder = new AlertDialog.Builder(context);builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { //do something } });
还有一种是显示列表的Dialog,它设置的listener是android.content.DialogInterface.OnMultiChoiceClickListener,如:AlertDialog.Builder builder = new AlertDialog.Builder(context);DialogInterface.OnMultiChoiceClickListener mutiListener = new DialogInterface.OnMultiChoiceClickListener() { @Override public void onClick(DialogInterface dialogInterface, int which, boolean isChecked) { //do something }};1.2 View绑定listener方式
随着Android相关技术的不断更新迭代,给View绑定listener的方式也是多种多样的。下面以Button为例来介绍日常开发中比较常见的几种绑定listener的方式。
·通过代码来设置listenerButton button = findViewById(R.id.button);button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //do something }});
这种方式是目前开发中最常用的方式,也是我们全埋点方案需要重点解决和重点支持的方式。
·通过android:onClick属性绑定listener
先在布局文件中声明Button的android:onClick属性,如:
我们设置android:onClick的属性值为“xmlOnClick”,此时的“xmlOnClick”代表点击处理逻辑对应的方法名。然后在对应的Activity文件中声明android:onClick属性指定的方法xmlOnClick:public void xmlOnClick(View view) { //do something}
注意:该方法必须有且仅有一个View类型的参数。
这种方式在一些新的项目中不是很常见,在一些比较老的Android项目中可能会有这样大量的使用方式。
·通过注解绑定listener
目前有很多第三方的库都提供了类似的功能,下面以ButterKnife为例:@OnClick({R2.id.butterknife})public void butterKnifeButtonOnClick(View view) { //do something}
首先定义一个方法,并且该方法有且仅有一个View类型的参数,然后在该方法上使用ButterKnife的@OnClick注解声明,其中的参数代表控件的android:id。
这种方式,也是目前比较流行的其中一种使用方式。
关于ButterKnife更详细用法可以参考其官网:https://github.com/JakeWharton/butterknife。
·listener含有Lambda语法
Lambda是Java8开始支持的,如:AppCompatButton button = findViewById(R.id.lamdbaButton);button.setOnClickListener(view ->Log.i("MainActivity", "Lambda OnClick"));
这种方式,也是目前比较流行的一种使用方式。
事实上,这根本就不算一种绑定listener的方式,只是绑定的listener中含有Lambda语法而已。之所以在这里要提到它,是因为这种方式会对我们选择全埋点方案时产生一定的影响,比如后面将要介绍的AspectJ全埋点方案目前就无法支持这种带有Lambda语法的点击事件。
关于Lambda的详细信息可以参考:https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html。
·通过DataBinding绑定listener
先在布局文件中声明android:onClick属性:
android:onClick属性值为“@{handlers::dataBindingOnClick}”,意为该按钮的点击处理逻辑为handlers对象的dataBindingOnClick方法,其中handlers对象是MainActivity的实例.
然后在对应的Java文件中声明android:onClick属性指定的方法dataBindingOnClick:public void dataBindingOnClick(View view) { //do something}
注意:该方法必须有且仅有一个View类型的参数。
这种方式,也是目前新流行的一种使用方式。
关于DataBinding更详细的用法请参考官网:https://developer.android.com/topic/libraries/data-binding/index.html。
由于全埋点重点解决的是控件的点击行为数据,所以了解控件都能设置哪些listener,以及设置或者绑定listener的不同方式,对于我们研究或者选择全埋点的方案,都会有非常大的帮助。 第2章 $AppViewScreen全埋点方案
$AppViewScreen事件,即页面浏览事件。在Android系统中,页面浏览其实就是指切换不同的Activity或Fragment(本书暂时只讨论切换Activity的情况)。对于一个Activity,它的哪个生命周期执行了,代表该页面显示出来了呢?通过对Activity生命周期的了解可知,其实就是onResume(Activity activity)的回调方法。所以,当一个Activity执行到onResume(Activity activity)生命周期时,也就代表该页面已经显示出来了,即该页面被浏览了。我们只要自动地在onResume里触发$AppViewScreen事件,即可解决$AppViewScreen事件的全埋点。2.1 关键技术Application.ActivityLifecycleCallbacks
ActivityLifecycleCallbacks是Application的一个内部接口,是从API 14(即Android 4.0)开始提供的。Application类通过此接口提供了一系列的回调方法,用于让开发者可以对Activity的所有生命周期事件进行集中处理(或称监控)。我们可以通过Application类提供的registerActivityLifecycleCallback(ActivityLifecycleCallbacks callback)方法来注册ActivityLifecycleCallbacks回调。
我们下面先看看Application.ActivityLifecycleCallbacks都提供了哪些回调方法。Application.ActivityLifecycleCallbacks接口定义如下: public interface ActivityLifecycleCallbacks { void onActivityCreated(Activity activity, Bundle savedInstanceState); void onActivityStarted(Activity activity); void onActivityResumed(Activity activity); void onActivityPaused(Activity activity); void onActivityStopped(Activity activity); void onActivitySaveInstanceState(Activity activity, Bundle outState); void onActivityDestroyed(Activity activity);}
以Activity的onResume(Activity activity)生命周期为例,如果我们注册了Activity-LifecycleCallbacks回调,Android系统会先回调ActivityLifecycleCallbacks的onActivity-Resumed(Activity activity)方法,然后再执行Activity本身的onResume函数(请注意这个调用顺序,因为不同的生命周期的执行顺序略有差异)。通过registerActivityLifecycleCallback方法名中的“register”字样可以知道,一个Application是可以注册多个ActivityLifecycleCallbacks回调的,我们通过registerActivityLifecycleCallback方法的内部实现也可以证实这一点。public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) { synchronized (mActivityLifecycleCallbacks) { mActivityLifecycleCallbacks.add(callback); }}
内部定义了一个list用来保存所有已注册的ActivityLifecycleCallbacks。2.2 原理概述
实现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的生命周期事件进行集中处理(监控)了。在注册的Application.ActivityLifecycleCallbacks的onActivityResumed(Activity activity)回调方法中,我们可以拿到当前正在显示的Activity对象,然后调用SDK的相关接口触发页面浏览事件($AppViewScreen)即可。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 */@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
目前这个主类比较简单,主要包含如下几个方法。
·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
第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() {
试读结束[说明:试读内容隐藏了图片]