minicap_工具使用


minicap介紹

minicap是開源項目STF(Smartphone Test Farm)中的一個工具,負責屏幕顯示。

stf自己寫了一個工具叫minicap用來替代原生的screencap,這個工具是stf框架的依賴工具之一,最近手頭上的項目剛好由於幀率卡頓需要優化,剛好來在testerhome社區看到對STF的介紹,WEB 端批量移動設備管理控制工具 STF 的環境搭建和運行

minicap工具是用NDK開發的,屬於Android的底層開發,該工具分為兩個部分,一個是動態連接庫.so文件,一個是minicap可執行文件。但不是通用的,因為CPU架構的不同分為不同的版本文件,STF提供的minicap文件根據CPU 的ABI分為如下4種:

arm64-v8aarmeabi-v7a,x86,x86_64 架構。而minicap.so文件在這個基礎上還要分為不同的sdk版本。這些都可以從Github地址:鏈接地址下載而來

結構樹目錄.
├── 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

准備對應文件

a、查看CPU架構(adb shell getprop ro.product.cpu.abi)及查看android版本level(adb shell getprop ro.build.version.sdk)

b、根據上面獲取的信息,將適合設備的可執行文件和.so文件push到手機的/data/local/tmp目錄下,或者在STF框架的源碼下找到vendor/minicap文件夾下

c、adb shell進入到目錄下chmod 777 minicap

d、測試一下minicap是否可用:(-P后面跟的參數為你屏幕的尺寸)

   adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1920@1080x1920/0 -t

安裝運行環境

a、安裝nodejs:

  查看版本號:node -v

b、安裝運行依賴 ws和express包 

  npm install ws –g

  npm install express -g

啟動手機端服務

 就是啟動了一個socket服務器

 adb shell LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P 1080x1920@1080x1920/0

本地端口轉發

a、跟上面的socket服務通信,首先我們要將本地的端口映射到minicap工具上,端口隨意:

  adb forward tcp:1717 localabstract:minicap

 b、輸入 node app.js 回車啟動服務:

  控制台顯示  Listening on port 9002 表示啟動成功

c、瀏覽器打開本地 localhost:9002  鏈接地址,查看

 

獲取信息

然后使用命令nc localhost 1717來與minicap通信,然后你會發現好多亂碼。官方提供了一個demo來看效果,在minicap項目下的example目錄

但是這些信息是有規則的,只是我們無法實際查看。但是我們做的工具需要用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的實現:

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.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.Stack; import java.util.concurrent.ConcurrentLinkedQueue; import javax.imageio.ImageIO; import org.apache.log4j.Logger; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.CollectingOutputReceiver; import com.android.ddmlib.IDevice; import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.SyncException; import com.android.ddmlib.TimeoutException; import com.wuba.utils.DirStructureUtil; import com.wuba.utils.TimeUtil; /** * @date 2015年8月12日 上午11:02:53 */ public class MiniCapUtil { private Logger LOG = Logger.getLogger(MiniCapUtil.class); // CPU架構的種類 public static final String ABIS_ARM64_V8A = "arm64-v8a"; public static final String ABIS_ARMEABI_V7A = "armeabi-v7a"; public static final String ABIS_X86 = "x86"; public static final String ABIS_X86_64 = "x86_64"; private Queue<byte[]> dataQueue = new ConcurrentLinkedQueue<byte[]>(); private Banner banner = new Banner(); private static final int PORT = 1717; private IDevice device; private String REMOTE_PATH = "/data/local/tmp"; private String ABI_COMMAND = "ro.product.cpu.abi"; private String SDK_COMMAND = "ro.build.version.sdk"; private String MINICAP_BIN = "minicap"; private String MINICAP_SO = "minicap.so"; private String MINICAP_CHMOD_COMMAND = "chmod 777 %s/%s"; private String MINICAP_WM_SIZE_COMMAND = "wm size"; private String MINICAP_START_COMMAND = "LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P %s@%s/0"; private boolean isRunning = false; public MiniCapUtil(IDevice device) { this.device = device; init(); } /** * 將minicap的二進制和.so文件push到/data/local/tmp文件夾下,啟動minicap服務 */ private void init() { String abi = device.getProperty(ABI_COMMAND); String sdk = device.getProperty(SDK_COMMAND); File minicapBinFile = new File(DirStructureUtil.getMinicapBin(), abi + File.separator + MINICAP_BIN); File minicapSoFile = new File(DirStructureUtil.getMinicapSo(), "android-" + sdk + File.separator + abi + File.separator + MINICAP_SO); try { // 將minicap的可執行文件和.so文件一起push到設備中 device.pushFile(minicapBinFile.getAbsolutePath(), REMOTE_PATH + File.separator + MINICAP_BIN); device.pushFile(minicapSoFile.getAbsolutePath(), REMOTE_PATH + File.separator + MINICAP_SO); executeShellCommand(String.format(MINICAP_CHMOD_COMMAND, REMOTE_PATH, MINICAP_BIN)); // 端口轉發 device.createForward(PORT, "minicap", DeviceUnixSocketNamespace.ABSTRACT); // 獲取設備屏幕的尺寸 String output = executeShellCommand(MINICAP_WM_SIZE_COMMAND); String size = output.split(":")[1].trim(); final String startCommand = String.format(MINICAP_START_COMMAND, size, size); // 啟動minicap服務 new Thread(new Runnable() { @Override public void run() { LOG.info("minicap服務器啟動"); executeShellCommand(startCommand); } }).start(); } catch (SyncException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (AdbCommandRejectedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (TimeoutException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private String executeShellCommand(String command) { CollectingOutputReceiver output = new CollectingOutputReceiver(); try { device.executeShellCommand(command, output, 0); } catch (TimeoutException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (AdbCommandRejectedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ShellCommandUnresponsiveException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return output.getOutput(); } public void startScreenListener() { isRunning = true; new Thread(new ImageConverter()).start(); new Thread(new ImageBinaryFrameCollector()).start(); } public void stopScreenListener() { isRunning = false; } private synchronized void createImageFromByte(byte[] binaryData) { InputStream in = new ByteArrayInputStream(binaryData); try { BufferedImage bufferedImage = ImageIO.read(in); ImageIO.write(bufferedImage, "jpg", new File("screen.jpg")); } catch (IOException e) { e.printStackTrace(); } } // 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; } class ImageBinaryFrameCollector implements Runnable { private Socket socket; @Override public void run() { LOG.debug("圖片二進制數據收集器已經開啟"); // TODO Auto-generated method stub InputStream stream = null; DataInputStream input = null; try { socket = new Socket("localhost", PORT); stream = socket.getInputStream(); input = new DataInputStream(stream); while (isRunning) { byte[] buffer; int len = 0; while (len == 0) { len = input.available(); } buffer = new byte[len]; input.read(buffer); dataQueue.add(buffer); } } catch (IOException e) { e.printStackTrace(); } finally { if (socket != null && socket.isConnected()) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } if (stream != null) { try { stream.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } LOG.debug("圖片二進制數據收集器已關閉"); } } class ImageConverter implements Runnable { private int readBannerBytes = 0; private int bannerLength = 2; private int readFrameBytes = 0; private int frameBodyLength = 0; private byte[] frameBody = new byte[0]; @Override public void run() { LOG.debug("圖片生成器已經開啟"); long start = System.currentTimeMillis(); while (isRunning) { byte[] binaryData = dataQueue.poll(); if (binaryData == null) continue; int len = binaryData.length; for (int cursor = 0; cursor < len;) { int byte10 = binaryData[cursor] & 0xff; if (readBannerBytes < bannerLength) { cursor = parserBanner(cursor, byte10); } else if (readFrameBytes < 4) { // 第二次的緩沖區中前4位數字和為frame的緩沖區大小 frameBodyLength += (byte10 << (readFrameBytes * 8)) >>> 0; cursor += 1; readFrameBytes += 1; } else { if (len - cursor >= frameBodyLength) { byte[] subByte = subByteArray(binaryData, 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; } byte[] finalBytes = subByteArray(frameBody, 0, frameBody.length); // 轉化成bufferImage createImageFromByte(finalBytes); long current = System.currentTimeMillis(); LOG.info("圖片已生成,耗時: " + TimeUtil.formatElapsedTime(current - start)); start = current; cursor += frameBodyLength; frameBodyLength = 0; readFrameBytes = 0; frameBody = new byte[0]; } else { byte[] subByte = subByteArray(binaryData, cursor, len); frameBody = byteMerger(frameBody, subByte); frameBodyLength -= (len - cursor); readFrameBytes += (len - cursor); cursor = len; } } } } LOG.debug("圖片生成器已關閉"); } private int parserBanner(int cursor, int byte10) { 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.debug(banner.toString()); } return cursor; } } }

總結

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秒內。

 

微信搜索【水勺子】關注我,獲取更多詳細信息


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM