把兩節的內容彙總起來,第一節講的是如何在apk中直接進行截屏,用到了Robotium的Solo類的takeScreenShot方法,有一個小的demo,以及從方法一直往裏鑽,知道它具體是怎麼進行截屏的。java
第二節講的是脫離apk,直接在PC端截圖,經過的是adb橋接的方式,調用ddmlib.jar包中的AndroidDebugBridge和IDevice的類,對其進行截屏,並保存到我想要的位置,是能夠寫成一個小工具的。android
視頻地址:http://study.163.com/course/courseLearn.htm?courseId=712011#/learn/video?lessonId=877120&courseId=712011 面試
1、面試問題引入:數組
一、怎樣在一個app崩潰前復現bug操做步驟?(非手工和人眼操做)微信
答:能夠經過截圖實現,在關鍵步驟處均進行截圖操做,這樣app崩潰了也可以根據以前的截圖進行現場確認和步驟復現。那麼如何實現截圖?多線程
能夠經過:app
一、monkeyrunner裏面——device.takeSnapshot()less
二、Robotium裏面——solo.takeScreenshot(String pictureName)異步
面試問題:ide
(1)takeScreenshot的實現原理?經過哪些方法獲得截圖?是單線程仍是多線程?獲得的視圖對象是單一View仍是View數組?若是沒有裝載sdk卡,或者說想要保存在PC端,該如何處理呢?
2、Robotium實現截屏操做,及原理
具體的screenshot以及robotium在有源碼的狀況下的一個具體testcase類就是以下這樣的示例:
package com.li.xiami.test; import static org.junit.Assert.*; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.android.robotium.solo.Solo; import com.li.xiami.MainActivity; import android.test.ActivityInstrumentationTestCase2; public class ScreenShot extends ActivityInstrumentationTestCase2<MainActivity> { //包名 static String packageName = "com.li.xiami"; //聲明一個robotium的solo類 private Solo solo; private static String tag = "xiami"; //構造方法中寫好包名和類名,讓ActivityInstrumentationTestCase2可以找到被測試的app //的MainActivity @SuppressWarnings("deprecation") public ScreenShot(){ //super(packageName, MainActivity.class); super(MainActivity.class); } @Before protected void setUp() throws Exception { super.setUp(); //初始化solo對象 solo = new Solo(getInstrumentation(), getActivity()); } @After protected void tearDown() throws Exception { solo.finishOpenedActivities(); } @Test public void test() { solo.clickOnButton("OK"); solo.sleep(1000); solo.takeScreenshot("123picture"); solo.sleep(3000); } }
第一次運行:
可是第一次運行的時候出現了這樣的問題:提示:Test Run Failed:java.lang.ClassNotFoundException
可是我該配置的都配置了(包括bulid path的配置,solo包的導入以及jnuit4的包的導入等,以及類名也檢查了好幾遍都是對的啊),後來才找到了問題的緣由:
個人project.properties中的target=android-18,而後個人AndroidManifest.xml中配置的uses-sdk的targetSdkVersion是寫的17:
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="17" />
因此就出現了這個問題,把這個也修改爲18以後,程序就能跑通了。。。
問題總結:
一、robotium中可能出現的Test run failed:classnotfoundexception的可能緣由:
(1)jar包的導入有問題,須要確認build path的Libraries和Order and Export,都須要勾選上
(2)真的是待測的apk的MainActivity的類沒找到,好比說有源碼的狀況,類名寫錯了;或者是無源碼的狀況,MainActivity的類名獲取錯誤了進而也寫錯了致使出現的這個問題
(3)就是剛纔出現的這個project.properties中的target與androidManifest.xml中配置的targetSdkVersion不匹配
全部說各類問題啊,不必定報的這個exception,就必定是你class not found。。。
第二次運行:
第二次運行好不容易跑通了,可是經過DDMS裏面的File Explorer工具查看mnt/sdcard/Robotium-Screenshots目錄下查看是否生成了我想要的文件,結果發現根本就沒有Robotium-Screenshots文件夾,也就是說當第一次往sdk卡里面寫東西的時候,居然連文件夾都沒有創建起來,那就要想到是否是權限問題?
而後就須要配置uses-permission節點:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
配置這個節點的具體位置在:manifest裏面,可是在Application節點以外,並且在Application節點以上,不然會報錯。。。
3、Robotium的截屏處理的代碼分析
步驟:
(1)
代碼分析:
追本溯源,開始找路。。。
第一步跳轉到的函數:takeScreenshot(String name)
/** * Takes a screenshot and saves it with the specified name in "/sdcard/Robotium-Screenshots/". * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test. * * @param name the name to give the screenshot * */
//上面的話翻譯下來就是:存儲的位置肯定了,就是在mnt/sdcard/Robotium-Screenshots/目錄下
//可是須要寫sd卡的權限,須要給under test的application在AndroidManifest.xml中配置permission,那麼這裏也就解釋了我上面的運行過程當中第二個問題
public void takeScreenshot(String name){ takeScreenshot(name, 100); }
第二步跳轉到的函數:takeScreenshot(String name, int quality)
/** * Takes a screenshot and saves the image with the specified name in "/sdcard/Robotium-Screenshots/". * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test. * * @param name the name to give the screenshot * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality) * */
//上面的話翻譯下來就是:圖片存儲位置以及讀寫權限與第一步中相同
//參數分別表示picture的name,以及清晰度(從0到100),默認是100,固然你也能夠直接在函數中調用這個函數,而後設置這個quality的值
public void takeScreenshot(String name, int quality){ screenshotTaker.takeScreenshot(name, quality); }
第三步跳轉到的函數:screenshotTaker.takeScreenshot(String name, int quality)
/** * Takes a screenshot and saves it in "/sdcard/Robotium-Screenshots/". * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test. * * @param view the view to take screenshot of * @param name the name to give the screenshot image * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). */
//第三步走到了一個新的類中,是screenShotTaker的類
//這個纔是真正的執行Screenshot的函數,這個纔是截圖的邏輯
public void takeScreenshot(final String name, final int quality) { //一、獲得目前屏幕全部視圖 View decorView = getScreenshotView(); if(decorView == null) return; //二、初始化 initScreenShotSaver(); //三、實例化截圖對象 ScreenshotRunnable runnable = new ScreenshotRunnable(decorView, name, quality); //四、調用截圖對象的run方法 activityUtils.getCurrentActivity(false).runOnUiThread(runnable); }
第四步(1 獲得屏幕全部視圖)跳轉到的函數:getScreenshotView()
/** * Gets the proper view to use for a screenshot. */ private View getScreenshotView() { //獲取到屏幕上的view View decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews()); final long endTime = SystemClock.uptimeMillis() + Timeout.getSmallTimeout(); while (decorView == null) { final boolean timedOut = SystemClock.uptimeMillis() > endTime; if (timedOut){ return null; } sleeper.sleepMini(); decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews()); } wrapAllGLViews(decorView); return decorView; }
第五步跳轉到的函數:viewFetcher.getWindowDecorViews()
/** * Returns the WindorDecorViews shown on the screen. * * @return the WindorDecorViews shown on the screen */ //翻譯下來就是:獲取到展現在screen上的全部WindowDecorViews,是一個View的數組,而後這個view的數組返回後,再做爲viewFetcher.getRecentDecorView的參數
//用反射方法去獲取 View 視圖數組
@SuppressWarnings("unchecked") public View[] getWindowDecorViews() { Field viewsField; Field instanceField; try { viewsField = windowManager.getDeclaredField("mViews"); instanceField = windowManager.getDeclaredField(windowManagerString); viewsField.setAccessible(true); instanceField.setAccessible(true); Object instance = instanceField.get(null); View[] result; if (android.os.Build.VERSION.SDK_INT >= 19) { result = ((ArrayList<View>) viewsField.get(instance)).toArray(new View[0]); } else { result = (View[]) viewsField.get(instance); } return result; } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; }
第六步跳轉到的函數:viewFetcher.getRecentDecorView(View[] views)
/** * Returns the most recent DecorView * * @param views the views to check * @return the most recent DecorView */
//翻譯下來就是:返回最近的DecorView
public final View getRecentDecorView(View[] views) { if(views == null) return null; final View[] decorViews = new View[views.length]; int i = 0; View view; //經過遍歷View數組,來獲得most recent DecorView for (int j = 0; j < views.length; j++) { view = views[j]; if (view != null && view.getClass().getName() .equals("com.android.internal.policy.impl.PhoneWindow$DecorView")) { decorViews[i] = view; i++; } } return getRecentContainer(decorViews); }
第七步:(1中的獲取屏幕已經結束,看2的init操做)
/** * This method initializes the aysnc screenshot saving logic */
//翻譯下來就是:初始化一個aysnc(異步)的sreenshot的保存邏輯
private void initScreenShotSaver() { if(screenShotSaverThread == null || screenShotSaver == null) { //聲明一個HandlerThread對象 screenShotSaverThread = new HandlerThread("ScreenShotSaver"); screenShotSaverThread.start(); //把screenShotSaverThread捆綁到handler screenShotSaver = new ScreenShotSaver(screenShotSaverThread); } }
可是這裏用到了HandlerThread和Handler,看之。。。
第八步跳轉的函數:ScreenShotSaver(HandlerThread thread)
/** * This class is a Handler which deals with saving the screenshots on a separate thread. * * The screenshot logic by necessity has to run on the ui thread. However, in practice * it seems that saving a screenshot (with quality 100) takes approx twice as long * as taking it in the first place. * * Saving the screenshots in a separate thread like this will thus make the screenshot * process approx 3x faster as far as the main thread is concerned. * */
//翻譯下來就是:這是一個繼承自Handler,在一個單獨的thread上處理如何存儲sreenchots的類
//screenshot的邏輯必需要跑在ui線程上,然而,事實上,好像這個保存screenshot反而花費了將近2倍的時間
//保存這個screenshots在另外一個線程中,就會使得這個處理可以快三倍,固然是與跑在主線程上相比而言
private class ScreenShotSaver extends Handler { public ScreenShotSaver(HandlerThread thread) { super(thread.getLooper()); }
第九步跳轉到的函數:(三、實例化截圖對象)ScreenshotRunnable(View view, String name, int quality)
這個ScreenshotRunnable類是實現了Runnable接口中的run方法,在其中根據不一樣的view類型進行不一樣的bitmap的轉換,獲得bitmap對象,以後若該bitmap不爲空,則存儲到sd卡中(調用的 screenShotSaver.saveBitmap(BitMap b, String name, int quality)),而後這裏的這個screenShotSaver是一個繼承自Handler的類
/** * Here we have a Runnable which is responsible for taking the actual screenshot, * and then posting the bitmap to a Handler which will save it. * 這是把runnable對象放進Handler對象裏面經過獲得的view去變成bitmap * 把runnable的run方法實現,首先把view轉成bitmap對象,以後調用以前的screenShotSaver的 * Handler對象save這個bitmap的對象 * This Runnable is run on the UI thread. */ private class ScreenshotRunnable implements Runnable { private View view; private String name; private int quality; public ScreenshotRunnable(final View _view, final String _name, final int _quality) { view = _view; name = _name; quality = _quality; } public void run() { if(view !=null){ Bitmap b; //根據是不是WebView作出不一樣的處理 if(view instanceof WebView){ b = getBitmapOfWebView((WebView) view); } else{ b = getBitmapOfView(view); } if(b != null) //若是bitmap對象不爲空,就存到sd卡里 screenShotSaver.saveBitmap(b, name, quality); else Log.d(LOG_TAG, "NULL BITMAP!!"); } } }
第十步跳轉到的函數:saveBitmap(Bitmap bitmap, String name, int quality),這裏會產生一個message,而後經過handlemessage來處理這個message
/** * This method posts a Bitmap with meta-data to the Handler queue. * * @param bitmap the bitmap to save * @param name the name of the file * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). */ public void saveBitmap(Bitmap bitmap, String name, int quality) { //獲取一個message對象,而後把bitmap的信息存儲到這個message中 //將這個message發出去,發送到looper,而後這個message會被handleMessage接收
//這裏沒有直接存儲,而使用message,是想要用到looper,使用looper的好處是:能夠短期內放10個左右的截圖
Message message = this.obtainMessage(); message.arg1 = quality; message.obj = bitmap; message.getData().putString("name", name); this.sendMessage(message); }
具體的handleMessage函數以下所示,也是位於這個screenShotSaver的類中:
/** * Here we process the Handler queue and save the bitmaps. * * @param message A Message containing the bitmap to save, and some metadata. */ public void handleMessage(Message message) { //複寫Handler的handleMessage方法,而後獲取到message對象,以後調用saveFile方法方法保存bitmap對象 String name = message.getData().getString("name"); int quality = message.arg1; Bitmap b = (Bitmap)message.obj; if(b != null) { saveFile(name, b, quality); b.recycle(); } else { Log.d(LOG_TAG, "NULL BITMAP!!"); } }
接下來就到了saveFile的函數中:
/** * Saves a file. * * @param name the name of the file * @param b the bitmap to save * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality). * */ private void saveFile(String name, Bitmap b, int quality){ //構造一個File輸出流,寫bitmap對象到sd卡 FileOutputStream fos = null; String fileName = getFileName(name); // File directory = new File(Environment.getExternalStorageDirectory() + "/Robotium-Screenshots/"); directory.mkdir(); File fileToSave = new File(directory,fileName); try { //初始化一個File的輸入輸出類,用以進行file的存儲,以後調用compress方法寫入 fos = new FileOutputStream(fileToSave); if (b.compress(Bitmap.CompressFormat.JPEG, quality, fos) == false) Log.d(LOG_TAG, "Compress/Write failed"); fos.flush(); fos.close(); } catch (Exception e) { Log.d(LOG_TAG, "Can't save the screenshot! Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test."); e.printStackTrace(); } }
金陽光測試 |
新浪微博:金陽光woody |
網站地址 |
一、百度搜:金陽光測試 二、官網:www.goldensunshine.cc |
微信公衆號 |