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

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

前言

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

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

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

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

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

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

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

那么话不多说,Let's go

一、定义事件

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

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

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

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

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

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

arduino 复制代码
/**
 * 以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 方便。

二、录制

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

kotlin 复制代码
  //开启录制
    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 层级之下的操作都能实现录制与回放了:

kotlin 复制代码
    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)
    }

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

三、回放

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

kotlin 复制代码
    //回放录制
    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 中录制与回放的效果,具体的使用与效果:

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

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

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

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

四、多Activity的录制与回放

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

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

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

kotlin 复制代码
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)
    }
}

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

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

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

scss 复制代码
    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 {
        ViewGroup9Activity.startInstance()
    }

效果:

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

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

后记

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

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

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

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

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

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

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

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

Ok,这一期就此完结。

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui