背景

上个文章介绍了stf项目,最后制作出来发现性能太差,研究了下性能瓶颈主要有两个:

  1. node服务器通过socket推出的数据在浏览器端回调函数中解析占用资源,因为频率太高;
  2. 浏览器端canvas太多绘制太频繁;
    以开秒表为例,基本5台设备,一秒绘制一次,都不流畅,而且cpu内存占用都近100%。为此我也特地去提过issue,似乎也是表明b/s模式性能有限,只有尝试c/s模式。

考虑到之前学过java swing。另外minicap项目是继续linux的,java跨平台编写客户端很方便,所以就用java来编写。最终项目和效果我已经放到github上了。性能有了大幅度提升,21台设备100ms刷新一次,cpu只占用50%,内存只占用20%。

minicap

minicap是openstf组织开源的另外一个项目,是用于stf项目获取屏幕截图的一个ndk库

1
2
adb forward tcp:1313 localabstract:minicap
adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1920@1080x1920/0 -t

abd forward命令将手机中的minicap生成的数据映射到电脑端的tcp 1313接口。java的socket就是基于Tcp协议的,所以创建socket客户端监听1313端口就可以获取到minicap输出的屏幕数据。
这种数据的协议,minicap也定出来了,就不多说了,贴下我的java解析代码:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import javax.imageio.ImageIO;
import main.MainFrame;
public class SelfParse extends Thread{
public static final int MAX_FRAME = 50000;
private BufferedImage bi1;
private String serial;
private int port;
public SelfParse(String serial, int port){ //startConnect函数一直没有结束,所以SelfParse实例一直为null
this.bi1 = new BufferedImage(MainFrame.AREA_WIDTH, MainFrame.AREA_HEIGHT, BufferedImage.TYPE_INT_RGB);
this.serial = serial;
this.port = port;
}
@Override
public void run() { //startConnect一直执行不完,所以需要单独一个线程来处理;否则会阻塞主线程
// TODO Auto-generated method stub
startConnect();
}
/**
* 拷贝数组
* 返回下次该插入的index
* @param sour
* @param dest
* @param sourSt 原数组开始index
* @param destSt 目标数组开始index
*/
private int copyByteArray(byte[] sour, byte[] dest, int sourSt, int sourEn, int destSt){
int k=0;
for(int i=sourSt; i<=sourEn; i++){
if(destSt+k >= dest.length) continue;
dest[destSt+k] = sour[i];
k++;
}
return destSt+k;
}
private void startConnect(){
Socket socket;
int actualFrameSize = 0; //记录每个frame的实际大小
byte[] frameArr = new byte[MAX_FRAME];
boolean HEAD_ONCE_FLAG = true;
try {
//create a local forward,把minicap中的数据转发到pc机1313接口
Util.runCommand(Util.ADB+" -s "+this.serial+" forward tcp:"+this.port+" localabstract:minicap");
//start minicap
Util.runCommand(Util.ADB+" -s "+this.serial+" shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1920@"+MainFrame.AREA_WIDTH+"x"+MainFrame.AREA_HEIGHT+"/0");
//创建socket连接
socket = new Socket("127.0.0.1", this.port);
System.out.println(this.port+"端口连接成功");
InputStream is = socket.getInputStream();
byte[] bytes = new byte[Util.FRAME_SIZE];
byte[] fourbts = new byte[4];
int hasread = 0, destSt = 0;
while((hasread = is.read(bytes)) > 0){
if(HEAD_ONCE_FLAG){ //第一次只接收header,bytes[24]为空
HEAD_ONCE_FLAG = false;
} else { //不是第一次发送,就不含有header,头四个字节直接表示Frame size
if(destSt <= 0){
fourbts[0] = bytes[3];
fourbts[1] = bytes[2];
fourbts[2] = bytes[1];
fourbts[3] = bytes[0];
actualFrameSize = Util.bytes2int(fourbts);
if(actualFrameSize > MAX_FRAME || actualFrameSize <= 0)
continue;
destSt = this.copyByteArray(bytes, frameArr, 4, hasread-1, 0);
} else {
destSt = this.copyByteArray(bytes, frameArr, 0, hasread-1, destSt);
if(destSt >= actualFrameSize){ //已经记录完成一帧
ByteArrayInputStream bais = new ByteArrayInputStream(frameArr, 0, actualFrameSize-1);
this.bi1 =ImageIO.read(bais);
bais.close();
destSt = 0;
}
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public BufferedImage getFrame(){
return this.bi1;
}
}

minicap发送数据分为如下部分:

  1. header部分socket连接成功之后只发送一次,包含24byte。
  2. 以后每次发送frame数据。每个frame包含4byte的头部信息,表示frame size。
    4byte总共32位,minicap用c++编写,使用的是uint。所以最大会有2^32个byte大小的frame。这在java中只有用long来存储,但是这显然太消耗内存。为了提升性能,最后1080x1920的屏幕肯定要压缩,这里最终选择压缩到180x320。观察下180x320这种大小下frame size大概在10000byte左右。所以完全可以用int来表示,这里最终每个frame的size大小变量actualFrameSize就是int类型。
    对应的存储帧数据的byte[] frameArr也可以设置一个长度为50000,这样保证了完全足够存储下每个frame数据。
    最终在每帧数据结束时(destSt >= actualFrameSize)生成bufferImage输出,就得到了屏幕数据。

另外起一个读取线程,可以控制读取bufferImage的频率