minicap 介绍

从WEB 端批量移动设备管理控制工具 STF 的环境搭建和运行文章了解到 STF 这个工具,然后试用了一下。最近在做一个测试工具,发现 Android 原生的截图工具截图非常缓慢,然后想起了 stf 工具中截图非常快,甚至连执行 monkey 的动作都能在 web 端查看,这就很爽了,所以在 github 上提了一个Issue,询问这个是如何实现的,很快得到答复,stf 自己写了一个工具叫 minicap 用来替代原生的 screencap,这个工具是 stf 框架的依赖工具。

minicap 使用

minicap 工具是用 NDK 开发的,属于 Android 的底层开发,该工具分为两个部分,一个是动态连接库.so 文件,一个是 minicap 可执行文件。但不是通用的,因为 CPU 架构的不同分为不同的版本文件,STF 提供的 minicap 文件根据 CPU 的 ABI 分为如下 4 种:

.

├── bin

│ ├── arm64-v8a

│ │ ├── minicap

│ │ └── minicap-nopie

│ ├── armeabi-v7a

│ │ ├── minicap

│ │ └── minicap-nopie

│ ├── x86

│ │ ├── minicap

│ │ └── minicap-nopie

│ └── x86_64

│ ├── minicap

│ └── minicap-nopie

└── shared

├── android-10

│ └── armeabi-v7a

│ └── minicap.so

├── android-14

│ ├── armeabi-v7a

│ │ └── minicap.so

│ └── x86

│ └── minicap.so

├── android-15

│ ├── armeabi-v7a

│ │ └── minicap.so

│ └── x86

│ └── minicap.so

├── android-16

│ ├── armeabi-v7a

│ │ └── minicap.so

│ └── x86

│ └── minicap.so

├── android-17

│ ├── armeabi-v7a

│ │ └── minicap.so

│ └── x86

│ └── minicap.so

├── android-18

│ ├── armeabi-v7a

│ │ └── minicap.so

│ └── x86

│ └── minicap.so

├── android-19

│ ├── armeabi-v7a

│ │ └── minicap.so

│ └── x86

│ └── minicap.so

├── android-21

│ ├── arm64-v8a

│ │ └── minicap.so

│ ├── armeabi-v7a

│ │ └── minicap.so

│ ├── x86

│ │ └── minicap.so

│ └── x86_64

│ └── minicap.so

├── android-22

│ ├── arm64-v8a

│ │ └── minicap.so

│ ├── armeabi-v7a

│ │ └── minicap.so

│ ├── x86

│ │ └── minicap.so

│ └── x86_64

│ └── minicap.so

├── android-9

│ └── armeabi-v7a

│ └── minicap.so

└── android-M

├── arm64-v8a

│ └── minicap.so

├── armeabi-v7a

│ └── minicap.so

├── x86

│ └── minicap.so

└── x86_64

└── minicap.so

从上面可以看出,minicap 可执行文件分为 4 种,分别针对arm64-v8a、armeabi-v7a,x86,x86_64 架构。而 minicap.so 文件在这个基础上还要分为不同的 sdk 版本。

获取设备的 CPU 版本和系统版本

CPU 版本

adb shell getprop ro.product.cpu.abi | tr -d '\r'

58deMacBook-Pro:minicap wuxian$ adb shell getprop ro.product.cpu.abi | tr -d '\r'

armeabi-v7a

系统版本

adb shell getprop ro.build.version.sdk | tr -d '\r'

58deMacBook-Pro:minicap wuxian$ adb shell getprop ro.build.version.sdk | tr -d '\r'

22

将文件 push 到手机

根据上面获取的信息,将适合设备的可执行文件和.so 文件 push 到手机的/data/local/tmp目录下,如果你不想自己 build 这些文件可以去 STF 框架的源码下找到 vendor/minicap 文件夹下找到这些文件,我上面的 tree 信息就是我在 stf 根目录 vendor/minicap 下打印的,所以我们将这两个文件导入到我手机的/data/local/tmp 目录下:

shell@shamu:/data/local/tmp $ ls -l

-rw-rw-r-- shell shell 1053609 2015-08-07 19:19 1.png

-rwxr-xr-x shell shell 1062992 2015-08-03 12:02 busybox

-rwxr-xr-x shell shell 358336 2015-08-03 12:02 busybox1

drwxrwxrwx shell shell 2015-07-21 15:16 dalvik-cache

-rw-r--r-- shell shell 193 2015-08-13 19:44 krperm.txt

-rwxrwxrwx shell shell 370424 2015-08-07 18:16 minicap

-rw-rw-rw- shell shell 13492 2015-08-07 18:26 minicap.so

-rw------- shell shell 11192 2015-08-06 10:46 ui.xml

-rw------- shell shell 2501 2015-08-07 10:36 uidump.xml

启动工具

首先我们测试一下我们的 minicap 工具是否可用,命令如下 (其中-P 后面跟的参数为你屏幕的尺寸,你可以修改成你自己设备的尺寸):

adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1440x2560@1440x2560/0 -t

最后输出 OK 就表明 minicap 可用:

58deMacBook-Pro:minicap wuxian$ adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1440x2560@1440x2560/0 -t

PID: 7105

INFO: Using projection 1440x2560@1440x2560/0

INFO: (external/MY_minicap/src/minicap_22.cpp:240) Creating SurfaceComposerClient

INFO: (external/MY_minicap/src/minicap_22.cpp:243) Performing SurfaceComposerClient init check

INFO: (external/MY_minicap/src/minicap_22.cpp:250) Creating virtual display

INFO: (external/MY_minicap/src/minicap_22.cpp:256) Creating buffer queue

INFO: (external/MY_minicap/src/minicap_22.cpp:261) Creating CPU consumer

INFO: (external/MY_minicap/src/minicap_22.cpp:265) Creating frame waiter

INFO: (external/MY_minicap/src/minicap_22.cpp:269) Publishing virtual display

INFO: (jni/minicap/JpgEncoder.cpp:64) Allocating 11061252 bytes for JPG encoder

INFO: (external/MY_minicap/src/minicap_22.cpp:284) Destroying virtual display

OK

然后我们启动 minicap 工具,命令如下 (就比上面的检测工具少了个-t):

adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1440x2560@1440x2560/0

本地端口转发

上面其实是启动了一个 socket 服务器,我们需要跟该 socket 服务通信,首先我们要将本地的端口映射到 minicap 工具上,端口自己随意:

adb forward tcp:1717 localabstract:minicap

获取信息

然后使用命令nc localhost 1717来与minicap通信,然后你会发现好多乱码。

效果

上面的乱码我们也看不懂,官方提供了一个 demo 来看效果,在minicap项目下的 example 目录,我们来启动该例子:

58deMacBook-Pro:example wuxian$ PORT=9002 node app.js

Listening on port 9002

然后我们在浏览器下输入localhost:9002就可以看到如下效果了:

minicap 传输的信息解析

我们在上面的nc localhost 1717 那一步可以看出来,minicap 工具会不断的向命令行下输出乱码信息,但是这些信息是有规则的,只是我们无法实际查看。但是我们做的工具需要用 java 来获得该信息,所以弄懂这些格式是很有必要的,结果分析后得出这些信息分 3 部分

Banner 模块 (第一部分)

这一部分的信息只在连接后,只发送一次,是一些汇总信息,一般为 24 个 16 进制字符,每一个字符都表示不同的信息:

位置

信息

0

版本

1

该 Banner 信息的长度,方便循环使用

2,3,4,5

相加得到进程 id 号

6,7,8,9

累加得到设备真实宽度

10,11,12,13

累加得到设备真实高度

14,15,16,17

累加得到设备的虚拟宽度

18,19,20,21

累加得到设备的虚拟高度

22

设备的方向

23

设备信息获取策略

携带图片大小信息和图片二进制信息模块 (第二部分)

得到上面的 Banner 部分处理完成后,以后不会再发送 Banner 信息,后续只会发送图片相关的信息。那么接下来就接受图片信息了,第一个过来的图片信息的前 4 个字符不是图片的二进制信息,而是携带着图片大小的信息,我们需要累加得到图片大小。这一部分的信息除去前四个字符,其他信息也是图片的实际二进制信息,比如我们接受到的信息长度为 n,那么 4~(n-4) 部分是图片的信息,需要保存下来。

只携带图片二进制信息模块 (第三部分)

每一个变化的界面都会有上面的 [携带图片大小信息和图片二进制信息模块],当得到大小后,或许发送过来的数据都是要组装成图片的二进制信息,知道当前屏幕的数据发送完成。

有 2 种方式可以看出来图片组装完成了:

又遇到第二部分

设定大小的数据已经装满了

Java 的实现 /**

*

*/

package com.wuba.utils.screenshot;

import java.awt.image.BufferedImage;

import java.io.ByteArrayInputStream;

import java.io.DataInputStream;

import java.io.File;

import java.io.IOException;

import java.io.InputStream;

import java.net.Socket;

import java.net.SocketAddress;

import java.net.UnknownHostException;

import java.nio.charset.Charset;

import java.util.ArrayList;

import java.util.List;

import java.util.Stack;

import javax.imageio.ImageIO;

import org.apache.log4j.Logger;

import com.android.ddmlib.AdbCommandRejectedException;

import com.android.ddmlib.IDevice;

import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace;

import com.android.ddmlib.TimeoutException;

import com.sun.org.apache.bcel.internal.generic.NEW;

import com.wuba.utils.TimeUtil;

/**

* @date 2015年8月12日 上午11:02:53

*/

public class MiniCapUtil implements ScreenSubject{

private Stack stack = new Stack();

private Logger LOG = Logger.getLogger(MiniCapUtil.class);

private Banner banner;

private static final int PORT = 1717;

private Socket socket;

private int readBannerBytes = 0;

private int bannerLength = 2;

private int readFrameBytes = 0;

private int frameBodyLength = 0;

private byte[] frameBody = new byte[0];

private IDevice device;

private int total;

private boolean debug = false;

private List observers = new ArrayList();

private byte[] finalBytes = null;

private BufferedImage bufferedImage;

public MiniCapUtil(IDevice device) {

this.device = device;

init();

}

private void init() {

banner = new Banner();

try {

this.device.createForward(PORT, "minicap",

DeviceUnixSocketNamespace.ABSTRACT);

} catch (TimeoutException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (AdbCommandRejectedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

public void takeBufferedImageByMinicap() {

InputStream stream = null;

DataInputStream input = null;

try {

socket = new Socket("localhost", PORT);

while (true) {

stream = socket.getInputStream();

input = new DataInputStream(stream);

byte[] buffer;

int len = 0;

while (len == 0) {

len = input.available();

}

buffer = new byte[len];

input.read(buffer);

LOG.info("length=" + buffer.length);

if (debug) {

continue;

}

byte[] currentBuffer = subByteArray(buffer, 0, buffer.length);

for (int cursor = 0; cursor < len;) {

int byte10 = buffer[cursor] & 0xff;

if (readBannerBytes < bannerLength) {

switch (readBannerBytes) {

case 0:

// version

banner.setVersion(byte10);

break;

case 1:

// length

bannerLength = byte10;

banner.setLength(byte10);

break;

case 2:

case 3:

case 4:

case 5:

// pid

int pid = banner.getPid();

pid += (byte10 << ((readBannerBytes - 2) * 8)) >>> 0;

banner.setPid(pid);

break;

case 6:

case 7:

case 8:

case 9:

// real width

int realWidth = banner.getReadWidth();

realWidth += (byte10 << ((readBannerBytes - 6) * 8)) >>> 0;

banner.setReadWidth(realWidth);

break;

case 10:

case 11:

case 12:

case 13:

// real height

int realHeight = banner.getReadHeight();

realHeight += (byte10 << ((readBannerBytes - 10) * 8)) >>> 0;

banner.setReadHeight(realHeight);

break;

case 14:

case 15:

case 16:

case 17:

// virtual width

int virtualWidth = banner.getVirtualWidth();

virtualWidth += (byte10 << ((readBannerBytes - 14) * 8)) >>> 0;

banner.setVirtualWidth(virtualWidth);

break;

case 18:

case 19:

case 20:

case 21:

// virtual height

int virtualHeight = banner.getVirtualHeight();

virtualHeight += (byte10 << ((readBannerBytes - 18) * 8)) >>> 0;

banner.setVirtualHeight(virtualHeight);

break;

case 22:

// orientation

banner.setOrientation(byte10 * 90);

break;

case 23:

// quirks

banner.setQuirks(byte10);

break;

}

cursor += 1;

readBannerBytes += 1;

if (readBannerBytes == bannerLength) {

LOG.info(banner.toString());

}

} else if (readFrameBytes < 4) {

// 第二次的缓冲区中前4位数字和为frame的缓冲区大小

frameBodyLength += (byte10 << (readFrameBytes * 8)) >>> 0;

cursor += 1;

readFrameBytes += 1;

total = frameBodyLength;

} else {

LOG.info("图片大小 : " + total);

// LOG.info("frame body部分");

// LOG.info(String.format("设想图片的大小 : %d", total));

if (len - cursor >= frameBodyLength) {

byte[] subByte = subByteArray(currentBuffer,

cursor, cursor + frameBodyLength);

frameBody = byteMerger(frameBody, subByte);

if ((frameBody[0] != -1) || frameBody[1] != -40) { LOG.error(String

.format("Frame body does not start with JPG header"));

return;

}

LOG.info(String.format("实际图片的大小 : %d",

frameBody.length));

if (finalBytes == null) {

finalBytes = subByteArray(frameBody, 0,

frameBody.length);

new Thread(new Runnable() {

@Override

public void run() {

// TODO Auto-generated method stub

try {

createImageFromByte();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}).start();

}

cursor += frameBodyLength;

frameBodyLength = 0;

readFrameBytes = 0;

frameBody = new byte[0];

} else {

// LOG.debug(String.format("body(len=%d)", len

// - cursor));

byte[] subByte = subByteArray(currentBuffer,

cursor, len);

frameBody = byteMerger(frameBody, subByte);

frameBodyLength -= (len - cursor);

readFrameBytes += (len - cursor);

cursor = len;

}

}

}

}

} catch (UnknownHostException e) {

// TODO Auto-generated catch block

e.printStackTrace();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

finally {

if (socket != null && socket.isConnected()) { try {

socket.close();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

if (stream != null) { try {

stream.close();

} catch (IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

private synchronized void createImageFromByte() throws IOException {

if (finalBytes.length == 0) {

LOG.info("frameBody大小为0");

}

InputStream in = new ByteArrayInputStream(finalBytes);

BufferedImage bufferedImage = ImageIO.read(in);

notifyObservers(bufferedImage);

// String filePath = String.format("0.jpg");

// LOG.info(filePath);

// ImageIO.write(bufferedImage, "jpg", new File(filePath));

finalBytes = null;

}

// java合并两个byte数组

private static byte[] byteMerger(byte[] byte_1, byte[] byte_2) {

byte[] byte_3 = new byte[byte_1.length + byte_2.length];

System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length);

System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length);

return byte_3;

}

private static byte[] subByteArray(byte[] byte1, int start, int end) {

byte[] byte2 = new byte[end - start];

System.arraycopy(byte1, start, byte2, 0, end - start);

return byte2;

}

private String bytesToHexString(byte[] src) {

StringBuilder stringBuilder = new StringBuilder();

if (src == null || src.length <= 0) {

return null;

}

for (int i = 0; i < src.length; i++) {

int v = src[i] & 0xFF;

String hv = Integer.toHexString(v);

if (hv.length() < 2) {

stringBuilder.append(0);

}

stringBuilder.append(hv + " ");

}

return stringBuilder.toString();

}

/*

* (non-Javadoc)

*

* @see

* com.wuba.utils.screenshot.AndroidScreenSubject#registerObserver(com.wuba

* .utils.screenshot.AndroidScreenObserver)

*/

@Override

public void registerObserver(AndroidScreenObserver o) {

// TODO Auto-generated method stub

observers.add(o);

}

/*

* (non-Javadoc)

*

* @see

* com.wuba.utils.screenshot.AndroidScreenSubject#removeObserver(com.wuba

* .utils.screenshot.AndroidScreenObserver)

*/

@Override

public void removeObserver(AndroidScreenObserver o) {

// TODO Auto-generated method stub

int index = observers.indexOf(o);

if (index != -1) { observers.remove(o);

}

}

/*

* (non-Javadoc)

*

* @see com.wuba.utils.screenshot.AndroidScreenSubject#notifyObservers()

*/

@Override

public void notifyObservers(BufferedImage image) {

// TODO Auto-generated method stub

for (AndroidScreenObserver observer : observers) {

observer.frameImageChange(image);

}

}

}

总结

1.在实际过程由于 minicap 发送信息的速度很快,如果不及时处理,会造成某一次获取的数据是将 minicap 多次发送的数据一起处理了,这就会造成错误。所以上面的代码是将生成 BufferImage 的操作放到了线程中,但是最好是将获取 socket 数据部分和解析数据部分独立开来,获取 socket 数据将获取到的数据立即放到队列中,然后立马得到下一次数据的获取,数据解析部分在独立线程中来获取队列中的信息来解析。这样就能避免上面提到的问题。

2.目前不支持下面三款机器和模拟器

Xiaomi "HM NOTE 1W" (Redmi Note 1W),

Huawei "G750-U10" (Honor 3X)

Lenovo "B6000-F" (Yoga Tablet 8).

3.我们实测的速度 (针对 N6) 原生为 5 秒左右,minicap 在 1 秒内。

源码

用 java 写了一个小 demo,有兴趣可以下载体验下

minicap_java

stf java_STF 框架之 minicap 工具相关推荐

  1. STF开源框架之minicap工具

    1.minicap 1.1 minicap介绍 minicap是开源项目STF(Smartphone Test Farm)中的一个工具,负责屏幕显示. stf自己写了一个工具叫minicap用来替代原 ...

  2. 推荐25款很棒的 HTML5 前端框架和开发工具【下篇】

    快速,安全,响应式,互动和美丽,这些优点吸引更多的 Web 开发人员使用 HTML5.HTML5 有许多新的特性功能,允许开发人员和设计师创建应用程序和网站,带给用户桌面应用程序的速度,性能和体验. ...

  3. ssm框架通用代码生成工具

    ssm框架通用代码生成工具 网上搜了一下,没找到什么很顺手的工具.想自己写一个.做了几天有点小成果.发上来看看有没有同行需要,或者大家有什么更好的工具,可以共享一下.谢谢了. 1.由于excel用的更 ...

  4. yii2 框架使用gii工具创建模块

    一.yii2 框架使用gii工具创建模块 1.打开http://127.0.0.1/PHPwork/basic/web/gii 1)点击Module Generator 2)填写完成后点击previe ...

  5. 安卓开发框架(MVP+主流框架+基类+工具类)--- Fresco

    转载自:http://blog.csdn.net/ljy_programmer/article/details/78273267 学习/参考地址:  https://www.fresco-cn.org ...

  6. 游戏开发心得——书籍篇——《游戏引擎框架》-专业工具

    游戏开发心得--书籍篇--<游戏引擎框架>-专业工具 FOR THE SIGMA FOR THE GTINDER FOR THE ROBOMASTER 简介: 学习<游戏引擎框架&g ...

  7. 后端常用数据持久层模板及框架以及一些工具类模板的配置使用集合

    文章目录 后端常用数据持久层模板及框架以及一些工具类模板的配置使用集合 JDBC.c3p0.hibernate配置模板:JDBC模板方法模式.抽象工厂模式封装模板:Spring+hibernate+c ...

  8. 微信小程序uni-app前端应用框架和HBuilderX工具使用及Git管理项目

    微信小程序uni-app前端应用框架和HBuilderX工具使用及Git管理项目 官方地址:https://uniapp.dcloud.io/ 1.起步 1.1.简介 uni-app 是一个使用 Vu ...

  9. 15款针对Bootstrap框架的开发工具

    转自http://www.csdn.net/article/2014-02-18/2818443-15-best-bootstrap-tools-for-designers Bootstrap是由前T ...

最新文章

  1. np.eye()的函数能将一个label数组,大小为(1,m)或者(m,1)的数组,转化成one-hot数组
  2. md5 java_java中MD5函数
  3. 进阶学习(3.14) Strategy Pattern 策略模式
  4. Java SE 12扩展Switch语句/表达式完整指南
  5. Windows服务的程序方面的资料
  6. python自动投递简历_python模拟登录前程无忧,发送简历
  7. 15_clickhouse,MySQL引擎;MySQL和ClickHouse中数据类型的对应关系
  8. 计算机网络安全-RSA加密原理
  9. XAMPP中启动tomcat报错的解决方法
  10. 常用类 String,Stringbuffer,Stringbuilder 包装类 日期类
  11. 冰点还原精灵V8.37.020系统还原软件
  12. Excel数据分析之环比和同比的计算
  13. [js]身份证号码验证
  14. win10访问服务器文件夹慢,win10系统访问共享文件夹速度特别慢的操作方法
  15. 怎么屏蔽计算机集成声卡,win10系统主板集成声卡关闭的设置方案
  16. JSON.stringify初步使用
  17. 浅谈网络安全产品的分类
  18. C++内存管理与指针的使用
  19. vi vim 快速跳到文件末尾 GA 在最后一行下方新增一行 (光标换行,文字不换行) GO
  20. TCP的拥塞避免、超时重传、快速重传、快速恢复

热门文章

  1. 用HTML+CSS+JS做一个漂亮简单的公司网站(JavaScript期末大作业)
  2. linux kdump文件 生成,linux kdump搭建
  3. 喜马拉雅APP基于Scrapy的Python爬虫
  4. 百姓基因:司法亲子鉴定是怎样进行的?它具有怎样的法律属性?
  5. 最全RabbitMQ教程1-安装使用篇
  6. 【JD算法题】定义一个数组的权值为,该数组最大值的出现次数。求长度为n且每个元素范围都在[1,n]的所有数组的权值之和。
  7. 武汉智能网联道路智能化建设规范
  8. 输入经纬度批量查询高程
  9. windows subst命令实现原理模拟2 - subst卸载已经挂载的盘符
  10. 【论文阅读笔记1】:Pre-trained Language Models for Text Generation: A Survey