草帽路飞UU 发表于 2017-6-22 11:58:01

STF 框架之 minicap 工具

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通信,然后你会发现好多乱码。
http://img.blog.csdn.net/20150814111145753
效果上面的乱码我们也看不懂,官方提供了一个demo来看效果,在minicap项目下的example目录,我们来启动该例子:
58deMacBook-Pro:example wuxian$ PORT=9002 node app.jsListening on port 9002然后我们在浏览器下输入localhost:9002就可以看到如下效果了:

http://img.blog.csdn.net/20150814111737618
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秒内。

草帽路飞UU 发表于 2017-6-22 11:59:34

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

                           

草帽路飞UU 发表于 2017-6-22 11:59:48

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 != -1) || frameBody != -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;
                        } 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;
      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;
      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 & 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);
      }
    }

}

乐哈哈yoyo 发表于 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

草帽路飞UU 发表于 2017-6-22 13:05:06

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

谢谢支持!
页: [1]
查看完整版本: STF 框架之 minicap 工具