带你读《Android全埋点解决方案》之一:全埋点概述

点击查看第二章
点击查看第三章
Android全埋点解决方案

带你读《Android全埋点解决方案》之一:全埋点概述


王灼洲 著

第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。

带你读《Android全埋点解决方案》之一:全埋点概述

□动态代理
所谓动态代理,就是指在代码运行的时候(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设置的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,如:
//选项菜单
@Override
public boolean onOptionsItemSelected(android.view.MenuItem) {

//do something

}

//上下文菜单
@Override
public 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);
//ChildClick
listview.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {

@Override
public boolean onChildClick(ExpandableListView expandableListView, 
                    View view, int groupPosition, int childPosition, long id) {
    //do something
    return true; 
}

});

//GroupClick
listview.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 的方式。
□通过代码来设置listener
Button 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:id="@+id/xmlOnClick"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="xmlOnClick"
android:text="android:onClick 绑定OnClickListener"/>

我们设置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全埋点解决方案》之一:全埋点概述

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 的不同方式,对于我们研究或者选择全埋点的方案,都会有非常大的帮助。

上一篇:IBM Watson:用人工智能提升美国零售业消费体验


下一篇:Android开发8——利用pull解析器读写XML文件