【Android測試】Android截圖的深水區


  ◆版權聲明:本文出自胖喵~的博客,轉載必須注明出處。

  轉載請注明出處:http://www.cnblogs.com/by-dream/p/6113059.html 

 

 

需求


  這兩天遇到這樣一個事情,因為某測試任務,需要在操作過程中連續的截圖,最終分析截圖。之前同事用的工具兼容性特別的不好,需要root,並且只適配固定幾個版本的機型,因此我決定自己實現一個。首先最先想到的就是使用Uiautomator 1中自帶的API來截圖。

  我們看下Uiautomator 1.0中提供的截圖相關的API是什么樣子的,在UiDevice中,我們找到了這個函數:

  

  很明顯,這個函數的調用就會截圖一次,並且每一次截圖圖片質量肯定很大,會消耗很多的時間,因此不能達到快速連續的截圖。不過我們又發現另外一個函數,貌似可以控制圖片質量:

  

  那我們就試試這兩個截圖的效果吧。

 

 

開始動手


  這里我在Uiautomator(對Uiautomator還不熟悉的同學請參考我的Uiautomator系列的三篇文章)中實現了如下的代碼:

    

  我們去手機的目錄下看看這兩個圖片:

  

  我們可以看到圖片的大小是一樣大的,咦真是奇怪,打開圖片看看圖片的真實效果如何呢?

  

  對比了下兩張圖片的清晰度,幾乎沒什么區別,那怎么回事呢?因此我決定看看這塊的代碼一探究竟。

 

 

源碼剖析


  這里給大家也提供一些源碼(點擊下載),拿到Uiautomator1.0版本的源碼后,我們去找UiDevice。

  

  這里可以看到不帶參數的tackscreenshot就是調用了帶參數的,只不過給了個默認值而已,那么兩張圖更應該一樣啊,我們接着再往后看:

  

  這里說一下 Tracer 是用來記錄跟蹤log的,可以忽略。因此我們繼續跟進 getAutomatorBridge():

  

  我們看看這個函數返回的變量是什么:

  

  這里在源碼中,我沒看到這個類,不過看到了一個 abstract 的UiAutomatorBridge 一個抽象類,那么基本上就確定這二者是集成的關系了,於是打開UiAutomatorBridge,繼續尋找 takeScreenshot 函數,果然就找到:

  

  這里面第一步獲得Bitmap對象是核心,而獲取Bitmap的方法,又和下面這個變量有關系:

  

  看它初始化的位置,那么我們自己構造就有點難了,因此我決定這里按照這個思路來進行反射。

 

 

反射獲取


  如果還不懂反射的話,建議先看看我的另一篇講反射的文章《反射技術引入》。這里我的思路是這樣的:

  

  

  從提供的API getUiDevice()入手,直到拿到Bitmap對象。話不多說,直接看整個的代碼實現的過程吧。

 1     void takeScreenShot()
 2     {
 3         File files1 = new File("/mnt/sdcard/xiaobo/pic1.png");
 4         File files2 = new File("/mnt/sdcard/xiaobo/pic2-ref.png");
 5         
 6         getUiDevice().takeScreenshot(files1);
 7 
 8         try
 9         {
10             reflectTakeScreenshot(files2);
11             
12         } catch (NoSuchMethodException e)    {
13             e.printStackTrace();
14         } catch (SecurityException e)    {
15             e.printStackTrace();
16         } catch (IllegalAccessException e)    {
17             e.printStackTrace();
18         } catch (IllegalArgumentException e)    {
19             e.printStackTrace();
20         } catch (InvocationTargetException e)    {
21             e.printStackTrace();
22         } catch (ClassNotFoundException e)    {
23             e.printStackTrace();
24         } catch (NoSuchFieldException e)        {
25             e.printStackTrace();
26         }
27     }
28     
29     /**
30      * 反射方式拿到Bitmap截圖
31      * */
32     void reflectTakeScreenshot(File files) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException, NoSuchFieldException
33     {
34         // 得到UiDevice 對象
35         UiDevice mdevice = getUiDevice();
36         
37         // 反射getAutomatorBridge()得到InstrumentationUiAutomatorBridge對象
38         Method method = mdevice.getClass().getDeclaredMethod("getAutomatorBridge", new Class[] {});
39         method.setAccessible(true);
40         Object bridge = method.invoke(mdevice, new Object[] {});
41         
42         // 反射得到UiAutomation對象
43         Class tmp = Class.forName("com.android.uiautomator.core.UiAutomatorBridge");
44         Field fields = tmp.getDeclaredField("mUiAutomation");
45         fields.setAccessible(true);
46         UiAutomation mUiAutomation = (UiAutomation)fields.get(bridge);
47         
48         // 顯式調用
49         Bitmap screenshot = mUiAutomation.takeScreenshot();
50         
51         save(screenshot, files);
52     }
53     
54     /**
55      * 參考谷歌的源代碼進行保存
56      * */
57     void save(Bitmap screenshot, File files)
58     {
59         if (screenshot == null) {
60             return ;
61        }
62        BufferedOutputStream bos = null;
63         try {
64             bos = new BufferedOutputStream(new FileOutputStream(files));
65             if (bos != null) {
66                 screenshot.compress(Bitmap.CompressFormat.PNG, 5, bos);
67                 bos.flush();
68             }
69         } catch (IOException ioe) {
70             Log.e("bryan", "failed to save screen shot to file", ioe);
71             return ;
72         } finally {
73             if (bos != null) {
74                 try {
75                     bos.close();
76                 } catch (IOException ioe) {
77                     /* ignore */
78                 }
79             }
80             screenshot.recycle();
81         } 
82     }
83     

  拿到Bitmap對象后,我們也參考谷歌的寫法,保存到本地,這里可以看到(66行)quality的值我依然給傳5。我們執行一下看看結果:

  

  可以看到大小還是一樣的,並且我自己打開后發現清晰度也是一樣的。這就奇怪了,究竟是怎么回事呢?

 

 

Google工程師的bug


  在圖片壓縮還不生效的情況下,我們就得仔細看看壓縮的代碼了。這里我們重點看下高亮的那句代碼:

  

  我勾選出的這一句話就是最核心的關鍵,我們先去查一下這個函數的API用法,不查不知道,一查全明白了:

    

  圖中我勾選中的這句話的意思是,對於一些無損的PNG的圖片,會忽略quality這個屬性的設置。但是我們在源碼中卻可以看到,谷歌的工程師對於PNG還是使用了壓縮,看來得給他提個bug了,哈哈。知道了PNG不能壓縮,那么我們把壓縮的方式切換成JPEG試試:

screenshot.compress(Bitmap.CompressFormat.PNG, quality, bos);

  這句替換為

screenshot.compress(Bitmap.CompressFormat.JPEG, quality, bos);

  修改完后,我們運行看看結果:

  

  壓縮終於生效了,我們看看真實兩張圖片的效果:

  

  

 

再次優化


  這個時候我想,能否滿足連續截圖的需求呢?如果截一張保存一張,那么保存的過程肯定會很慢,那么能否先記錄在內存中,最終結束的時候再寫文件呢?於是我講Bitmap對象壓入一個List中,結果保存了大概幾十張之后手機就卡死了。

  后來在深入了解了Bitmap的原理之后才知道,Bitmap對象在內存中的占用非常的高,原因是圖片按照長*寬存儲,並且每個像素點上可能還有多個位元素,因此加在一起就多了。我們可以看看占內存的情況:

  

  一張1920*1080的圖,原始的Bitmap占用為 7.9MB,經過壓縮后為225KB保存成為文件后,大小只剩下了5.6KB。所以對於讀取來的圖片只能壓縮完之后,再保存了。最終實現的代碼為:

  1 package QQ;
  2 
  3 import java.io.BufferedOutputStream;
  4 import java.io.File;
  5 import java.io.FileOutputStream;
  6 import java.io.IOException;
  7 import java.lang.reflect.Field;
  8 import java.lang.reflect.InvocationTargetException;
  9 import java.lang.reflect.Method;
 10 import java.util.Calendar;
 11 
 12 import android.R.integer;
 13 import android.app.UiAutomation;
 14 import android.graphics.Bitmap;
 15 import android.util.Log;
 16 
 17 import com.android.uiautomator.core.UiDevice;
 18 import com.android.uiautomator.core.UiObjectNotFoundException;
 19 import com.android.uiautomator.testrunner.UiAutomatorTestCase;
 20 
 21 public class Test_jietu extends UiAutomatorTestCase
 22 {
 23 
 24     public void testDemo() throws IOException, UiObjectNotFoundException {
 25 
 26         int i = 0;
 27         while (true)
 28         {
 29             System.out.println(++i);
 30             takeScreenShot();
 31         }
 32 
 33     }
 34 
 35     void takeScreenShot() {
 36         // File files1 = new File("/mnt/sdcard/xiaobo/pic1.png");
 37         // getUiDevice().takeScreenshot(files1);
 38 
 39         File files2 = new File("/mnt/sdcard/xiaobo/" + getTimeString() + ".jpeg");
 40 
 41         try
 42         {
 43             reflectTakeScreenshot(files2);
 44 
 45         } catch (NoSuchMethodException e)
 46         {
 47             e.printStackTrace();
 48         } catch (SecurityException e)
 49         {
 50             e.printStackTrace();
 51         } catch (IllegalAccessException e)
 52         {
 53             e.printStackTrace();
 54         } catch (IllegalArgumentException e)
 55         {
 56             e.printStackTrace();
 57         } catch (InvocationTargetException e)
 58         {
 59             e.printStackTrace();
 60         } catch (ClassNotFoundException e)
 61         {
 62             e.printStackTrace();
 63         } catch (NoSuchFieldException e)
 64         {
 65             e.printStackTrace();
 66         }
 67     }
 68 
 69     /**
 70      * 反射方式拿到Bitmap截圖
 71      * */
 72     void reflectTakeScreenshot(File files) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException,
 73             NoSuchFieldException {
 74         // 得到UiDevice 對象
 75         UiDevice mdevice = getUiDevice();
 76 
 77         // 反射getAutomatorBridge()得到InstrumentationUiAutomatorBridge對象
 78         Method method = mdevice.getClass().getDeclaredMethod("getAutomatorBridge", new Class[] {});
 79         method.setAccessible(true);
 80         Object bridge = method.invoke(mdevice, new Object[] {});
 81 
 82         // 反射得到UiAutomation對象
 83         Class tmp = Class.forName("com.android.uiautomator.core.UiAutomatorBridge");
 84         Field fields = tmp.getDeclaredField("mUiAutomation");
 85         fields.setAccessible(true);
 86         UiAutomation mUiAutomation = (UiAutomation) fields.get(bridge);
 87 
 88         // 顯式調用
 89         Bitmap screenshot = mUiAutomation.takeScreenshot();
 90 
 91         // 壓縮
 92         screenshot = compress(screenshot);
 93 
 94         save(screenshot, files);
 95     }
 96 
 97     /**
 98      * 參考谷歌的源代碼進行保存
 99      * */
100     void save(Bitmap screenshot, File files) {
101         if (screenshot == null)
102         {
103             return;
104         }
105 
106         BufferedOutputStream bos = null;
107         try
108         {
109             bos = new BufferedOutputStream(new FileOutputStream(files));
110             if (bos != null)
111             {
112                 screenshot.compress(Bitmap.CompressFormat.JPEG, 50, bos);
113                 bos.flush();
114             }
115         } catch (IOException ioe)
116         {
117             Log.e("bryan", "failed to save screen shot to file", ioe);
118             return;
119         } finally
120         {
121             if (bos != null)
122             {
123                 try
124                 {
125                     bos.close();
126                 } catch (IOException ioe)
127                 { /* ignore */}
128             }
129 
130             // 釋放Bitmap在c層的內存
131             screenshot.recycle();
132         }
133     }
134 
135     /**
136      * 簡單壓縮一下圖片
137      * */
138     Bitmap compress(Bitmap bitmap) {
139         System.out.println("source bitmap :" + bitmap.getByteCount());
140         if (bitmap != null)
141         {
142             bitmap = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth() / 6, bitmap.getHeight() / 6, true);
143             System.out.println("compress bitmap :" + bitmap.getByteCount());
144             return bitmap;
145         }
146         return bitmap;
147     }
148 
149     /*
150      * 得到當前時間
151      */
152     public String getTimeString() {
153         // 取得當前時間
154         Calendar calendar = Calendar.getInstance();
155         calendar.setTimeInMillis(System.currentTimeMillis());
156         return calendar.get(Calendar.HOUR_OF_DAY) + "_" + calendar.get(Calendar.MINUTE) + "_" + calendar.get(Calendar.SECOND) + "_" + calendar.get(Calendar.MILLISECOND);
157     }
158 
159 }

  這里提供了完整的工程供大家下載。當然如果有願意使用這個截圖的工具的小伙伴,可以下載這個jar包,然后使用下面兩條命令,就可以使用了。

  命令1:adb push Screenshot.jar /data/local/tmp/

  命令2:adb shell uiautomator runtest Screenshot.jar -c QQ.Test_jietu

 


免責聲明!

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



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