注册

😳骚操作玩这么花的吗?Android基于Act实现事件的录制与回放!

基于Activity封装实现录制与回放


前言


在前文中我们通过 ViewGr0up 实现过自己的录制与回放,但是那只是用于复(学)习,并不能真正在实际开发中应用上,或者说能用但是不好用需要大量的修改,


而大厂实现的录制与回放方案有很多种但大多都没有开源。一般在大厂会对应用的稳定性进行监控,不管是测试还是线上监控,都离不开用户操作的录制与回放。


一个 App 开发完成上架之后,一般我们会收集用户设备的内存帧率,崩溃信息,ANR信息等,这些都是基操,但是现在平台会提出了更高的要求,录制用户操作与回放用户操作,很多大厂都在进行这方面的探索。


目前业内做的比较好的录制与回放稳定性平台搭建包括不限于美团,爱奇艺,字节,网易,货拉拉等。


不同于测试阶段可以用 PC + ADB 实现录制与操作的思路,在应用内部我们就需要预先埋点用户的事件操作与回放逻辑,并且生成对应的日志信息。


那么实现录制与回放有哪些方法?哪一种更方便呢?本文只是探讨一下基于 Activity 实现的,比较简单的、比较基本的录制与回放功能,方便大家参考。


当然本文只是基于 Demo 性质,只用于本机录制本机回放,如果真要做到兼容多平台多设备,如需要ORC文本识别与图片识别进行定位,屏幕大小适配坐标等其他一系列的深入优化就不在本文的探讨范围。其实只要实现了核心功能,其他都是细枝末节需要时间打磨。


那么话不多说,Let's go


300.png


一、定义事件


在前文 ViewGr0up 的文章中,我们知道了事件的伪造与保存,如何定制伪造事件时间轴,如何分发伪造事件,本文也是一个思路。


整体思路基于前文 ViewGr0up 的例子,还是把事件用对象封装起来,只是我们封装的对象换成了 MotionEvent ,并且不需要修改内部的操作时间了,我们用事件对象的 time 时间来制作伪造事件触发的时间轴。


这样对于事件的录制我们就能直接通过 Activity 的事件分发 dispatchTouchEvent 中直接保存我们的事件对象了。


基于这个思路,我们的事件的对象封装:


public class EventState {
public MotionEvent event; //事件
public long time; //开始录制到该事件发生的时间
}

Activity的事件集合,方便后期扩展为多个Activity的事件队列,如果只需要录制一个 Activity 的事件那么则可以无需双重队列。


/**
* 以Activity为单位,以队列的形式存储MotionEvent
*/

public class ActEventStates {
/**
* 存储元素为一个队列,存放一个Act中的操作状态。如果有多个Act,则是双重队列
*/

public static Queue<Queue<EventState>> eventStates = new LinkedList<>();

public static boolean isRecord = false; //是否在录制

public static boolean isPlay = false; //是否在播放
}

为什么要用 Queue ?


首先我们只需要回放一次,如果想回放多次可以用持久化存储,对于已经回放过的事件我们不希望还存在内存中,特别是后期做多 Activity 之间的跳转之后的回放,如果之前的事件还存在内存中会有重复回放的问题,而用 List 去手动管理没有 Queue 方便。


二、录制


先定义一个开始与停止的方法:


  //开启录制
fun startRecord() {
//如果是录制状态
if (ActEventStates.isRecord) {
ActEventStates.isPlay = false

//初始化队列,对应一个Act是一个队列
activityEvents = LinkedList()
// Act录制事件的开始时间
startTime = System.currentTimeMillis()
//保存到内存中
ActEventStates.eventStates.add(activityEvents)
}
}

//停止录制
fun stopRecord() {
val state = EventState()
state.event = null
state.time = System.currentTimeMillis() - startTime
activityEvents?.add(state)
}

基于Act的录制,直接在分发事件的时候把事件从 Activity 级别就录制进去,这样只要在 Activity 层级之下的操作都能实现录制与回放了:


    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
//只有在录制状态下才会保存事件并添加到队列中
if (ActEventStates.isRecord && activityEvents != null) {
//不要直接存原始的 MotionEvent,因为用过就回收的,之前我们是通过自定义 Event 来做的,这里简单一点直接重新伪造一次其实更方便
val obtain = MotionEvent.obtain(ev)
//初始化自己的 EventState 用于保存当前事件对象
val state = EventState()
//赋值当前事件,用伪造过的事件
state.event = obtain
//赋值当前事件发生的时间
state.time = System.currentTimeMillis() - startTime
//把每一次事件 EventState 对象添加到队列中
activityEvents?.add(state)
}
return super.dispatchTouchEvent(ev)
}

每一行代码都尽量给出注释。


三、回放


其实和我们之前的 ViewGr0up 的思路是一致的,只是把自定义的事件换成原生的 MotionEvent 来保存,还是根据 Handler 分发不同事件的时间轴。


    //回放录制
fun playRecord() {
//如果是播放状态
if (ActEventStates.isPlay) {
ActEventStates.isRecord = false

//延时1秒开始播放
handler.postDelayed({
Thread {
if (!ActEventStates.eventStates.isEmpty()) {
//遍历每一个Act的事件,支持多个Act的录制与回放
val pop = ActEventStates.eventStates.remove()
while (!pop.isEmpty()) {
val state = pop.remove()
//根据事件的时间顺序播放
handler.postDelayed({
if (state.event == null) {
YYLogUtils.w("没了,回放录制完成")
} else {
dispatchTouchEvent(state.event)
}
}, state.time)

}
}
}.start()
}, 1000)
}
}

在当前的 Activity 中录制与回放的效果,具体的使用与效果:


    startRecode.click {
ActEventStates.isRecord = true
toast("开始录制")
startRecord()
}

endRecode.click {
ActEventStates.isRecord = false
toast("停止录制")
stopRecord()
}

//点击回放
btnReplay.click {
ActEventStates.isPlay = true
toast("回放录制")
playRecord()
}

act_record01.gif


单独的 Activity 上录制与回放是可以了,但是我们的应用又不是 Compose 或 Flutter,我们大部分项目还是多 Activity 的,如何实现多 Activity 跳转之后的录制与回放才是真正的问题。


四、多Activity的录制与回放


由于我们之前定义的数据格式就是 Queue 队列,所以我们很方便的就能实现多 Activity 的录制与回放效果,只需要在每一个 Activity 的 onResume 方法中尝试录制与播放即可。


由于当前的 Queue 的数据格式的性质,回放完成之后就没有了,跳转 Activity 之后就无需从头开始播放,特别适合这个场景。


只是需要注意的点是 Activity 的返回除了 Appbar 的页面返回按钮点击,我们还能使用系统的返回键或国产OS的左侧右侧滑动返回操作,所以我们需要对系统的返回操作单独做处理,修改之后的核心代码如下:


abstract class BaseActivity<VM : BaseViewModel> : AbsActivity() {

...

// ================== 事件录制 ======================

var handler = Handler(Looper.getMainLooper())

/**
* 存放当前activity中的事件
*/

private var activityEvents: Queue<EventState>? = null

/**
* 当前activity可见之后的时间点,每次 onResume 之后都创建一个新的队列,同时也赋值新的statetime
*/

private var startTime: Long = 0


override fun onResume() {
super.onResume()
startRecord() //尝试录制
playRecord() //尝试回放
}

//开启录制
protected fun startRecord() {
//如果是录制状态
if (ActEventStates.isRecord) {
ActEventStates.isPlay = false

//初始化队列,对应一个Act是一个队列
activityEvents = LinkedList()
// Act录制事件的开始时间
startTime = System.currentTimeMillis()
//保存到内存中
ActEventStates.eventStates.add(activityEvents)
}
}

//停止录制
protected fun stopRecord() {
val state = EventState()
state.event = null
state.time = System.currentTimeMillis() - startTime
activityEvents?.add(state)
}

override fun onBackPressed() {
val state = EventState()
state.event = null
state.isBackPress = true
state.time = System.currentTimeMillis() - startTime
activityEvents?.add(state)
super.onBackPressed()
}

//回放录制
protected fun playRecord() {
//如果是播放状态
if (ActEventStates.isPlay) {
ActEventStates.isRecord = false

//延时1秒开始播放
handler.postDelayed({
Thread {
if (!ActEventStates.eventStates.isEmpty()) {
//遍历每一个Act的事件,支持多个Act的录制与回放
val pop = ActEventStates.eventStates.remove()
while (!pop.isEmpty()) {
val state = pop.remove()
//根据事件的时间顺序播放
handler.postDelayed({
if (state.event == null) {
if (state.isBackPress) {
YYLogUtils.w("手动调用系统返回按键")
onBackPressed() //手动调用系统返回按键
} else {
YYLogUtils.w("没了,回放录制完成")
}

} else {
dispatchTouchEvent(state.event)
}
}, state.time)

}
}
}.start()
}, 1000)
}
}

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
//只有在录制状态下才会保存事件并添加到队列中
if (ActEventStates.isRecord && activityEvents != null) {
//不要直接存原始的 MotionEvent,因为用过就回收的,之前我们是通过自定义 Event 来做的,这里简单一点直接重新伪造一次其实更方便
val obtain = MotionEvent.obtain(ev)
//初始化自己的 EventState 用于保存当前事件对象
val state = EventState()
//赋值当前事件,用伪造过的事件
state.event = obtain
//赋值当前事件发生的时间
state.time = System.currentTimeMillis() - startTime
//把每一次事件 EventState 对象添加到队列中
activityEvents?.add(state)
}
return super.dispatchTouchEvent(ev)
}
}

对于事件的封装我们添加了是否是系统返回的标记:


public class EventState {
public boolean isBackPress;
public MotionEvent event; //事件
public long time; //开始录制到该事件发生的时间
}

使用的方式就没有变化,我们添加几个 Activity 的跳转试试:


    startRecode.click {
ActEventStates.isRecord = true
toast("开始录制")
startRecord()
}

endRecode.click {
ActEventStates.isRecord = false
toast("停止录制")
stopRecord()
}

//点击回放
btnReplay.click {
ActEventStates.isPlay = true
toast("回放录制")
playRecord()
}

btnJump1.click {
TemperatureViewActivity.startInstance()
}
btnJump2.click {
ViewGr0up9Activity.startInstance()
}

效果:


act_record02.gif


为了区分实际手指操作与回放的操作的差异,我打开了开发者选项中的触摸反馈,第一次效果是带触摸反馈的,回放录制的效果是没有触摸反馈的,并且支持 Appbar的返回按键与系统的返回键。


如果想回放多次,则需要在停止录制的时候把事件保存到本地,如何保存对象到本地?和前文一样的思路,可以用Json,可以压缩,可以加密,甚至可以自定义数据格式与解析,这一个步骤就无需我多说了吧。


后记


回到前文,虽然自动化测试中我们常用到录制与回放的功能,但是对于线上的监控与云真机回放对于的操作,其实与类似Python自动化脚本还是有区别,与 PC + ADB 的方式也有区别,基于App本身实现的可以更好的用于线上的稳定性监控。


当然了由于本文是实验性质并不完善,浅尝辄止,只是提供一个思路,真要实现完整的功能并不是一个人短时间能搞出来的,如果你想要实现类似的功能可以参考实现。


比如后期如我们需要区分事件类型,点击的文本与图标,使用文本或图片识别进行定位,输入框的适配,等等一系列的功能并不是那么的容易还有很长的路要走,想起来都头皮发麻。


好了,关于最基础的功能来说的话,本机的 App 应用的录制与回放就讲到这里,那么除此方式之外还有哪些更方便的实现方式呢?我也很好奇,也欢迎大家交流讨论哦!


而对于本机其他第三方 App 应用的录制与回放又有哪些方式实现呢?这又是完全不同的另一个故事了。


言归正传,关于本文的内容如果想查看源码可以点击这里 【传送门】。你也可以关注我的这个Kotlin项目,我有时间都会持续更新。


惯例,我如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力啦!


Ok,这一期就此完结。



作者:Newki
来源:juejin.cn/post/7330104253825646601

0 个评论

要回复文章请先登录注册