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

243 篇文章 3 订阅

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

前言

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

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

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

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

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

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

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

那么话不多说,Let's go

300.png

一、定义事件

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

整体思路基于前文 ViewGroup 的例子,还是把事件用对象封装起来,只是我们封装的对象换成了 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)
}

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

三、回放

其实和我们之前的 ViewGroup 的思路是一致的,只是把自定义的事件换成原生的 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()
}

图片

单独的 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 {
    ViewGroup9Activity.startInstance()
}

效果:

录制:

图片

回放:

图片

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

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

后记

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

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

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

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

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

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

作者:Newki
链接:https://juejin.cn/post/7330104253825646601
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,这是你可以使用的步骤来实现一个Android FTP客户端: 1. 使用Android Studio创建一个新的Android项目。 2. 在build.gradle文件中添加对Apache Commons Net库的依赖,它是实现FTP客户端所必需的库。 dependencies { implementation 'commons-net:commons-net:3.7' } 3. 在您的主活动中添加一些用于连接到FTP服务器的代码。您可以使用FTPClient类来连接和操作FTP服务器。 FTPClient ftpClient = new FTPClient(); ftpClient.connect(server, port); ftpClient.login(username, password); 4. 现在,您可以使用FTPClient实例来执行各种操作,如上传文件、下载文件、创建目录等。 例如,要上传文件,您可以使用以下代码: InputStream inputStream = new FileInputStream(localFile); ftpClient.storeFile(serverFile, inputStream); inputStream.close(); 5. 最后,记得在退出应用程序时断开与FTP服务器的连接。 ftpClient.logout(); ftpClient.disconnect(); 希望这对你有帮助! ### 回答2: 要实现一个Android的FTP客户端,可以按照以下步骤进行: 1. 导入FTP类库:首先需要在Android项目中导入FTP的类库,例如Apache Commons Net库或者Android FTP库。这些类库提供了FTP客户端所需的功能和方法。 2. 创建FTP连接:在Android应用中,可以使用FTP客户端类来创建一个FTP连接对象。通过指定FTP服务器的IP地址、用户名和密码来建立连接。 3. 执行FTP操作:一旦连接建立成功,就可以执行FTP操作。这些操作可以包括上传文件、下载文件、删除文件、创建文件夹等。可以通过调用FTP客户端对象的相应方法来执行这些操作。 4. 实现文件传输:要上传或下载文件,可以使用FTP客户端提供的方法。要上传文件,可以将本地文件的路径作为参数,通过调用相应方法将文件传输到FTP服务器上。要下载文件,可以指定要下载的文件路径和下载的本地路径,然后调用相应方法来实现。 5. 错误处理:在实现FTP客户端时,还需要考虑错误处理。例如,在连接或文件传输过程中可能会发生网络异常或服务器错误。可以使用try-catch语句来捕获这些异常,并根据需要采取相应的处理措施。 6. 界面设计:为了更好地与用户交互,可以设计一个用户界面来显示FTP操作的进度和结果。可以使用Android中提供的布局和小部件来创建用户界面,并更新进度和显示结果。 以上是一个基本的Android实现FTP客户端的步骤。根据具体的需求,还可以进行更多的功能扩展,例如实现断点续传、支持多线程下载等。 ### 回答3: 实现一个Android上的FTP客户端可以通过以下步骤: 1. 添加权限:在AndroidManifest.xml文件中添加网络权限,以便应用程序可以进行网络通信。 2. 创建FTP连接类:创建FTP连接类,用于建立和管理与FTP服务器的连接。该类应该包括连接到FTP服务器的方法、断开连接的方法以及上传和下载文件的方法。 3. 用户界面设计:创建一个用户界面,以便用户可以输入FTP服务器的地址、用户名和密码。还可以添加其他的选项,例如显示已上传和已下载的文件列表。 4. 连接到FTP服务器:当用户点击连接按钮时,读取用户输入的FTP服务器地址、用户名和密码,并使用FTP连接类中的方法连接到服务器上。 5. 上传文件:创建一个文件选择器,以便用户可以选择要上传的文件。当用户选择文件后,使用FTP连接类中的上传文件的方法将文件上传到服务器上。 6. 下载文件:显示服务器上的文件列表,当用户选择要下载的文件时,使用FTP连接类中的下载文件的方法将文件下载到设备上。 7. 错误处理:在连接到服务器、上传和下载文件过程中,需要添加错误处理机制,以便在发生错误时提示用户出现问题,并提供重新连接或重新上传/下载的选项。 8. UI界面优化:可以添加进度条显示上传和下载的进度,增加用户体验。 9. 测试和调试:编译并运行应用程序,测试连接、上传和下载功能。在出现错误时,使用日志输出和调试工具进行调试。 10. 发布应用程序:完成开发后,可以使用Android工具生成应用程序的安装文件,并发布到应用商店供用户下载使用。 以上就是大致的步骤,根据实际需求还可以对功能进行扩展和优化,比如实现断点续传、多线程并行传输等功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值