Android中使用ASPECTJ进行用户操作路径跟踪与日志搜集
编写初衷
在Android App开发中,出现了bug和崩溃测试们就会提着手机上门,然后开发一顿操作,bug消失了,测试们又只有进行大量的操作来复现。
这样的情况想必大家都遇到过,更极端的是线上出现了bug,虽然可以设置崩溃日志上传来收集崩溃日志,但是用户是怎么操作的,我们也只能靠猜
为什么不能有一个工具,记录下最近打开了什么界面,点击了哪些按钮,并且记录到本地,方便开发们查看呢?于是笔者有了写这个工具的想法。
我们来缕一缕:我们的工具要实现的几个功能
- 记录activity的打开和关闭情况
- 记录fragment的打开和关闭情况
- 记录控件的点击情况
- 把这些日志存放到本地,方便上传和查阅
用什么技术实现
有了目标,我们就可以考虑怎样实现了,大概有以下几种方式实现操作收集
这种技术有个名称,叫做埋点,因为我们就像埋地雷一样在指定位置设置代码,当触发的时候打日志并收集
- 人工手动埋点
- 使用特殊控件,原理与1类似,不过工作转移给了写控件的人
- 使用aop框架进行自动化埋点,比如aspectj或者asm
- AccessibilityService配合ContentDescription进行埋点
选择哪项技术呢,我们逐项分析
- 虽然最简单,但是工作量大,容错错埋漏埋,放弃
- 工作量巨大,使用者学习成本高,放弃
- 使用简单,通用,考虑使用
- 需要开发时添加ContentDescription并且某些机型无法开启AccessibilityService相关设置,有局限性
综合考虑之后,我们选择了使用aop的方式埋点,我们现在有两个选择,aspectj或者asm,但是有于asm过于灵活学习和使用均有一定门槛,所以我们选择简单易用的aspectj
基本思路
通过对aspectj的学习,我们发现可以对onclick方法以及生命周期方法进行切点设置,至于什么是切点,将会在后续部分解释
日志方面,我们采用了开源项目ZLog进行记录,该框架实现了日志写到文件,以及对文件的数量和大小管理功能,比较方便
对于最后一项,由于使用itembinding等框架的时候会遇到捕获不到具体类的情况,所以按需要进行捕捉
关于Aspectj
这个工具最关键的还是怎样使用aspectj去做埋点,关于aspectj的使用网上有很多例子,这里就不赘述了,下面贴出一个简单的使用文件,大家配合注解看一下,如果还是不太明白建议先去搜一些基本使用的帖子
@Aspect//标注这个类是一个aspectj需要处理的类
class AspectJTest {
@Pointcut("execution(void _internalCallbackOnClick(..))")//切点,检测返回值为void的_internalCallbackOnClick方法
fun onBindingClick() {
}
@Around("onBindingClick()")//在合适的时机对切点进行处理
@Throws(Throwable::class)
fun onClickMethodBinding(joinPoint: ProceedingJoinPoint) {
val args = joinPoint.args//获取方法参数
if (args.size >= 1 && args[1] is View) {
val view = args[1] as View//获取view
val id = view.id
//处理该view或者打印日志
}
joinPoint.proceed() //执行原来的代码
}
}
需要解决的问题
虽然看起来aspectj用起来很简单,但还是有一些问题需要我们处理
- 怎样获取点击事件的控件id和它所在的类名
- 怎么获取在list中的点击事件,并获取它的位置信息
关于第一点,我们普通使用setOnClick设置是可以拿到的,但是当使用databinding等技术的时候情况就比较复杂了,我们如果使用以下代码,就会获取到生成类里的一个onclick文件,根本不知道哪个页面的控件被点击了@Around("onClick()") @Throws(Throwable::class) fun onClickMethodAround(joinPoint: ProceedingJoinPoint) { //获取点击事件view对象及名称,可以对不同按钮的点击事件进行统计 val target = joinPoint.target var className = "" if (target != null) { className = target.javaClass.name if (className.contains("$")) { className = className.split("\\$").toTypedArray()[0] } if (className.contains("_ViewBinding")) { className = className.split("_ViewBinding").toTypedArray()[0] } }//看似可以获取,但是实际使用itembinding的时候只能拿到生成的onclick文件名,没有其他信息 joinPoint.proceed() //执行原来的代码 }
要处理这个问题,我们需要监听生成类中的点击事件,在笔者这里,这个事件方法名叫做_internalCallbackOnClick,我们添加一个对它的切点就可以了
对于问题2,我们可以通过获取view的父view,并判断它的类型,再通过强转来解决,代码如下
val view = args[0] as View
var index = -1
if (view.parent is RecyclerView) {
index = (view.parent as RecyclerView).getChildPosition(view)
}
输出
当完成了埋点之后,我们就可以把日志输出到文件里了,这里我使用了一个叫ZLog的库,不过由于有一些年头了,我直接复制了文件到项目中。如果大家有兴趣可以去看看ZLog代码仓库并点个star
总结与完全代码
解决了上面的问题,我们就可以检测到想要的信息并保存了,以后测试来找我们的时候排查bug又多了一点点线索,线上用户报bug的时候也不用胡乱猜测了,是不是感觉很有用呢?(可能并没有
如果想看完全的代码,可以到github上的这里来看看如果能顺便点个star就再好不过了,如果有任何问题,也可以留言讨论