51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 2766|回复: 4
打印 上一主题 下一主题

[转贴] STF 框架之 minicap 工具

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2017-6-22 11:58:01 | 只看该作者 回帖奖励 |正序浏览 |阅读模式
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种:
  1. .
  2. ├── bin
  3. │   ├── arm64-v8a
  4. │   │   ├── minicap
  5. │   │   └── minicap-nopie
  6. │   ├── armeabi-v7a
  7. │   │   ├── minicap
  8. │   │   └── minicap-nopie
  9. │   ├── x86
  10. │   │   ├── minicap
  11. │   │   └── minicap-nopie
  12. │   └── x86_64
  13. │       ├── minicap
  14. │       └── minicap-nopie
  15. └── shared
  16.     ├── android-10
  17.     │   └── armeabi-v7a
  18.     │       └── minicap.so
  19.     ├── android-14
  20.     │   ├── armeabi-v7a
  21.     │   │   └── minicap.so
  22.     │   └── x86
  23.     │       └── minicap.so
  24.     ├── android-15
  25.     │   ├── armeabi-v7a
  26.     │   │   └── minicap.so
  27.     │   └── x86
  28.     │       └── minicap.so
  29.     ├── android-16
  30.     │   ├── armeabi-v7a
  31.     │   │   └── minicap.so
  32.     │   └── x86
  33.     │       └── minicap.so
  34.     ├── android-17
  35.     │   ├── armeabi-v7a
  36.     │   │   └── minicap.so
  37.     │   └── x86
  38.     │       └── minicap.so
  39.     ├── android-18
  40.     │   ├── armeabi-v7a
  41.     │   │   └── minicap.so
  42.     │   └── x86
  43.     │       └── minicap.so
  44.     ├── android-19
  45.     │   ├── armeabi-v7a
  46.     │   │   └── minicap.so
  47.     │   └── x86
  48.     │       └── minicap.so
  49.     ├── android-21
  50.     │   ├── arm64-v8a
  51.     │   │   └── minicap.so
  52.     │   ├── armeabi-v7a
  53.     │   │   └── minicap.so
  54.     │   ├── x86
  55.     │   │   └── minicap.so
  56.     │   └── x86_64
  57.     │       └── minicap.so
  58.     ├── android-22
  59.     │   ├── arm64-v8a
  60.     │   │   └── minicap.so
  61.     │   ├── armeabi-v7a
  62.     │   │   └── minicap.so
  63.     │   ├── x86
  64.     │   │   └── minicap.so
  65.     │   └── x86_64
  66.     │       └── minicap.so
  67.     ├── android-9
  68.     │   └── armeabi-v7a
  69.     │       └── minicap.so
  70.     └── android-M
  71.         ├── arm64-v8a
  72.         │   └── minicap.so
  73.         ├── armeabi-v7a
  74.         │   └── minicap.so
  75.         ├── x86
  76.         │   └── minicap.so
  77.         └── x86_64
  78.             └── minicap.so
复制代码

从上面可以看出,minicap可执行文件分为4种,分别针对arm64-v8a、armeabi-v7a,x86,x86_64 架构。而minicap.so文件在这个基础上还要分为不同的sdk版本。
获取设备的CPU版本和系统版本CPU版本
  1. adb shell getprop ro.product.cpu.abi | tr -d '\r'

  2. 58deMacBook-Pro:minicap wuxian$ adb shell getprop ro.product.cpu.abi | tr -d '\r'
  3. armeabi-v7a
复制代码
系统版本
  1. adb shell getprop ro.build.version.sdk | tr -d '\r'

  2. 58deMacBook-Pro:minicap wuxian$ adb shell getprop ro.build.version.sdk | tr -d '\r'
  3. 22
复制代码
将文件push到手机根据上面获取的信息,将适合设备的可执行文件和.so文件push到手机的/data/local/tmp目录下,如果你不想自己build这些文件可以去STF框架的源码下找到vendor/minicap文件夹下找到这些文件,我上面的tree信息就是我在stf根目录vendor/minicap下打印的,所以我们将这两个文件导入到我手机的/data/local/tmp目录下:
  1. shell@shamu:/data/local/tmp $ ls -l
  2. -rw-rw-r-- shell    shell     1053609 2015-08-07 19:19 1.png
  3. -rwxr-xr-x shell    shell     1062992 2015-08-03 12:02 busybox
  4. -rwxr-xr-x shell    shell      358336 2015-08-03 12:02 busybox1
  5. drwxrwxrwx shell    shell             2015-07-21 15:16 dalvik-cache
  6. -rw-r--r-- shell    shell         193 2015-08-13 19:44 krperm.txt
  7. -rwxrwxrwx shell    shell      370424 2015-08-07 18:16 minicap
  8. -rw-rw-rw- shell    shell       13492 2015-08-07 18:26 minicap.so
  9. -rw------- shell    shell       11192 2015-08-06 10:46 ui.xml
  10. -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可用:

  1. 58deMacBook-Pro:minicap wuxian$ adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1440x2560@1440x2560/0 -t
  2. PID: 7105
  3. INFO: Using projection 1440x2560@1440x2560/0
  4. INFO: (external/MY_minicap/src/minicap_22.cpp:240) Creating SurfaceComposerClient
  5. INFO: (external/MY_minicap/src/minicap_22.cpp:243) Performing SurfaceComposerClient init check
  6. INFO: (external/MY_minicap/src/minicap_22.cpp:250) Creating virtual display
  7. INFO: (external/MY_minicap/src/minicap_22.cpp:256) Creating buffer queue
  8. INFO: (external/MY_minicap/src/minicap_22.cpp:261) Creating CPU consumer
  9. INFO: (external/MY_minicap/src/minicap_22.cpp:265) Creating frame waiter
  10. INFO: (external/MY_minicap/src/minicap_22.cpp:269) Publishing virtual display
  11. INFO: (jni/minicap/JpgEncoder.cpp:64) Allocating 11061252 bytes for JPG encoder
  12. INFO: (external/MY_minicap/src/minicap_22.cpp:284) Destroying virtual display
  13. OK

  14. 然后我们启动minicap工具,命令如下(就比上面的检测工具少了个-t):
  15. 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.jsListening 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种方式可以看出来图片组装完成了:
  • 又遇到第二部分
  • 设定大小的数据已经装满了
总结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秒内。

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏
回复

使用道具 举报

该用户从未签到

5#
 楼主| 发表于 2017-6-22 13:05:06 | 只看该作者
乐哈哈yoyo 发表于 2017-6-22 13:04
在github看得一头雾水,搜到了楼主这篇好文,太赞了!!
补充一下:
运行 adb shell LD_LIBRARY_PATH=/da ...

谢谢支持!
回复 支持 反对

使用道具 举报

  • TA的每日心情
    无聊
    2024-7-12 13:16
  • 签到天数: 1 天

    连续签到: 1 天

    [LV.1]测试小兵

    4#
    发表于 2017-6-22 13:04:00 | 只看该作者
    在github看得一头雾水,搜到了楼主这篇好文,太赞了!!
    补充一下:
    运行 adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1800@1080x1800/0 -t
    提示Permission denied的,试下 adb shell chmod 0755 /data/local/tmp/minicap
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    3#
     楼主| 发表于 2017-6-22 11:59:48 | 只看该作者
    1. break;
    2.                         case 18:
    3.                         case 19:
    4.                         case 20:
    5.                         case 21:
    6.                             // virtual height
    7.                             int virtualHeight = banner.getVirtualHeight();
    8.                             virtualHeight += (byte10 << ((readBannerBytes - 18) * 8)) >>> 0;
    9.                             banner.setVirtualHeight(virtualHeight);
    10.                             break;
    11.                         case 22:
    12.                             // orientation
    13.                             banner.setOrientation(byte10 * 90);
    14.                             break;
    15.                         case 23:
    16.                             // quirks
    17.                             banner.setQuirks(byte10);
    18.                             break;
    19.                         }

    20.                         cursor += 1;
    21.                         readBannerBytes += 1;

    22.                         if (readBannerBytes == bannerLength) {
    23.                             LOG.info(banner.toString());
    24.                         }
    25.                     } else if (readFrameBytes < 4) {
    26.                         // 第二次的缓冲区中前4位数字和为frame的缓冲区大小
    27.                         frameBodyLength += (byte10 << (readFrameBytes * 8)) >>> 0;
    28.                         cursor += 1;
    29.                         readFrameBytes += 1;
    30.                         total = frameBodyLength;

    31.                     } else {
    32.                         LOG.info("图片大小 : " + total);
    33.                         // LOG.info("frame body部分");
    34.                         // LOG.info(String.format("设想图片的大小 : %d", total));
    35.                         if (len - cursor >= frameBodyLength) {
    36.                             byte[] subByte = subByteArray(currentBuffer,
    37.                                     cursor, cursor + frameBodyLength);
    38.                             frameBody = byteMerger(frameBody, subByte);
    39.                             if ((frameBody[0] != -1) || frameBody[1] != -40) {
    40.                                 LOG.error(String
    41.                                         .format("Frame body does not start with JPG header"));
    42.                                 return;
    43.                             }
    44.                             LOG.info(String.format("实际图片的大小 : %d",
    45.                                     frameBody.length));
    46.                             if (finalBytes == null) {
    47.                                 finalBytes = subByteArray(frameBody, 0,
    48.                                         frameBody.length);
    49.                                 new Thread(new Runnable() {

    50.                                     @Override
    51.                                     public void run() {
    52.                                         // TODO Auto-generated method stub
    53.                                         try {
    54.                                             createImageFromByte();
    55.                                         } catch (IOException e) {
    56.                                             // TODO Auto-generated catch block
    57.                                             e.printStackTrace();
    58.                                         }
    59.                                     }
    60.                                 }).start();
    61.                             }
    62.                             cursor += frameBodyLength;
    63.                             frameBodyLength = 0;
    64.                             readFrameBytes = 0;
    65.                             frameBody = new byte[0];
    66.                         } else {
    67.                             // LOG.debug(String.format("body(len=%d)", len
    68.                             // - cursor));
    69.                             byte[] subByte = subByteArray(currentBuffer,
    70.                                     cursor, len);
    71.                             frameBody = byteMerger(frameBody, subByte);
    72.                             frameBodyLength -= (len - cursor);
    73.                             readFrameBytes += (len - cursor);
    74.                             cursor = len;
    75.                         }
    76.                     }
    77.                 }
    78.             }
    79.         } catch (UnknownHostException e) {
    80.             // TODO Auto-generated catch block
    81.             e.printStackTrace();
    82.         } catch (IOException e) {
    83.             // TODO Auto-generated catch block
    84.             e.printStackTrace();
    85.         }

    86.         finally {
    87.             if (socket != null && socket.isConnected()) {
    88.                 try {
    89.                     socket.close();
    90.                 } catch (IOException e) {
    91.                     // TODO Auto-generated catch block
    92.                     e.printStackTrace();
    93.                 }
    94.             }
    95.             if (stream != null) {
    96.                 try {
    97.                     stream.close();
    98.                 } catch (IOException e) {
    99.                     // TODO Auto-generated catch block
    100.                     e.printStackTrace();
    101.                 }
    102.             }
    103.         }

    104.     }

    105.     private synchronized void createImageFromByte() throws IOException {
    106.         if (finalBytes.length == 0) {
    107.             LOG.info("frameBody大小为0");
    108.         }
    109.         InputStream in = new ByteArrayInputStream(finalBytes);
    110.         BufferedImage bufferedImage = ImageIO.read(in);
    111.         notifyObservers(bufferedImage);
    112.         // String filePath = String.format("0.jpg");
    113.         // LOG.info(filePath);
    114.         // ImageIO.write(bufferedImage, "jpg", new File(filePath));
    115.         finalBytes = null;
    116.     }

    117.     // java合并两个byte数组
    118.     private static byte[] byteMerger(byte[] byte_1, byte[] byte_2) {
    119.         byte[] byte_3 = new byte[byte_1.length + byte_2.length];
    120.         System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length);
    121.         System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length);
    122.         return byte_3;
    123.     }

    124.     private static byte[] subByteArray(byte[] byte1, int start, int end) {
    125.         byte[] byte2 = new byte[end - start];
    126.         System.arraycopy(byte1, start, byte2, 0, end - start);
    127.         return byte2;
    128.     }

    129.     private String bytesToHexString(byte[] src) {
    130.         StringBuilder stringBuilder = new StringBuilder();
    131.         if (src == null || src.length <= 0) {
    132.             return null;
    133.         }
    134.         for (int i = 0; i < src.length; i++) {
    135.             int v = src[i] & 0xFF;
    136.             String hv = Integer.toHexString(v);
    137.             if (hv.length() < 2) {
    138.                 stringBuilder.append(0);
    139.             }
    140.             stringBuilder.append(hv + " ");
    141.         }
    142.         return stringBuilder.toString();

    143.     }

    144.     /*
    145.      * (non-Javadoc)
    146.      *
    147.      * @see
    148.      * com.wuba.utils.screenshot.AndroidScreenSubject#registerObserver(com.wuba
    149.      * .utils.screenshot.AndroidScreenObserver)
    150.      */
    151.     @Override
    152.     public void registerObserver(AndroidScreenObserver o) {
    153.         // TODO Auto-generated method stub
    154.         observers.add(o);

    155.     }

    156.     /*
    157.      * (non-Javadoc)
    158.      *
    159.      * @see
    160.      * com.wuba.utils.screenshot.AndroidScreenSubject#removeObserver(com.wuba
    161.      * .utils.screenshot.AndroidScreenObserver)
    162.      */
    163.     @Override
    164.     public void removeObserver(AndroidScreenObserver o) {
    165.         // TODO Auto-generated method stub
    166.         int index = observers.indexOf(o);
    167.         if (index != -1) {
    168.             observers.remove(o);
    169.         }

    170.     }

    171.     /*
    172.      * (non-Javadoc)
    173.      *
    174.      * @see com.wuba.utils.screenshot.AndroidScreenSubject#notifyObservers()
    175.      */
    176.     @Override
    177.     public void notifyObservers(BufferedImage image) {
    178.         // TODO Auto-generated method stub
    179.         for (AndroidScreenObserver observer : observers) {
    180.             observer.frameImageChange(image);
    181.         }
    182.     }

    183. }
    复制代码
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    2#
     楼主| 发表于 2017-6-22 11:59:34 | 只看该作者
    [code]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<Byte[]> stack = new Stack<Byte[]>();

        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<AndroidScreenObserver> observers = new ArrayList<AndroidScreenObserver>();

        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);

                               
    回复 支持 反对

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-9-21 08:32 , Processed in 0.071629 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表