把两节的内容汇总起来,第一节讲的是如何在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类就是如下这样的示例:

  1. package com.li.xiami.test;
  2.  
  3. import static org.junit.Assert.*;
  4.  
  5. import org.junit.After;
  6. import org.junit.Before;
  7. import org.junit.Test;
  8.  
  9. import com.android.robotium.solo.Solo;
  10. import com.li.xiami.MainActivity;
  11.  
  12. import android.test.ActivityInstrumentationTestCase2;
  13.  
  14. public class ScreenShot extends ActivityInstrumentationTestCase2<MainActivity> {
  15.  
  16. //包名
  17. static String packageName = "com.li.xiami";
  18. //声明一个robotium的solo类
  19. private Solo solo;
  20.  
  21. private static String tag = "xiami";
  22.  
  23. //构造方法中写好包名和类名,让ActivityInstrumentationTestCase2能够找到被测试的app
  24. //的MainActivity
  25. @SuppressWarnings("deprecation")
  26. public ScreenShot(){
  27. //super(packageName, MainActivity.class);
  28. super(MainActivity.class);
  29. }
  30.  
  31. @Before
  32. protected void setUp() throws Exception {
  33. super.setUp();
  34. //初始化solo对象
  35. solo = new Solo(getInstrumentation(), getActivity());
  36. }
  37.  
  38. @After
  39. protected void tearDown() throws Exception {
  40. solo.finishOpenedActivities();
  41. }
  42.  
  43. @Test
  44. public void test() {
  45. solo.clickOnButton("OK");
  46. solo.sleep(1000);
  47. solo.takeScreenshot("123picture");
  48. solo.sleep(3000);
  49. }
  50.  
  51. }

第一次运行:

但是第一次运行的时候出现了这样的问题:提示: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)

  1. /**
  2. * Takes a screenshot and saves it with the specified name in "/sdcard/Robotium-Screenshots/".
  3. * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test.
  4. *
  5. * @param name the name to give the screenshot
  6. *
  7. */
    //上面的话翻译下来就是:存储的位置确定了,就是在mnt/sdcard/Robotium-Screenshots/目录下
    //但是需要写sd卡的权限,需要给under test的application在AndroidManifest.xml中配置permission,那么这里也就解释了我上面的运行过程中第二个问题
  8. public void takeScreenshot(String name){
  9. takeScreenshot(name, 100);
  10. }

第二步跳转到的函数:takeScreenshot(String name, int quality)

  1. /**
  2. * Takes a screenshot and saves the image with the specified name in "/sdcard/Robotium-Screenshots/".
  3. * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test.
  4. *
  5. * @param name the name to give the screenshot
  6. * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality)
  7. *
  8. */
    //上面的话翻译下来就是:图片存储位置以及读写权限与第一步中相同
    //参数分别表示picture的name,以及清晰度(从0到100),默认是100,当然你也可以直接在函数中调用这个函数,然后设置这个quality的值
  9. public void takeScreenshot(String name, int quality){
  10. screenshotTaker.takeScreenshot(name, quality);
  11. }

第三步跳转到的函数:screenshotTaker.takeScreenshot(String name, int quality)

  1. /**
  2. * Takes a screenshot and saves it in "/sdcard/Robotium-Screenshots/".
  3. * Requires write permission (android.permission.WRITE_EXTERNAL_STORAGE) in AndroidManifest.xml of the application under test.
  4. *
  5. * @param view the view to take screenshot of
  6. * @param name the name to give the screenshot image
  7. * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
  8. */
    //第三步走到了一个新的类中,是screenShotTaker的类
    //这个才是真正的执行Screenshot的函数,这个才是截图的逻辑
  1. public void takeScreenshot(final String name, final int quality) {
  2. //1、得到目前屏幕所有视图
  3. View decorView = getScreenshotView();
  4. if(decorView == null)
  5. return;
  6. //2、初始化
  7. initScreenShotSaver();
  8. //3、实例化截图对象
  9. ScreenshotRunnable runnable = new ScreenshotRunnable(decorView, name, quality);
  10. //4、调用截图对象的run方法
  11. activityUtils.getCurrentActivity(false).runOnUiThread(runnable);
  12. }

第四步(1 得到屏幕所有视图)跳转到的函数:getScreenshotView()

  1. /**
  2. * Gets the proper view to use for a screenshot.
  3. */
  4. private View getScreenshotView() {
  5. //获取到屏幕上的view
  6. View decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews());
  7. final long endTime = SystemClock.uptimeMillis() + Timeout.getSmallTimeout();
  8.  
  9. while (decorView == null) {
  10.  
  11. final boolean timedOut = SystemClock.uptimeMillis() > endTime;
  12.  
  13. if (timedOut){
  14. return null;
  15. }
  16. sleeper.sleepMini();
  17. decorView = viewFetcher.getRecentDecorView(viewFetcher.getWindowDecorViews());
  18. }
  19. wrapAllGLViews(decorView);
  20.  
  21. return decorView;
  22. }

第五步跳转到的函数:viewFetcher.getWindowDecorViews()

  1. /**
  2. * Returns the WindorDecorViews shown on the screen.
  3. *
  4. * @return the WindorDecorViews shown on the screen
  5. */
  6. //翻译下来就是:获取到展示在screen上的所有WindowDecorViews,是一个View的数组,然后这个view的数组返回后,再作为viewFetcher.getRecentDecorView的参数
    //用反射方法去获取 View 视图数组
        
  7. @SuppressWarnings("unchecked")
  8. public View[] getWindowDecorViews()
  9. {
  10. Field viewsField;
  11. Field instanceField;
  12. try {
  13. viewsField = windowManager.getDeclaredField("mViews");
  14. instanceField = windowManager.getDeclaredField(windowManagerString);
  15. viewsField.setAccessible(true);
  16. instanceField.setAccessible(true);
  17. Object instance = instanceField.get(null);
  18. View[] result;
  19. if (android.os.Build.VERSION.SDK_INT >= 19) {
  20. result = ((ArrayList<View>) viewsField.get(instance)).toArray(new View[0]);
  21. } else {
  22. result = (View[]) viewsField.get(instance);
  23. }
  24. return result;
  25. } catch (SecurityException e) {
  26. e.printStackTrace();
  27. } catch (NoSuchFieldException e) {
  28. e.printStackTrace();
  29. } catch (IllegalArgumentException e) {
  30. e.printStackTrace();
  31. } catch (IllegalAccessException e) {
  32. e.printStackTrace();
  33. }
  34. return null;
  35. }

第六步跳转到的函数:viewFetcher.getRecentDecorView(View[] views)

  1. /**
  2. * Returns the most recent DecorView
  3. *
  4. * @param views the views to check
  5. * @return the most recent DecorView
  6. */
    //翻译下来就是:返回最近的DecorView
  7.  
  8. public final View getRecentDecorView(View[] views) {
  9. if(views == null)
  10. return null;
  11.  
  12. final View[] decorViews = new View[views.length];
  13. int i = 0;
  14. View view;
  15.       //通过遍历View数组,来得到most recent DecorView
  16. for (int j = 0; j < views.length; j++) {
  17. view = views[j];
  18. if (view != null && view.getClass().getName()
  19. .equals("com.android.internal.policy.impl.PhoneWindow$DecorView")) {
  20. decorViews[i] = view;
  21. i++;
  22. }
  23. }
  24. return getRecentContainer(decorViews);
  25. }

第七步:(1中的获取屏幕已经结束,看2的init操作)

  1. /**
  2. * This method initializes the aysnc screenshot saving logic
  3. */
      //翻译下来就是:初始化一个aysnc(异步)的sreenshot的保存逻辑
  4. private void initScreenShotSaver() {
  5. if(screenShotSaverThread == null || screenShotSaver == null) {
  6. //声明一个HandlerThread对象
  7. screenShotSaverThread = new HandlerThread("ScreenShotSaver");
  8. screenShotSaverThread.start();
  9. //把screenShotSaverThread捆绑到handler
  10. screenShotSaver = new ScreenShotSaver(screenShotSaverThread);
  11. }
  12. }

但是这里用到了HandlerThread和Handler,看之。。。

第八步跳转的函数:ScreenShotSaver(HandlerThread thread)

  1. /**
  2. * This class is a Handler which deals with saving the screenshots on a separate thread.
  3. *
  4. * The screenshot logic by necessity has to run on the ui thread. However, in practice
  5. * it seems that saving a screenshot (with quality 100) takes approx twice as long
  6. * as taking it in the first place.
  7. *
  8. * Saving the screenshots in a separate thread like this will thus make the screenshot
  9. * process approx 3x faster as far as the main thread is concerned.
  10. *
  11. */
       //翻译下来就是:这是一个继承自Handler,在一个单独的thread上处理如何存储sreenchots的类
    //screenshot的逻辑必须要跑在ui线程上,然而,事实上,好像这个保存screenshot反而花费了将近2倍的时间
    //保存这个screenshots在另一个线程中,就会使得这个处理能够快三倍,当然是与跑在主线程上相比而言
  12. private class ScreenShotSaver extends Handler {
  13. public ScreenShotSaver(HandlerThread thread) {
  14. super(thread.getLooper());
  15. }

第九步跳转到的函数:(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的类

  1. /**
  2. * Here we have a Runnable which is responsible for taking the actual screenshot,
  3. * and then posting the bitmap to a Handler which will save it.
  4. * 这是把runnable对象放进Handler对象里面通过得到的view去变成bitmap
  5. * 把runnable的run方法实现,首先把view转成bitmap对象,之后调用之前的screenShotSaver的
  6. * Handler对象save这个bitmap的对象
  7. * This Runnable is run on the UI thread.
  8. */
  9. private class ScreenshotRunnable implements Runnable {
  10.  
  11. private View view;
  12. private String name;
  13. private int quality;
  14.  
  15. public ScreenshotRunnable(final View _view, final String _name, final int _quality) {
  16. view = _view;
  17. name = _name;
  18. quality = _quality;
  19. }
  20.  
  21. public void run() {
  22. if(view !=null){
  23. Bitmap b;
  24. //根据是否是WebView做出不同的处理
  25. if(view instanceof WebView){
  26. b = getBitmapOfWebView((WebView) view);
  27. }
  28. else{
  29. b = getBitmapOfView(view);
  30. }
  31. if(b != null)
  32. //如果bitmap对象不为空,就存到sd卡里
  33. screenShotSaver.saveBitmap(b, name, quality);
  34. else
  35. Log.d(LOG_TAG, "NULL BITMAP!!");
  36. }
  37. }
  38. }

第十步跳转到的函数:saveBitmap(Bitmap bitmap, String name, int quality),这里会产生一个message,然后通过handlemessage来处理这个message

  1. /**
  2. * This method posts a Bitmap with meta-data to the Handler queue.
  3. *
  4. * @param bitmap the bitmap to save
  5. * @param name the name of the file
  6. * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
  7. */
  8. public void saveBitmap(Bitmap bitmap, String name, int quality) {
  9. //获取一个message对象,然后把bitmap的信息存储到这个message中
  10. //将这个message发出去,发送到looper,然后这个message会被handleMessage接收
           //这里没有直接存储,而使用message,是想要用到looper,使用looper的好处是:可以短时间内放10个左右的截图
  1. Message message = this.obtainMessage();
  2. message.arg1 = quality;
  3. message.obj = bitmap;
  4. message.getData().putString("name", name);
  5. this.sendMessage(message);
  6. }

具体的handleMessage函数如下所示,也是位于这个screenShotSaver的类中:

  1. /**
  2. * Here we process the Handler queue and save the bitmaps.
  3. *
  4. * @param message A Message containing the bitmap to save, and some metadata.
  5. */
  6. public void handleMessage(Message message) {
  7. //复写Handler的handleMessage方法,然后获取到message对象,之后调用saveFile方法方法保存bitmap对象
  8. String name = message.getData().getString("name");
  9. int quality = message.arg1;
  10. Bitmap b = (Bitmap)message.obj;
  11. if(b != null) {
  12. saveFile(name, b, quality);
  13. b.recycle();
  14. }
  15. else {
  16. Log.d(LOG_TAG, "NULL BITMAP!!");
  17. }
  18. }

接下来就到了saveFile的函数中:

  1. /**
  2. * Saves a file.
  3. *
  4. * @param name the name of the file
  5. * @param b the bitmap to save
  6. * @param quality the compression rate. From 0 (compress for lowest size) to 100 (compress for maximum quality).
  7. *
  8. */
  9. private void saveFile(String name, Bitmap b, int quality){
  10. //构造一个File输出流,写bitmap对象到sd卡
  11. FileOutputStream fos = null;
  12. String fileName = getFileName(name);
  13.        //
  14. File directory = new File(Environment.getExternalStorageDirectory() + "/Robotium-Screenshots/");
  15. directory.mkdir();
  16.  
  17. File fileToSave = new File(directory,fileName);
  18. try {
  19. //初始化一个File的输入输出类,用以进行file的存储,之后调用compress方法写入
  20. fos = new FileOutputStream(fileToSave);
  21. if (b.compress(Bitmap.CompressFormat.JPEG, quality, fos) == false)
  22. Log.d(LOG_TAG, "Compress/Write failed");
  23. fos.flush();
  24. fos.close();
  25. } catch (Exception e) {
  26. 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.");
  27. e.printStackTrace();
  28. }
  29. }

金阳光测试

新浪微博:金阳光woody

         

          网站地址

1、百度搜:金阳光测试

2、官网:www.goldensunshine.cc

微信公众号

截图原理(一)——Android自动化测试学习历程的更多相关文章

  1. Robotium原理初步--Android自动化测试学习历程

    章节:自动化基础篇——Robotium原理初步(第四讲) 主要讲解内容与笔记: 一.基于控件 1.spinner——下拉菜单 2.TabHost——左右滑动选择菜单,类似电话本 3.Gallery—— ...

  2. Appium原理初步--Android自动化测试学习历程

    章节:自动化基础篇——Appium原理初步(第七讲) 本期关键词: Appium.跨语言跨平台.Bootstrap 主要讲解内容及笔记: 一.what is appium 一种封装了uiautomat ...

  3. Selenium原理初步--Android自动化测试学习历程

    章节:自动化基础篇——Selenium原理初步(第五讲) 注:其实所有的东西都是应该先去用,但是工具基本都一样,底层都是用的最基础的内容实现的,测试应该做的是: (1)熟练使用工具,了解各个工具的利弊 ...

  4. MonkeyRunner原理初步--Android自动化测试学习历程

    章节:自动化基础篇——MonkeyRunner原理初步 主要讲解内容及笔记: 一.理论知识和脚本演示 最佳方式是上官网文档去查看monkeyrunner的介绍,官网上不去,就找了一个本地的androi ...

  5. AndroidDriver原理初步--Android自动化测试学习历程

    章节:自动化基础篇——AndroidDriver原理初步(第六讲) 主要讲解内容及笔记: 一.AndroidDriver核心原理 对上图的解析: PC端的端口通过adb,将android版的Remot ...

  6. 截图原理(二)——android自动化测试学习历程

    接上一篇(截图原理) 视频地址:http://study.163.com/course/courseLearn.htm?courseId=712011#/learn/video?lessonId=87 ...

  7. 百度Cafe原理--Android自动化测试学习历程

    主要讲解内容及笔记: 一.Cafe原理 Cafe是一款自动化测试框架,解决问题:跨进程测试.快速深度测试 官网:http://baiduqa.github.io/Cafe/ Cafe provides ...

  8. Monkey原理初步和改良优化--Android自动化测试学习历程

    章节:自动化基础篇——Monkey原理初步和改良优化(第三讲) 主要讲解内容与笔记: 一.理论知识: 直接看文档,来了解monkey的概念.基本原理,以及如何使用. First,what is And ...

  9. 自动化预备知识上&&下--Android自动化测试学习历程

    章节:自动化基础篇——自动化预备知识上&&下 主要讲解内容及笔记: 一.需要具备的能力: 测试一年,编程一年,熟悉并掌握业界自动化测试工具(monkey--压力测试.monkeyrun ...

随机推荐

  1. vue:vue引入组建的多种写法

    vue的路由组件中,引入模块的两种写法:(@等价于..)死的写法:不是按需加载1:import Index from '@/components/Index'(import Index from '. ...

  2. CTags配好后仍找不到函数定义的问题

    若把CTags的Setting-User配好后,Navigation to Defination一个类或者函数发现仍无法跳转时,可以把需要查找的文件夹拉进sublime任一窗口中再试试. 因为CTag ...

  3. Java相关文章

    servlet/filter/listener/interceptor区别与联系 web.xml配置详解 在Eclipse中使用JUnit4进行单元测试(初级篇) 单点登录原理与简单实现 spring ...

  4. linux 内核假死循环导致的问题

    [, comm: -IFileSender Tainted: G B ENX -- ZTE Grantley/S1008 [:[<ffffffff810fb2cb>] [<fffff ...

  5. Pronunciation Guide for 25 Common Fruits

    Pronunciation Guide for 25 Common Fruits Share Tweet Share Tagged With: Vocabulary Words Know how to ...

  6. linq partion by 用法

    var PartinoByList = list.OrderBy(x => x.DateType).GroupBy(x => new { x.ProductCatagoryId, x.Su ...

  7. jquery 初始化数据 添加html 第一次玩0.0

    /** * Created by Eee_xiang on 2018/04/12. * 联动响应事件 */ (function () { $.linkEvent = function (options ...

  8. 原生js上传文件,使用new FormData()

    当创建一个内容较多的表单,表单里面又有了文件上传,文件上传也需要表单提交,单一的上传文件很好操作: <form action="接口" enctype="multi ...

  9. Delphi在Listview中加入Edit控件

    原帖 : http://www.cnblogs.com/hssbsw/archive/2012/06/03/2533092.html Listview是一个非常有用的控件,我们常常将大量的数据(如数据 ...

  10. 吴裕雄 python深度学习与实践(5)

    import numpy as np data = np.mat([[1,200,105,3,False], [2,165,80,2,False], [3,184.5,120,2,False], [4 ...