Skip to content

本项目通过Android字节码插桩插件实现Android端无埋点(或自动埋点),并且支持根据配置文件实现业务数据的自动采集。

Notifications You must be signed in to change notification settings

neuyu/LazierTracker

This branch is up to date with nailperry-zd/LazierTracker:master.

Folders and files

NameName
Last commit message
Last commit date

Latest commit

author
zhangdan
Mar 8, 2018
76d867f · Mar 8, 2018

History

58 Commits
Mar 8, 2018
Mar 8, 2018
Mar 8, 2018
Mar 8, 2018
Mar 8, 2018
Jun 10, 2017
Mar 5, 2018
Mar 8, 2018
Mar 8, 2018
Sep 15, 2017
Jun 10, 2017
Jun 10, 2017
Jun 10, 2017
Mar 8, 2018
Jun 10, 2017
Mar 8, 2018
Mar 5, 2018
Jun 10, 2017

Repository files navigation

简介

本项目通过Android字节码插桩插件实现Android端无埋点(或自动埋点),并且支持根据配置文件实现业务数据的自动采集。

无埋点插件

为便于大家深入理解Android字节码插桩插件,特别梳理了一篇文章应用于Android无埋点的Gradle插件解析,供大家参考。

原理

试想一下我们代码埋点的过程:首先定位到事件响应函数,例如Button的onClick函数,然后在该事件响应函数中调用SDK数据搜集接口。

我们的gradle插件采用 Android gradle 插件提供的最新的Transform API,在Apk编译环节中、class打包成dex之前,插入了中间环节,调用 ASM API对class文件的字节码进行扫描,当扫描到目标事件响应函数时,在函数头部或尾部插入SDK数据搜集代码。

开发环境

  • 语言:Groovy
  • 字节码操作库:ASM5.0
  • 工具:Android Studio 2.3.3(Mac)
  • Gradle:1.5+

注意事项

在AS 3.0中,需要在projectgradle.properties中添加

android.enableD8=true

使用

使用Android字节码插桩插件,您可能需要做些自定义的配置,比如在ReWriterConfig中配置注入代码的类名及待注入的方法映射

例如

public static String sAgentClassName = 'com/codeless/tracker/PluginAgent'

sInterfaceMethods.put('onClick(Landroid/view/View;)V', new MethodCell(
                'onClick',
                '(Landroid/view/View;)V',
                'android/view/View$OnClickListener',
                'onClick',
                '(Landroid/view/View;)V',
                1, 1,
                [Opcodes.ALOAD]))

上述代码表明当一个ActivityFragment实现了View$OnClickListener接口时,使用本插件遍历到该ActivityFragment字节码中的onClick(View v)时,向该方法中插入com.codeless.tracker.PluginAgent.onClick(v)com.codeless.tracker.PluginAgent中的onClick(View v)方法即是您想要注入到点击事件响应onClick中的代码。

1. 本地插件集成

appbuild.grade中添加

// 直接引用buildsrc的插件类
apply plugin: com.codeless.plugin.InjectPluginImpl

2. 自定义参数

appbuild.grade中添加如下代码,各配置项的含义请参考英文注释

codelessConfig {
    //this will determine the name of this plugin transform, no practical use.
    pluginName = 'myPluginTest'
    //turn this on to make it print help content, default value is true
    showHelp = true
    //this flag will decide whether the log of the modifying process be printed or not, default value is false
    keepQuiet = false
    //this is a kit feature of the plugin, set it true to see the time consume of this build
    watchTimeConsume = false

    //this is the most important part, 3rd party JAR packages that want our plugin to inject;
    //our plugin will inject package defined in 'AndroidManifest.xml' and 'butterknife.internal.butterknife.internal.DebouncingOnClickListener' by default.
    //structure is like ['butterknife.internal','com.a.c'], type is HashSet<String>.
    //You can also specify the name of the class;
    //example: ['com.xxx.xxx.BaseFragment']
    targetPackages = []
}

3. 远程插件集成

这一步需要您修改好ReWriterConfig后,发布插件到远程仓库,然后在app中引用远程插件。具体步骤请参考Codeless-Gradle-Plugin-Repo

支持插桩的目标方法

1. 目标方法在Fragment中声明

目标方法:

  • onResume()V
  • onPause()V
  • setUserVisibleHint(Z)V
  • onHiddenChanged(Z)V

具体实现:

  • 对app中指定包进行扫描,筛选出所有父类为android/app/Fragmentandroid/support/v4/app/Fragment的类。
  • 对这些Fragment子类的onResumedonPausedonHiddenChangedsetFragmentUserVisibleHint方法的字节码进行修改,添加数据采集代码。

目标效果:

public class BaseFragment extends Fragment {
    public BaseFragment() {
    }

    public void onResume() {
        super.onResume();
        PluginAgent.onFragmentResume(this);
    }

    public void onHiddenChanged(boolean var1) {
        super.onHiddenChanged(var1);
        PluginAgent.onFragmentHiddenChanged(this);
    }

    public void onPause() {
        super.onPause();
        PluginAgent.onFragmentPause(this);
    }

    public void setUserVisibleHint(boolean var1) {
        super.setUserVisibleHint(var1);
        PluginAgent.setFragmentUserVisibleHint(this, var1);
    }
}

2. 目标方法在接口中声明

目标方法:

  • onClick(Landroid/view/View;)V
  • onClick(Landroid/content/DialogInterface;I)V
  • onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V
  • onItemSelected(Landroid/widget/AdapterView;Landroid/view/View;IJ)V
  • onGroupClick(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z
  • onChildClick(Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)Z
  • onRatingChanged(Landroid/widget/RatingBar;FZ)V
  • onStopTrackingTouch(Landroid/widget/SeekBar;)V
  • onCheckedChanged(Landroid/widget/CompoundButton;Z)V
  • onCheckedChanged(Landroid/widget/RadioGroup;I)V
  • ...

具体实现:

  • 对app中指定包进行扫描,筛选出实现了目标接口的类,在目标方法中添加数据采集代码。

例如,筛选出实现了android/view/View$OnClickListener接口的类,然后在onClick(Landroid/view/View;)V方法中注入采集数据的代码。

目标效果:

public class MainActivity extends AppCompatActivity implements OnClickListener, android.content.DialogInterface.OnClickListener, OnItemClickListener, OnItemSelectedListener, OnRatingBarChangeListener, OnSeekBarChangeListener, OnCheckedChangeListener, android.widget.RadioGroup.OnCheckedChangeListener, OnGroupClickListener, OnChildClickListener {
    public MainActivity() {
    }

    protected void onCreate(Bundle var1) {
        super.onCreate(var1);
        this.setContentView(2130968603);
    }

    public void onClick(View var1) {
        PluginAgent.onClick(var1);
    }

    public void onClick(DialogInterface var1, int var2) {
        PluginAgent.onClick(this, var1, var2);
    }

    public void onItemClick(AdapterView<?> var1, View var2, int var3, long var4) {
        PluginAgent.onItemClick(this, var1, var2, var3, var4);
    }

    public void onItemSelected(AdapterView<?> var1, View var2, int var3, long var4) {
        PluginAgent.onItemSelected(this, var1, var2, var3, var4);
    }

    public void onNothingSelected(AdapterView<?> var1) {
    }

    public void onCheckedChanged(CompoundButton var1, boolean var2) {
        PluginAgent.onCheckedChanged(this, var1, var2);
    }

    public boolean onChildClick(ExpandableListView var1, View var2, int var3, int var4, long var5) {
        PluginAgent.onChildClick(this, var1, var2, var3, var4, var5);
        return false;
    }

    public boolean onGroupClick(ExpandableListView var1, View var2, int var3, long var4) {
        PluginAgent.onGroupClick(this, var1, var2, var3, var4);
        return false;
    }

    public void onCheckedChanged(RadioGroup var1, int var2) {
        PluginAgent.onCheckedChanged(this, var1, var2);
    }

    public void onRatingChanged(RatingBar var1, float var2, boolean var3) {
        PluginAgent.onRatingChanged(this, var1, var2, var3);
    }

    public void onProgressChanged(SeekBar var1, int var2, boolean var3) {
    }

    public void onStartTrackingTouch(SeekBar var1) {
    }

    public void onStopTrackingTouch(SeekBar var1) {
        PluginAgent.onStopTrackingTouch(this, var1);
    }
}

ASM语法实战

目标方法对应的ASM字节码操作

业务数据采集

业务数据的采集需要下发json格式的配置文件,该文件本该由埋点服务器下发。这里为了演示方便,将配置文件放在tracker模块的assets目录下。

configure.json

示例1

假设页面布局如下:

one_button

根据我们的ViewPath计算规则(参考网易HubbleData之Android无埋点实践),可知该按钮的ViewPath为:

/MainWindow/ContentFrameLayout[0]#android:content/RelativeLayout[0]#activity_main/AppCompatButton[0]#btn

现在希望点击按钮后,搜集该按钮所在Activity的成员变量mTestField的值,如下图所示:

mTestField

根据网易乐得提出的数据路径DSL语言规则(参考Android无埋点数据收集SDK关键技术),可知变量mTestField的数据引用路径应该表示为this.context.mTestField

因此,上述业务数据搜集需求可用如下配置表示:

{
  "pageName": "MainActivity",
  "viewPath": "/MainWindow/ContentFrameLayout[0]#android:content/RelativeLayout[0]#activity_main/AppCompatButton[0]#btn",
  "eventType": "viewClick",
  "dataPath": "this.context.mTestField"
}

运行工程,触发事件,无埋点采集到的数据效果如下:

D/LazierTracker: 成功打点事件->@eventId = 73acfdf0c708e1dc3f90f4611da2569167872469c6ae697e51688fa7207eef62
D/LazierTracker: attributes@businessData = 我是测试变量
D/LazierTracker: attributes@viewPath = /MainWindow/ContentFrameLayout[0]#android:content/RelativeLayout[0]#activity_main/AppCompatButton[0]#btn
D/LazierTracker: attributes@pageName = MainActivity

示例2

如图所示:

Example_RecyclerView

该页面列表由RecyclerView实现,红框圈住的视图为RecyclerView的第0个item,该视图对应的ViewPath为:

MainWindow/ContentFrameLayout[0]#android:content/LinearLayout[0]#root_view/FrameLayout[0]#content_view/FrameLayout[0]#fragment_container/NNFeedsFragment[0]/FrameLayout[0]#content_view/LinearLayout[0]/NoScrollViewPager[0]#view_pager/NNFNewsListFragment[0]/FrameLayout[0]#content_view/FrameLayout[0]#list_fragment_container/NNFSmartRefreshLayout[0]#refreshLayout/RecyclerView[0]#rrv_news_infos/LinearLayout[0]

现在希望点击按钮后,搜集该视图中新闻的infoId,变量infoId未展示在界面上,但在内存中。为了拿到该变量值,需要拿到该视图对应的数据源。做法是,先拿到RecyclerView第0项的ViewHolder,看代码可知是NewsInfoHolderTriS,而NewsInfoHolderTriS继承NewsInfoHolderNone,该视图对应的数据源即是NewsInfoHolderNone中的成员变量mNewsInfo,如下图所示:

mNewsInfo

mNewsInfo中包含具体的infoIdtitle等字段,按此描述,数据引用路径应该为item.mNewsInfo.infoId&item.mNewsInfo.title

因此,上述业务数据搜集需求可用如下配置表示:

{
  "pageName": "SampleFeedsActivity",
  "viewPath": "/MainWindow/ContentFrameLayout[0]#android:content/LinearLayout[0]#root_view/FrameLayout[0]#content_view/FrameLayout[0]#fragment_container/NNFeedsFragment[0]/FrameLayout[0]#content_view/LinearLayout[0]/NoScrollViewPager[0]#view_pager/NNFNewsListFragment[0]/FrameLayout[0]#content_view/FrameLayout[0]#list_fragment_container/NNFSmartRefreshLayout[0]#refreshLayout/RecyclerView[0]#rrv_news_infos/LinearLayout[0]",
  "eventType": "viewClick",
  "dataPath": "item.mNewsInfo",
  "subPath": [
    "infoId",
    "title"
  ]
}

运行工程,触发事件,无埋点采集到的数据效果如下:

D/LazierTracker: 成功打点事件->@eventId = b9f3be79bad49fb69fc7508f79702fc044da4d2e695432d9df0b62a41c37c740
D/LazierTracker: attributes@infoId = II2TH7RYJ1VOUR0
D/LazierTracker: attributes@viewPath = /MainWindow/ContentFrameLayout[0]#android:content/LinearLayout[0]#root_view/FrameLayout[0]#content_view/FrameLayout[0]#fragment_container/NNFeedsFragment[0]/FrameLayout[0]#content_view/LinearLayout[0]/NoScrollViewPager[0]#view_pager/NNFNewsListFragment[0]/FrameLayout[0]#content_view/FrameLayout[0]#list_fragment_container/NNFSmartRefreshLayout[0]#refreshLayout/RecyclerView[0]#rrv_news_infos/LinearLayout[0]
D/LazierTracker: attributes@title = 五年的内战把叙利亚变成了什么样子
D/LazierTracker: attributes@pageName = SampleFeedsActivity

viewPath支持正则匹配

针对RecyclerViewitem的业务数据采集,不建议为每个item进行配置,而是抽取item共性,采用正则表达式构造viewPath,使用正则表达式时,一个viewPath可匹配多个控件。

例如,本例中的viewPath可以改为 .*rrv_news_infos/(LinearLayout|RelativeLayout)\\[[0-9]+\\]$,从而匹配所有频道的新闻列表item。整体配置如下:

{
  "pageName": "SampleFeedsActivity",
  "viewPath": ".*rrv_news_infos/(LinearLayout|RelativeLayout)\\[[0-9]+\\]$",
  "eventType": "viewClick",
  "dataPath": "item.mNewsInfo",
  "subPath": [
    "infoId",
    "title"
  ]
}

待续

无埋点采集业务数据功能仍在探索中...

致谢

  1. 本项目buildsrc模块是字节码插桩插件源码,其开发灵感来源于开源项目HiBeaver。特此感谢。
  2. 业务数据采集原理参考网易乐得方案Android无埋点数据收集SDK关键技术。特此感谢。
  3. 为验证无埋点对复杂业务数据的采集效果,本项目app模块引入了网易有料Android UI SDK演示demo的部分示例代码,用于测试复杂业务场景。特此感谢。

About

本项目通过Android字节码插桩插件实现Android端无埋点(或自动埋点),并且支持根据配置文件实现业务数据的自动采集。

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Java 56.8%
  • Groovy 43.2%