{"id":19168093,"url":"https://github.com/nailperry-zd/laziertracker","last_synced_at":"2025-04-05T20:06:12.662Z","repository":{"id":109998837,"uuid":"83945431","full_name":"nailperry-zd/LazierTracker","owner":"nailperry-zd","description":"本项目通过Android字节码插桩插件实现Android端无埋点（或自动埋点），并且支持根据配置文件实现业务数据的自动采集。","archived":false,"fork":false,"pushed_at":"2018-03-08T11:25:53.000Z","size":1250,"stargazers_count":521,"open_issues_count":13,"forks_count":113,"subscribers_count":14,"default_branch":"master","last_synced_at":"2025-03-29T19:03:13.298Z","etag":null,"topics":["android","aop","asm","autotrack","autotracker","bytecode","codeless","gradle","gradle-plugin"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nailperry-zd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-03-05T04:45:40.000Z","updated_at":"2025-02-25T07:51:21.000Z","dependencies_parsed_at":"2023-05-22T22:59:00.210Z","dependency_job_id":null,"html_url":"https://github.com/nailperry-zd/LazierTracker","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nailperry-zd%2FLazierTracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nailperry-zd%2FLazierTracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nailperry-zd%2FLazierTracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nailperry-zd%2FLazierTracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nailperry-zd","download_url":"https://codeload.github.com/nailperry-zd/LazierTracker/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247393569,"owners_count":20931812,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","aop","asm","autotrack","autotracker","bytecode","codeless","gradle","gradle-plugin"],"created_at":"2024-11-09T09:41:10.877Z","updated_at":"2025-04-05T20:06:12.633Z","avatar_url":"https://github.com/nailperry-zd.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 简介\n本项目通过Android字节码插桩插件实现Android端无埋点（或自动埋点），并且支持根据配置文件实现业务数据的自动采集。\n\n# 无埋点插件\n\n为便于大家深入理解Android字节码插桩插件，特别梳理了一篇文章[应用于Android无埋点的Gradle插件解析](https://www.jianshu.com/p/250c83449dc0)，供大家参考。\n\n## 原理\n\n试想一下我们代码埋点的过程：首先定位到事件响应函数，例如Button的onClick函数，然后在该事件响应函数中调用SDK数据搜集接口。\n\n我们的gradle插件采用 Android gradle 插件提供的最新的Transform API，在Apk编译环节中、class打包成dex之前，插入了中间环节，调用 ASM API对class文件的字节码进行扫描，当扫描到目标事件响应函数时，在函数头部或尾部插入SDK数据搜集代码。\n\n## 开发环境\n- 语言：Groovy\n- 字节码操作库：ASM5.0\n- 工具：Android Studio 2.3.3(Mac)\n- Gradle：1.5+\n\n**注意事项**\n\n在AS 3.0中，需要在`project`的 `gradle.properties`中添加\n\n```java\nandroid.enableD8=true\n```\n\n## 使用\n\n使用Android字节码插桩插件，您可能需要做些自定义的配置，比如在`ReWriterConfig`中配置注入代码的类名及待注入的方法映射\n\n例如\n\n\tpublic static String sAgentClassName = 'com/codeless/tracker/PluginAgent'\n\t\n\tsInterfaceMethods.put('onClick(Landroid/view/View;)V', new MethodCell(\n\t                'onClick',\n\t                '(Landroid/view/View;)V',\n\t                'android/view/View$OnClickListener',\n\t                'onClick',\n\t                '(Landroid/view/View;)V',\n\t                1, 1,\n\t                [Opcodes.ALOAD]))\n上述代码表明当一个`Activity`或`Fragment`实现了`View$OnClickListener`接口时，使用本插件遍历到该`Activity`或`Fragment`字节码中的`onClick(View v)`时，向该方法中插入`com.codeless.tracker.PluginAgent.onClick(v)`。`com.codeless.tracker.PluginAgent`中的`onClick(View v)`方法即是您想要注入到点击事件响应`onClick`中的代码。\n\n### 1. 本地插件集成       \n\n`app`的`build.grade`中添加\n\t\n\t// 直接引用buildsrc的插件类\n\tapply plugin: com.codeless.plugin.InjectPluginImpl\n\n### 2. 自定义参数\n\n`app`的`build.grade`中添加如下代码，各配置项的含义请参考英文注释\n\n```\ncodelessConfig {\n    //this will determine the name of this plugin transform, no practical use.\n    pluginName = 'myPluginTest'\n    //turn this on to make it print help content, default value is true\n    showHelp = true\n    //this flag will decide whether the log of the modifying process be printed or not, default value is false\n    keepQuiet = false\n    //this is a kit feature of the plugin, set it true to see the time consume of this build\n    watchTimeConsume = false\n\n    //this is the most important part, 3rd party JAR packages that want our plugin to inject;\n    //our plugin will inject package defined in 'AndroidManifest.xml' and 'butterknife.internal.butterknife.internal.DebouncingOnClickListener' by default.\n    //structure is like ['butterknife.internal','com.a.c'], type is HashSet\u003cString\u003e.\n    //You can also specify the name of the class;\n    //example: ['com.xxx.xxx.BaseFragment']\n    targetPackages = []\n}\n```\n\n### 3. 远程插件集成\n\n这一步需要您修改好`ReWriterConfig`后，发布插件到远程仓库，然后在app中引用远程插件。具体步骤请参考[Codeless-Gradle-Plugin-Repo](https://github.com/nailperry-zd/Codeless-Gradle-Plugin-Repo)\n\n## 支持插桩的目标方法\n\n### 1. 目标方法在Fragment中声明\n\n目标方法：\n\n- onResume()V\n- onPause()V\n- setUserVisibleHint(Z)V\n- onHiddenChanged(Z)V\n\n具体实现：\n\n- 对app中指定包进行扫描，筛选出所有父类为`android/app/Fragment`或`android/support/v4/app/Fragment`的类。\n- 对这些Fragment子类的`onResumed`，`onPaused`，`onHiddenChanged`，`setFragmentUserVisibleHint`方法的字节码进行修改，添加数据采集代码。\n\n目标效果：\n\n```\npublic class BaseFragment extends Fragment {\n    public BaseFragment() {\n    }\n\n    public void onResume() {\n        super.onResume();\n        PluginAgent.onFragmentResume(this);\n    }\n\n    public void onHiddenChanged(boolean var1) {\n        super.onHiddenChanged(var1);\n        PluginAgent.onFragmentHiddenChanged(this);\n    }\n\n    public void onPause() {\n        super.onPause();\n        PluginAgent.onFragmentPause(this);\n    }\n\n    public void setUserVisibleHint(boolean var1) {\n        super.setUserVisibleHint(var1);\n        PluginAgent.setFragmentUserVisibleHint(this, var1);\n    }\n}\n```\n\n### 2. 目标方法在接口中声明\n\n目标方法：\n\n- onClick(Landroid/view/View;)V\n- onClick(Landroid/content/DialogInterface;I)V\n- onItemClick(Landroid/widget/AdapterView;Landroid/view/View;IJ)V\n- onItemSelected(Landroid/widget/AdapterView;Landroid/view/View;IJ)V\n- onGroupClick(Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z\n- onChildClick(Landroid/widget/ExpandableListView;Landroid/view/View;IIJ)Z\n- onRatingChanged(Landroid/widget/RatingBar;FZ)V\n- onStopTrackingTouch(Landroid/widget/SeekBar;)V\n- onCheckedChanged(Landroid/widget/CompoundButton;Z)V\n- onCheckedChanged(Landroid/widget/RadioGroup;I)V\n- ...\n\n具体实现：\n\n- 对app中指定包进行扫描，筛选出实现了目标接口的类，在目标方法中添加数据采集代码。\n\n\u003e 例如，筛选出实现了`android/view/View$OnClickListener`接口的类，然后在`onClick(Landroid/view/View;)V`方法中注入采集数据的代码。\n\n目标效果：\n\n```\npublic class MainActivity extends AppCompatActivity implements OnClickListener, android.content.DialogInterface.OnClickListener, OnItemClickListener, OnItemSelectedListener, OnRatingBarChangeListener, OnSeekBarChangeListener, OnCheckedChangeListener, android.widget.RadioGroup.OnCheckedChangeListener, OnGroupClickListener, OnChildClickListener {\n    public MainActivity() {\n    }\n\n    protected void onCreate(Bundle var1) {\n        super.onCreate(var1);\n        this.setContentView(2130968603);\n    }\n\n    public void onClick(View var1) {\n        PluginAgent.onClick(var1);\n    }\n\n    public void onClick(DialogInterface var1, int var2) {\n        PluginAgent.onClick(this, var1, var2);\n    }\n\n    public void onItemClick(AdapterView\u003c?\u003e var1, View var2, int var3, long var4) {\n        PluginAgent.onItemClick(this, var1, var2, var3, var4);\n    }\n\n    public void onItemSelected(AdapterView\u003c?\u003e var1, View var2, int var3, long var4) {\n        PluginAgent.onItemSelected(this, var1, var2, var3, var4);\n    }\n\n    public void onNothingSelected(AdapterView\u003c?\u003e var1) {\n    }\n\n    public void onCheckedChanged(CompoundButton var1, boolean var2) {\n        PluginAgent.onCheckedChanged(this, var1, var2);\n    }\n\n    public boolean onChildClick(ExpandableListView var1, View var2, int var3, int var4, long var5) {\n        PluginAgent.onChildClick(this, var1, var2, var3, var4, var5);\n        return false;\n    }\n\n    public boolean onGroupClick(ExpandableListView var1, View var2, int var3, long var4) {\n        PluginAgent.onGroupClick(this, var1, var2, var3, var4);\n        return false;\n    }\n\n    public void onCheckedChanged(RadioGroup var1, int var2) {\n        PluginAgent.onCheckedChanged(this, var1, var2);\n    }\n\n    public void onRatingChanged(RatingBar var1, float var2, boolean var3) {\n        PluginAgent.onRatingChanged(this, var1, var2, var3);\n    }\n\n    public void onProgressChanged(SeekBar var1, int var2, boolean var3) {\n    }\n\n    public void onStartTrackingTouch(SeekBar var1) {\n    }\n\n    public void onStopTrackingTouch(SeekBar var1) {\n        PluginAgent.onStopTrackingTouch(this, var1);\n    }\n}\n```\n\n## ASM语法实战\n\n[目标方法对应的ASM字节码操作](/bytecodes.md)\n\n# 业务数据采集\n\n业务数据的采集需要下发json格式的配置文件，该文件本该由埋点服务器下发。这里为了演示方便，将配置文件放在`tracker`模块的`assets`目录下。\n\n[configure.json](https://github.com/nailperry-zd/LazierTracker/blob/master/tracker/src/main/assets/configure.json)\n\n## 示例1\n\n假设页面布局如下：\n\n![one_button](imgs/one_button.png)\n\n根据我们的ViewPath计算规则（参考[网易HubbleData之Android无埋点实践](https://www.jianshu.com/p/8459a75ce5ca)），可知该按钮的ViewPath为：\n\n```java\n/MainWindow/ContentFrameLayout[0]#android:content/RelativeLayout[0]#activity_main/AppCompatButton[0]#btn\n```\n\n现在希望点击按钮后，搜集该按钮所在Activity的成员变量`mTestField`的值，如下图所示：\n\n![mTestField](imgs/mTestField.png)\n\n根据网易乐得提出的数据路径DSL语言规则（参考[Android无埋点数据收集SDK关键技术](https://www.jianshu.com/p/b5ffe845fe2d)），可知变量`mTestField`的数据引用路径应该表示为`this.context.mTestField`。\n\n因此，上述业务数据搜集需求可用如下配置表示：\n\n```java\n{\n  \"pageName\": \"MainActivity\",\n  \"viewPath\": \"/MainWindow/ContentFrameLayout[0]#android:content/RelativeLayout[0]#activity_main/AppCompatButton[0]#btn\",\n  \"eventType\": \"viewClick\",\n  \"dataPath\": \"this.context.mTestField\"\n}\n```\n\n运行工程，触发事件，无埋点采集到的数据效果如下：\n\n```java\nD/LazierTracker: 成功打点事件-\u003e@eventId = 73acfdf0c708e1dc3f90f4611da2569167872469c6ae697e51688fa7207eef62\nD/LazierTracker: attributes@businessData = 我是测试变量\nD/LazierTracker: attributes@viewPath = /MainWindow/ContentFrameLayout[0]#android:content/RelativeLayout[0]#activity_main/AppCompatButton[0]#btn\nD/LazierTracker: attributes@pageName = MainActivity\n```\n\n## 示例2\n\n如图所示：\n\n![Example_RecyclerView](imgs/Example_RecyclerView.png)\n\n该页面列表由RecyclerView实现，红框圈住的视图为RecyclerView的第0个item，该视图对应的ViewPath为：\n\n```java\nMainWindow/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]\n```\n\n现在希望点击按钮后，搜集该视图中新闻的`infoId`，变量`infoId`未展示在界面上，但在内存中。为了拿到该变量值，需要拿到该视图对应的数据源。做法是，先拿到`RecyclerView`第0项的`ViewHolder`，看代码可知是`NewsInfoHolderTriS`，而`NewsInfoHolderTriS`继承`NewsInfoHolderNone`，该视图对应的数据源即是`NewsInfoHolderNone`中的成员变量`mNewsInfo`，如下图所示：\n\n![mNewsInfo](imgs/mNewsInfo.png)\n\n`mNewsInfo`中包含具体的`infoId`、`title`等字段，按此描述，数据引用路径应该为`item.mNewsInfo.infoId`\u0026`item.mNewsInfo.title`\n\n因此，上述业务数据搜集需求可用如下配置表示：\n\n```java\n{\n  \"pageName\": \"SampleFeedsActivity\",\n  \"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]\",\n  \"eventType\": \"viewClick\",\n  \"dataPath\": \"item.mNewsInfo\",\n  \"subPath\": [\n    \"infoId\",\n    \"title\"\n  ]\n}\n```\n\n运行工程，触发事件，无埋点采集到的数据效果如下：\n\n```java\nD/LazierTracker: 成功打点事件-\u003e@eventId = b9f3be79bad49fb69fc7508f79702fc044da4d2e695432d9df0b62a41c37c740\nD/LazierTracker: attributes@infoId = II2TH7RYJ1VOUR0\nD/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]\nD/LazierTracker: attributes@title = 五年的内战把叙利亚变成了什么样子\nD/LazierTracker: attributes@pageName = SampleFeedsActivity\n```\n\n**viewPath支持正则匹配**\n\n针对`RecyclerView`item的业务数据采集，不建议为每个item进行配置，而是抽取item共性，采用正则表达式构造viewPath，使用正则表达式时，一个viewPath可匹配多个控件。\n\n例如，本例中的viewPath可以改为\n`.*rrv_news_infos/(LinearLayout|RelativeLayout)\\\\[[0-9]+\\\\]$`，从而匹配所有频道的新闻列表item。整体配置如下：\n\n```java\n{\n  \"pageName\": \"SampleFeedsActivity\",\n  \"viewPath\": \".*rrv_news_infos/(LinearLayout|RelativeLayout)\\\\[[0-9]+\\\\]$\",\n  \"eventType\": \"viewClick\",\n  \"dataPath\": \"item.mNewsInfo\",\n  \"subPath\": [\n    \"infoId\",\n    \"title\"\n  ]\n}\n```\n\n## 待续\n\n无埋点采集业务数据功能仍在探索中...\n\n# 致谢\n\n1. 本项目`buildsrc`模块是字节码插桩插件源码，其开发灵感来源于开源项目[HiBeaver](https://github.com/BryanSharp/hibeaver)。特此感谢。\n2. 业务数据采集原理参考网易乐得方案[Android无埋点数据收集SDK关键技术](https://www.jianshu.com/p/b5ffe845fe2d)。特此感谢。\n3. 为验证无埋点对复杂业务数据的采集效果，本项目`app`模块引入了[网易有料Android UI SDK演示demo](https://github.com/NetEaseYouliao/NewsFeeds-UI-Demo-Android)的部分示例代码，用于测试复杂业务场景。特此感谢。","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnailperry-zd%2Flaziertracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnailperry-zd%2Flaziertracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnailperry-zd%2Flaziertracker/lists"}