Code Monkey home page Code Monkey logo

laziertracker's Introduction

简介

本项目通过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的部分示例代码,用于测试复杂业务场景。特此感谢。

laziertracker's People

Contributors

nailperry-zd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laziertracker's Issues

README.MD

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

问个问题这个插件的正确引入方式是什么?

我通过Module方式引入,在build.gradle中按照文档中的方式引入会报错:
Error:(266, 0) Could not find method codelessdaConfig() for arguments [build_7d4m8ark868yosfs5856t9l9k$_run_closure7@21635d9d] on project ':app' of type org.gradle.api.Project.
Open File

大牛您好,一个关于使用的问题

`
OkHttpClient mOkHttpClient = new OkHttpClient();
//创建一个Request
final Request request = new Request.Builder()
.url("https://github.com/hongyangAndroid")
.build();
//new call
Call call2 = mOkHttpClient.newCall(request);
//请求加入调度
call2.enqueue(new Callback() {

        @Override
        public void onFailure(Call call, IOException e) {
            Log.e("@@@@", "失败");
        }

        @Override
        public void onResponse(okhttp3.Call call, okhttp3.Response response) throws IOException {

            Log.e("@@@@", "成功");
        }
    });

`

sInterfaceMethods.put('onFailure(Lokhttp3/Call;Ljava/io/IOException)V', new MethodCell( 'onFailure', '(Lokhttp3/Call;Ljava/io/IOException)V', 'okhttp3/Callback', 'onFailure', '(Lokhttp3/Call;Ljava/io/IOException)V', 1, 2, [Opcodes.ALOAD, Opcodes.ALOAD])) sInterfaceMethods.put('onResponse(Lokhttp3/Call;Lokhttp3/Response)V', new MethodCell( 'onResponse', '(Lokhttp3/Call;Lokhttp3/Response)V', 'okhttp3/Callback', 'onResponse', '(Lokhttp3/Call;Lokhttp3/Response)V', 1, 2, [Opcodes.ALOAD, Opcodes.ALOAD]))

大牛您好,请问一下 我想在网络请求这块插入代码 这样写有什么问题吗,就在回调接口onFailure 和onResponse中调用

className处理出错

代码位置: InjectTransform-isJarNeedModify(File jarFile)

    className = entryName.replace("/", ".").replace(".class","")

出现问题: 仅后一个替换.class字符串的 函数生效.... className中的/符号并没有被替换掉

拆成俩条语句之后生效...

     className = entryName.replace("/", ".")
     className = className.replace(".class", "")

view变化的问题

如何确定view对应的唯一业务id,事实上这样仍然是有概率出现view_path的变化的吧,比如同级插入一个类型相同无id的view,或者版本变更

Execution failed for task ':app:transformClassesWithDexBuilderForDebug'. DEX打包错误

在打最后一个class的文件包为dex的时候,报错,如果不修改class是没有问题的,请问有什么解决方案吗?非常感谢!

  • What went wrong: 16:00:15.917 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] Execution failed for task ':app:transformClassesWithDexBuilderForDebug'. 16:00:15.917 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] > com.android.build.api.transform.TransformException: com.android.builder.dexing.DexArchiveBuilderException: com.android.builder.dexing.DexArchiveBuilderException: Failed to process /Users/MyDemos/ASMAndroidDemo/app/build/intermediates/transforms/inject transform for asm/debug/34 16:00:15.917 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]

Jar修改的时候匹配有问题

调用modifyJarFile的时候,path2Classname有问题。

class文件,路径名是android\arch\core\R.class这种形式,path2Classname的正则用File.separator能替换成android.arch.core.R.class。

但是jar文件,路径名是com/bumptech/glide/BuildConfig.class这种形式,正则里的File.separator就不起效,会导致jar扫描的时候没法匹配到期望的目标类。

view变化的问题

如何确定view对应的唯一业务id,事实上这样仍然是有概率出现view_path的变化的吧,比如同级插入一个类型相同无id的view,或者版本变更

rebuild project got error

first build is perfect,but when change some code and rebuild it ,got this error:

Error:Execution failed for task ':app:transformClassesWithLazierTrackerForDebug'.

com/codeless/plugin/InjectTransform$_transform_closure1

how can i fix it?

codelessConfig 配置必须要把本地的仓库发布到远端吗

本地添加codelessConfig配置后报错
class InjectPluginImpl implements Plugin {
@OverRide
void apply(Project project) {
println "myPlugin here"
project.extensions.create('codelessConfig', InjectPluginParams)
registerTransform(project)
initDir(project);
。。。。

groovy.lang.MissingPropertyException: No such property: InjectPluginParams for class: com.ant.statisticplugin.InjectPluginImpl
at com.ant.statisticplugin.InjectPluginImpl.apply(InjectPluginImpl.groovy:14)
at com.ant.statisticplugin.InjectPluginImpl.apply(InjectPluginImpl.groovy)

关于path

path 维护成本高,请问为什么不采用页面Activity+resIdName的方式匹配

Return code 1 for dex process

Execution failed for task ':app:transformClassesWithDexForDebug'.

com.android.build.api.transform.TransformException: java.lang.RuntimeException: com.android.ide.common.process.ProcessException: java.util.concurrent.ExecutionException: com.android.ide.common.process.ProcessException: Return code 1 for dex process

我在您的代码里新增了如下代码:
sFragmentMethods.put('onAttach(Landroid/content/Context;)V', new MethodCell(
'onAttach',
'(Landroid/content/Context;)V',
'',// parent省略, android/app/Fragment 或 android/support/v4/app/Fragment
'Companion.onFragmentAttach',
'(Ljava/lang/Object;)V',
0, 1,
[Opcodes.ALOAD]))
我知道是我哪里没有写对,但是通过借鉴您的代码和查阅ASM文档后发现不了问题,忘能得到指正

我自己发布了一个插件,这个插件只有在你的demo里是没问题的,但是我新建一个功能,配置和你的demo一模一样,仍然是报这个错

AS 3.3.2编译不过

Could not find project build script dependency on org.jacoco.core

如果我主动引入jacoco,就提示我gradle版本不对

你好

    private static void saveModifiedJarForCheck(File optJar) {
        File dir = DataHelper.ext.pluginTmpDir;
        File checkJarFile = new File(dir, optJar.getName());
        if (checkJarFile.exists()) {
            checkJarFile.delete();
        }
        FileUtils.copyFile(optJar, checkJarFile);
    }

请问这段代码,只是用来检查修改的jar包的对吗

請問 应用于Android无埋点的Gradle插件解析

大神你好

再次謝謝你的文章    应用于Android无埋点的Gradle插件解析

https://github.com/nailperry-zd/LazierTracker/wiki/%E5%BA%94%E7%94%A8%E4%BA%8EAndroid%E6%97%A0%E5%9F%8B%E7%82%B9%E7%9A%84Gradle%E6%8F%92%E4%BB%B6%E8%A7%A3%E6%9E%90

我想請問一下
om.android.build.gradle.internal.plugins.AppPlugin

com.android.build.gradle.AppPlugin  

除了一個是  Java 一個是  kotlin之外有啥差異啊?

另外

1.  目前看到分析source code的方式大多是 dependencies 處引入

例如

compile 'com.android.tools.build:gradle:3.0.1'

但是這只有部分的code 

根據 https://android.googlesource.com/platform/tools/base 實際上有其他相依性的部分

我查找了一下

gradle 在AS  (Android Studio)在AS成熟後有3次大改版

gradle_3.0.0  /gradle_3.1.2 /gradle_3.4.0

分別是
 https://android.googlesource.com/platform/tools/base/+/refs/tags/gradle_3.0.0/build-system/
                           https://android.googlesource.com/platform/tools/base/+/refs/tags/gradle_3.1.2/build-system/
             
https://android.googlesource.com/platform/tools/base/+/refs/tags/gradle_3.4.0/build-system/

請問這該如何完整提取? 變且編譯出來?

又目前是3.6.3甚至更新

有辦法sync到 3.6.3甚至目前版本嗎?

  1. 大神看來有在研究Android Gradle Plugin    其中Plugin中有  createTaskManager 裡面有DexArchiveBuilderTask

請問我該如何 在 Transform 後   『單獨』 調用這個task  ?

因為想要借用來  將一個裡面都是 class  的 folder轉換成  dex檔案

又或者有參考改寫的範例嗎?

因為我設計了一個  Transform 根據 dependencies  在  jarInputs 階段 輸出與 解壓縮 

我指定的jar

Looks good! keep working!

at stack depth 0, expected type java.lang.Object but found int
这个问题似乎不是你调用方法传参的问题,而是你使用aload指令操作的栈上的数据类型不对,似乎应该使用iload

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.