zoukankan      html  css  js  c++  java
  • 截图原理(一)——Android自动化测试学习历程

    把两节的内容汇总起来,第一节讲的是如何在apk中直接进行截屏,用到了Robotium的Solo类的takeScreenShot方法,有一个小的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();
                }
            }

    金阳光测试

    新浪微博:金阳光woody

             

              网站地址

    1、百度搜:金阳光测试

    2、官网:www.goldensunshine.cc

    微信公众号

  • 相关阅读:
    python functools.lru_cache做备忘功能
    python clock装饰器 计算函数执行时间,执行结果及传入的参数
    python 闭包计算移动均值及nonlocal的使用
    使用 memoryview 和 struct 查看一个 GIF 图像的首部
    寻医问药 爬虫
    defaultdict使用及__missing__理解
    字典推导式
    collections deque队列及其他队列
    bisect 二分查找
    python array 使用创建10万浮点数
  • 原文地址:https://www.cnblogs.com/keke-xiaoxiami/p/4325267.html
Copyright © 2011-2022 走看看