◆版權聲明:本文出自胖喵~的博客,轉載必須注明出處。
轉載請注明出處: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
