把两节的内容汇总起来,第一节讲的是如何在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

微信公众号

最新文章

  1. 重复安装相同包名APK出现的问题。
  2. 套题整理 Orz DXY
  3. lightoj1038
  4. 日志组件logback的介绍及配置使用方法
  5. POJ 1436 (线段树 区间染色) Horizontally Visible Segments
  6. 记第一次web前端校招笔试
  7. Android的事件处理
  8. android中常用的尺寸单位及其关系
  9. 关于ASP.NET Web Api的HelpPage文档注释问题
  10. NWERC2016-Problem A(Arranging Hat)
  11. Log4j 2翻译 Garbage-free Steady State Logging(稳定的以不会生成垃圾的状态来记录日志)
  12. SpringCloud系列——SSO 单点登录
  13. erlang二进制
  14. (set)MG loves gold hdu6019
  15. java基础应用循环的应用
  16. SQL Server 2008 报表服务入门【转】
  17. Loadrunner回放脚本时报错Action.c(41): Error -27979: Requested form not found [MsgId: MERR-27979]
  18. Linux服务器redhat配置本地yum源
  19. Sublime Text 2 快捷键(转)
  20. Mysql中的条件语句if、case

热门文章

  1. ArrayList、LinkedList、Vector的区别。
  2. iOS 视图间的几种通信方式
  3. mongo aggregate 用法记录
  4. ubuntu安装pgAdmin 4
  5. javascript:控制一个元素高度始终等于浏览器高度
  6. spark Kryo serialization failed: Buffer overflow 错误
  7. Reduction: the word AT
  8. 二叉堆复习(包括d堆)
  9. PL/SQL编码规范的一些建议
  10. server安装