Skip to content

Commit

Permalink
feat(tracker): 支持subPath
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangdan committed Mar 8, 2018
1 parent 92d45b9 commit e847018
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 37 deletions.
131 changes: 126 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
## 简介
# 简介
本项目通过Android字节码插桩插件实现Android端无埋点(或自动埋点),并且支持根据配置文件实现业务数据的自动采集。

为便于大家深入理解Android字节码插桩插件,特别梳理了一篇文章[应用于Android无埋点的Gradle插件解析](https://www.jianshu.com/p/250c83449dc0),供大家参考。

# 无埋点插件

## 原理

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

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

## 开发环境
- 语言:Groovy
- 字节码操作库:ASM5.0
- 工具:Android Studio 2.2(Mac)
- 工具:Android Studio 2.3.3(Mac)
- Gradle:1.5+

**注意事项**
Expand All @@ -17,7 +25,7 @@
android.enableD8=true
```

## 插件使用
## 使用

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

Expand Down Expand Up @@ -206,6 +214,119 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,

[目标方法对应的ASM字节码操作](/bytecodes.md)

## 致谢
# 业务数据采集

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

[configure.json](https://github.com/nailperry-zd/LazierTracker/blob/master/tracker/src/main/assets/configure.json)

## 示例1

假设页面布局如下:

![one_button](imgs/one_button.png)

根据我们的ViewPath计算规则(参考[网易HubbleData之Android无埋点实践](https://www.jianshu.com/p/8459a75ce5ca)),可知该按钮的ViewPath为:

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

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

![mTestField](imgs/mTestField.png)

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

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

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

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

```java
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](imgs/Example_RecyclerView.png)

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

```java
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](imgs/mNewsInfo.png)

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

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

```java
{
"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"
]
}
```

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

```java
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支持正则匹配**

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

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

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

## 待续

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

# 致谢

Android字节码插桩插件开发灵感来源于开源项目[HiBeaver](https://github.com/BryanSharp/hibeaver),特此感谢。
1. 本项目`buildsrc`模块是字节码插桩插件源码,其开发灵感来源于开源项目[HiBeaver](https://github.com/BryanSharp/hibeaver)。特此感谢。
2. 业务数据采集原理参考网易乐得方案[Android无埋点数据收集SDK关键技术](https://www.jianshu.com/p/b5ffe845fe2d)。特此感谢。
3. 为验证无埋点对复杂业务数据的采集效果,本项目`app`模块引入了[网易有料Android UI SDK演示demo](https://github.com/NetEaseYouliao/NewsFeeds-UI-Demo-Android)的部分示例代码,用于测试复杂业务场景。特此感谢。
15 changes: 7 additions & 8 deletions app/app.iml
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,20 @@
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/LazierTracker" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
Expand All @@ -99,7 +99,6 @@
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
Expand Down
18 changes: 15 additions & 3 deletions tracker/src/main/assets/configure.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,31 @@
"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/RelativeLayout[0]",
"eventType": "viewClick",
"dataPath": "item.mNewsInfo.title"
"dataPath": "item.mNewsInfo",
"subPath": [
"infoId",
"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.title"
"dataPath": "item.mNewsInfo",
"subPath": [
"infoId",
"title"
]
},
{
"pageName": "SampleFeedsActivity",
"viewPath": ".*rrv_news_infos/(LinearLayout|RelativeLayout)\\[[0-9]+\\]$",
"eventType": "viewClick",
"dataPath": "item.mNewsInfo.title"
"dataPath": "item.mNewsInfo",
"subPath": [
"infoId",
"title"
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
public class ConfigConstants {
public final static String PAGENAME = "pageName";
public final static String VIEWPATH = "viewPath";
public final static String VIEWPATHSUB = "subPath";
public final static String EVENTTYPE = "eventType";
public final static String DATAPATH = "dataPath";
public final static String BUSINESSDATA = "businessData";
Expand Down
20 changes: 16 additions & 4 deletions tracker/src/main/java/com/codeless/tracker/PluginAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.codeless.tracker.utils.PathUtil;
import com.codeless.tracker.utils.StringEncrypt;

import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -53,29 +54,40 @@ public static void onClick(View view) {
if (context instanceof Activity) {
String pageName = context.getClass().getSimpleName();
String currViewPath = PathUtil.getViewPath(view);
String eventId = StringEncrypt.Encrypt(pageName + currViewPath, StringEncrypt.DEFAULT);
Map<String, Object> configureMap = Tracker.instance(context).getConfigureMap();
if (null != configureMap) {
JSONArray nodesArr = (JSONArray) configureMap.get(pageName);
if (null != nodesArr && nodesArr.size() > 0) {
for (int i = 0; i < nodesArr.size(); i++) {
JSONObject nodeObj = nodesArr.getJSONObject(i);
String viewPath = nodeObj.getString(ConfigConstants.VIEWPATH);
String dataPath = nodeObj.getString(ConfigConstants.DATAPATH);
if (currViewPath.equals(viewPath) || PathUtil.match(currViewPath, viewPath)) {
// 按照路径dataPath搜集数据
Object businessData = PathUtil.getDataObj(view, nodeObj);
Object businessData = PathUtil.getDataObj(view, dataPath);
Map<String, Object> attributes = new HashMap<>();
attributes.put(ConfigConstants.PAGENAME, pageName);
attributes.put(ConfigConstants.VIEWPATH, currViewPath);
attributes.put(ConfigConstants.BUSINESSDATA, businessData);
Tracker.instance(context).trackEvent(pageName + currViewPath, attributes);
JSONArray subPaths = nodeObj.getJSONArray(ConfigConstants.VIEWPATHSUB);
if (null == subPaths || subPaths.size() == 0) {
attributes.put(ConfigConstants.BUSINESSDATA, businessData);
} else {
for (int j = 0; j < subPaths.size(); j++) {
String subPath = subPaths.getString(j);
Object obj = PathUtil.getDataObj(businessData, subPath);
attributes.put(subPath, obj);
}
}
Tracker.instance(context).trackEvent(eventId, attributes);
hasBusiness = true;
break;
}
}
}
}
if (!hasBusiness) {
Tracker.instance(context).trackEvent(pageName + currViewPath, null);
Tracker.instance(context).trackEvent(eventId, null);
}
}
}
Expand Down
22 changes: 12 additions & 10 deletions tracker/src/main/java/com/codeless/tracker/utils/PathUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,26 @@ public static String getResIdName(Context context, View view) {
}
}

public static Object getDataObj(View view, JSONObject nodeObj) {
public static Object getDataObj(Object obj, String dataPath) {
// 按照路径dataPath搜集数据
String dataPath = nodeObj.getString(ConfigConstants.DATAPATH);
String[] paths = dataPath.split("\\.");
Object refer = view;
Object refer = obj;
for (int j = 0; j < paths.length; j++) {
String path = paths[j];
switch (path) {
case ConfigConstants.START_THIS:
refer = view;
refer = obj;
break;
case ConfigConstants.START_ITEM:
// 路径的起点
Object viewParent = view.getParent();
if (ReflectorUtil.isInstanceOfV7RecyclerView(viewParent)) {
android.support.v7.widget.RecyclerView recyclerView = (android.support.v7.widget.RecyclerView) viewParent;
RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
refer = vh;
if (obj instanceof View) {
// 路径的起点
View view = (View) obj;
Object viewParent = view.getParent();
if (ReflectorUtil.isInstanceOfV7RecyclerView(viewParent)) {
android.support.v7.widget.RecyclerView recyclerView = (android.support.v7.widget.RecyclerView) viewParent;
RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
refer = vh;
}
}
break;
case ConfigConstants.KEY_CONTEXT:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.codeless.tracker.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
* Created by zhangdan on 16/12/26.
*/

public class StringEncrypt {
public static final String DEFAULT = "SHA-256";
/**
* Encrypting the string. Algorithms such as MD5, SHA-1, SHA-256; default SHA-256.
*
* @param strSrc the string to be Encrypted
* @param encName Algorithm for Encryption
* @return
*/
public static String Encrypt(String strSrc, String encName) {
MessageDigest md = null;
String strDes = null;

byte[] bt = strSrc.getBytes();
try {
if (encName == null || encName.equals("")) {
encName = "SHA-256";
}
md = MessageDigest.getInstance(encName);
md.update(bt);
strDes = bytes2Hex(md.digest()); // to HexString
} catch (NoSuchAlgorithmException e) {
return null;
}
return strDes;
}

public static String bytes2Hex(byte[] bts) {
String des = "";
String tmp = null;
for (int i = 0; i < bts.length; i++) {
tmp = (Integer.toHexString(bts[i] & 0xFF));
if (tmp.length() == 1) {
des += "0";
}
des += tmp;
}
return des;
}
}
Loading

0 comments on commit e847018

Please sign in to comment.