向/dev/input/eventx的触摸设备写入实现高效安卓模拟触摸

/dev/input/eventx的触摸设备写入实现高效安卓模拟触摸

简介

android模拟触摸有很多种方式,比如在shell里可以使用input命令或者更接近底层的sendevent来模拟各种触摸事件.

然而它们的共性就是经过了多层封装,调用效率会很低.如果想要高速地注入触摸事件(每秒约100个触摸点),这种方法就很不现实了.

那不如干脆自己用纯C实现一个内部的sendevent,这样效率就能有很大提升,每秒上千个触摸点不在话下(不过这么多会让安卓躺平全部丢掉)

以下推荐先了解linux的触摸协议,高考生时间有限不再展开,且向eventX在当前主流高版本安卓写入需要root权限(用户input组内,而shell不在此列)

Linux输入子系统:多点触控协议 – multi-touch-protocol.txt【转】 - Sky&Zhang - 博客园

从sendevent开始

使用sendevent模拟一个简略的触摸事件如下:

1
2
3
4
5
6
7
8
9
10
11
12
#按下
sendevent /dev/input/event2 3 57 1145 # EV_ABS ABS_MT_TRACKING_ID (分配ID)
sendevent /dev/input/event2 3 53 500 # EV_ABS ABS_MT_POSITION_X (500)
sendevent /dev/input/event2 3 54 500 # EV_ABS ABS_MT_POSITION_Y (500)

sendevent /dev/input/event2 1 330 1 # EV_KEY BTN_TOUCH DOWN
sendevent /dev/input/event2 0 0 0 # EV_SYN SYN_REPORT

# 抬起 (UP)
sendevent /dev/input/event2 3 57 -1 # EV_ABS ABS_MT_TRACKING_ID (释放ID)
sendevent /dev/input/event2 1 330 0 # EV_KEY BTN_TOUCH UP
sendevent /dev/input/event2 0 0 0 # EV_SYN SYN_REPORT

以上例子实现了短暂点按(500,500)的效果.想要模拟触摸只需要按顺序向event2写入这些数据即可.

同样的,我们还能模拟手指在屏幕上的方向\压力等,这里不再赘述.

需要注意的是,触摸屏的坐标和屏幕显示的坐标不一定相同,在本人手机上这个比是1,而在学校电脑上x和y的比例分别为18和32,屏幕坐标需要乘以这两个比例才能转化到触摸事件的坐标,该比例可以通过getevent -i查到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
device 4: /dev/input/event2
bus: 0003
vendor 1fd2
product 8201
version 0110
name: "LGD LGD_inTouchProAP,86,20,4269,20211211"
location: "usb-mstar-1.1/input0"
id: "TEST876D9CC1AA4"
version: 1.0.1
events:
KEY (0001): 014a
ABS (0003): 0000 : value 32361, min 0, max 32767, fuzz 0, flat 0, resolution 17 #注意这里,0x00是ABS_X的event code
0001 : value 29113, min 0, max 32767, fuzz 0, flat 0, resolution 31 #0x01是ABS_Y
002f : value 0, min 0, max 19, fuzz 0, flat 0, resolution 0
0030 : value 0, min 0, max 32767, fuzz 0, flat 0, resolution 0
0031 : value 0, min 0, max 32767, fuzz 0, flat 0, resolution 0
0034 : value 0, min 0, max 1, fuzz 0, flat 0, resolution 0
0035 : value 0, min 0, max 32767, fuzz 0, flat 0, resolution 17 #0035:ABS_MT_POSITION_X
0036 : value 0, min 0, max 32767, fuzz 0, flat 0, resolution 31 #0x36:ABS_MT_POSITION_Y
0039 : value 0, min 0, max 65535, fuzz 0, flat 0, resolution 0
input props:
INPUT_PROP_DIRECT

可以看到xy被映射到(32767,32767)的触摸范围,比例就是resolution+1.

sendevent的代码很简单,甚至到了简陋的地步,它是toybox的一部分:

toybox/toys/android/sendevent.c at master · landley/toybox · GitHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void sendevent_main(void)
{
int fd = xopen(*toys.optargs, O_RDWR);
int version;
struct input_event ev;

if (ioctl(fd, EVIOCGVERSION, &version))
perror_exit("EVIOCGVERSION failed for %s", *toys.optargs);

memset(&ev, 0, sizeof(ev));
// TODO: error checking and support for named constants.
ev.type = atoi(toys.optargs[1]);
ev.code = atoi(toys.optargs[2]);
ev.value = atoi(toys.optargs[3]);
xwrite(fd, &ev, sizeof(ev));
}

由此可见只需要构建一个input_event struct然后直接写到设备就能实现事件注入.

实践

在我的彩蛋项目里,我构建了一个简陋的框架:

printer.h的一部分:

1
2
3
4
5
6
7
8
9
10
11
#include <linux/input.h>

#define EV(__TYP, __CODE, __VAL) \
(struct input_event){ .type = (__TYP), .code = (__CODE), .value = (__VAL) }
#define LEN_EV sizeof(struct input_event)
#define MDELAY(__MS)\
usleep((__MS)*1000)

//每个触摸点之间的时间间隔(ms)
#define G_DELAY 0.7
#define DEV "/dev/input/event4"

printer.c的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include ...
#include "printer.h"


int main()
{
int version;

int fd_dev = open(DEV, O_RDWR);
if (ioctl(fd_dev, EVIOCGVERSION, &version)){
printf("EVIOCGVERSION failed for %s", DEV);
exit(1);
}

...
//点击(500,500)
tap(fd_dev,500,500);
//模拟滑动(500,500)->(600,600)->(700,700):
start_touch(fd_dev,500,500);
touch_point(fd_dev,600,600);
touch_point(fd_dev,700,700);
end_touch(fd_dev);
close(fd_dev);
return 0;
}


// 封装一个点击函数
void tap(int fd, int x, int y)
{
start_touch(fd,x,y);
MDELAY(G_DELAY);
end_touch(fd);
}
// 开始一次点击(手指接触)
void start_touch(int fd,int x, int y)
{
// EV_KEY BTN_TOUCH DOWN
send(fd, &EV(EV_KEY,BTN_TOUCH,BTN_TOUCH_DOWN));
// EV_ABS ABS_MT_TRACKING_ID
send(fd, &EV(EV_ABS,ABS_MT_TRACKING_ID,0x1145));
touch_point(fd,x,y);
}
// 结束点击,手指离开
void end_touch(int fd)
{
// EV_ABS ABS_MT_TRACKING_ID (释放ID)
send(fd, &EV(EV_ABS,ABS_MT_TRACKING_ID,-1));
// EV_KEY BTN_TOUCH UP
send(fd, &EV(EV_KEY,BTN_TOUCH,BTN_TOUCH_UP));
// EV_SYN SYN_REPORT
send(fd, &EV(EV_SYN,SYN_REPORT,0));
}
//增加一个轨迹点(移动)
void touch_point(int fd,int x,int y){
// EV_ABS ABS_MT_POSITION_X
send(fd, &EV(EV_ABS,ABS_MT_POSITION_X,x));
// EV_ABS ABS_MT_POSITION_Y
send(fd, &EV(EV_ABS,ABS_MT_POSITION_Y,y));
// EV_SYN SYN_REPORT 0
send(fd, &EV(EV_SYN,SYN_REPORT,0));
MDELAY(G_DELAY);
}
void send(int fd, struct input_event *ev)
{
if(!(write(fd, ev, LEN_EV) == LEN_EV))
printf("Error WriteDeviceFailed for %s",DEV);
}

效果

以下视频原速播放

原图边界:

image_2025-05-02_14-23-08.png (1920×1080)

画了两次,用来解决每个边界都不太全的问题.


向/dev/input/eventx的触摸设备写入实现高效安卓模拟触摸
https://www.hakurei.org.cn/2025/05/02/linux-kernel-touch-event-inject/
作者
zjkimin
发布于
2025年5月2日
更新于
2025年5月2日
许可协议