1. 程式人生 > >截圖原理(一)――Android自動化測試學習歷程

截圖原理(一)――Android自動化測試學習歷程

把兩節的內容彙總起來,第一節講的是如何在apk中直接進行截圖,用到了Robotium的Solo類的takeScreenShot方法,有一個小的 android demo,以及從方法一直往裡鑽,知道它具體是怎麼進行截圖的。

第二節講的是脫離apk,直接在PC端截圖,通過的是adb橋接的方式,呼叫ddmlib.jar包中的AndroidDebugBridge和IDevice的類,對其進行截圖,並儲存到我想要的位置,是可以寫成一個小工具的。

視訊地址:http://study.163.com/course/courseLearn.htm?courseId=712011#/learn/video?lessonId=877120&courseId=712011 

一、面試問題引入:

1、怎樣在一個app崩潰前復現bug操作步驟?(非手工和人眼操作)

答:可以通過截圖實現,在關鍵步驟處均進行截圖操作,這樣app崩潰了也能夠根據之前的截圖進行現場確認和步驟復現。那麼如何實現截圖?

可以通過:

1、monkeyrunner裡面——device.takeSnapshot()

2、Robotium裡面——solo.takeScreenshot(String pictureName)

面試問題:

(1)takeScreenshot的實現原理?通過哪些方法得到截圖?是單執行緒還是多執行緒?得到的檢視物件是單一View還是View陣列?如果沒有裝載sdk卡,或者說想要儲存在PC端,該如何處理呢?

二、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之後,程式就能跑通了。。。

問題總結:

1、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節點以上,否則會報錯。。。

三、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) {
        //1、得到目前螢幕所有檢視
        View decorView = getScreenshotView();
        if(decorView == null) 
            return;
        //2、初始化
        initScreenShotSaver();
        //3、例項化截圖物件
        ScreenshotRunnable runnable = new ScreenshotRunnable(decorView, name, quality);
        //4、呼叫截圖物件的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());
        }

第九步跳轉到的函式:(3、例項化截圖物件)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();
            }
        }