uiautomater自动java 线程挂起和唤醒唤醒

24490人阅读
自动化测试
一、一个BUG引发的问题
& & 如果研发过程中有一个BUG:“不断的切换手机语言出现花屏现象”。这个问题我们如何验证呢?我想,最好的方式应该是自动化测试。
& & 那么,自动化测试可以完成哪些任务呢?
& & 简单的说,那些重复性的测试工作,都可以交给自动化完成:
& & & & 1、设置手机的语言
& & & & 2、添加、删除、收藏联系人
& & & & 3、拨号、挂断
& & & & 4、甚至发送短信、收藏短信
& & 如果需要上面的功能,那么就开始自动化之旅吧。
二、Android自动化测试简单介绍
& & Android自动化测试主要分为Monkeyrunner、Rubotium、UiAutomator、Monkey(在我看来这个不算)等。主要特点:
& & 1、Monkeyrunner:优点:操作最为简单,可以录制测试脚本,可视化操作;缺点:主要生成坐标的自动化操作,移植性不强,功能最为局限;
& & 2、Rubotium:主要针对某一个APK进行自动化测试,APK可以有源码,也可以没有源码,功能强大;缺点是针对APK操作,而且需要对APK重新签名(有工具),因此操作相对复杂;
& & 3、UiAutomator:优点:可以对所有操作进行自动化,操作简单;缺点:Android版本需要高于4.0,无法根据控件ID操作,相对来说功能较为局限,但也够用了;
& & 4、Monkey:准确来说,这不算是自动化测试,因为其只能产生随机的事件,无法按照既定的步骤操作;
& & 由上面介绍可以有这样的结论:测试某个APK,可以选择Rubotium;测试过程可能涉及多个APK,选择UiAutomator;一些简单的测试,选择Monkeyrunner;
& & 本文主要介绍UiAutomator的使用方法。
三、环境搭建
3.1、必备条件:
& & 1、JDK
& & 2、SDK(API高于15)
& & 3、Eclipse(安装ADT插件)
& & 4、ANT(用于编译生成jar)
3.2、简要步骤:
& & 1、安装JDK并添加环境变量。
& & & & 安装后,一定要通过JAVA_HOME的方式添加环境变量,即先建立JAVA_HOME变量,然后在path中添加%JAVA_HOME%\
& & 2、添加SDK环境变量。
& & & & 一定要先建立ANDROID_HOME,然后把%ANDROID_HOME%\tools添加到path中;
& & 3、安装Eclipse,并安装ADT插件。
& & 4、安装ANT工具,并添加环境变量。
& & & & 同样一定要先建立%ANT_HOME%变量,然后在path中添加%ANT_HOME%\bin
四、详细操作
4.1、建立工程
& & 用Eclipse新建Java Project,注意,不是Android Project!
4.2、添加JUnit库
4.3、添加Android库
& & 找到路径Android-sdk\platforms\android-17\下面的android.jar和uiautomator.jar添加进来:
& & 所有库添加完应该是这个样子:
4.4、在src中添加包,然后添加class文件
& & 文件内容为:
import com.android.uiautomator.core.UiO
import com.android.uiautomator.core.UiObjectNotFoundE
import com.android.uiautomator.core.UiS
import com.android.uiautomator.core.UiS
import com.android.uiautomator.testrunner.UiAutomatorTestC
public class Runner extends UiAutomatorTestCase {
public void testDemo() throws UiObjectNotFoundException {
getUiDevice().pressHome();
// 进入设置菜单
UiObject settingApp = new UiObject(new UiSelector().text(&Settings&));
settingApp.click();
Thread.sleep(3000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
// 进入语言和输入法设置
UiScrollable settingItems = new UiScrollable( new UiSelector().scrollable(true));
UiObject languageAndInputItem = settingItems.getChildByText(
new UiSelector().text(&Language & input&), &Language & input&, true);
languageAndInputItem.clickAndWaitForNewWindow();
& & 上面工程路径在e:\workspace\AutoRunner,类全名为com.Runner,至于具体的作用我们现在不去关心。
4.5、找到SDK ID
& & CMD进入\Android-sdk\tools\目录下,运行命令:
& & android list
& & 查看API大于15的SDK的ID值,当前是6;
4.6、创建build文件
& & 仍然在\Android-sdk\tools\目录下,运行命令:
& & android create uitest-project -n &name& -t &android-sdk-ID& -p &path&
& & 比如:
& & android create uitest-project -n AutoRunner -t 6 -p e:\workspace\AutoRunner
& & 上面的name就是将来生成的jar包的名字,可以自己定义,android-sdk-ID就是上面看到的6;path是Eclipse新建的工程的路径;运行命令后,将会在工程的根目录下生成build.xml文件。如果没生成,检查上面的步骤。
4.7、编译生成jar
& & CMD进入项目的工程目录,然后运行ant build,将使用ant编译生成jar,成功将会提示:
& & 然后会在bin目录下生成jar文件。
4.8、push并运行jar
& & adb push &jar文件路径& data/local/tmp
& & adb shell uiautomator runtest &jar文件名& -c &工程中的类名,包含包名&
& & 比如:
& & adb push e:\workspace\AutoRunner\bin\AutoRunner.jar data/local/tmp
& & adb shell uiautomator runtest AutoRunner.jar -c com.Runner
& & 然后就能看到手机会按照Runner中的步骤自动执行。具体效果就是,进入设置菜单,然后再进入“语言和输入法”菜单
五、代码分析
& & 我们从几个最重要的对象来介绍。
5.1、UiDevice对象
& & getUiDevice()的方法可以得到一个UiDevice的对象,通过这个对象可以完成一些针对设备的动作:
& & click(int x, int y)
& & ----在(x,y)表示的像素地方点击
& & pressBack()
& & pressDelete()
& & pressEnter()
& & pressHome()
& & pressMenu()
& & pressSearch()
& & ----点击相应的按键
& & wakeUp()
& & ----当手机处于灭屏状态时,唤醒屏幕,并解锁。
& & swipe(startX, startY, endX, endY, steps)
& & ----在手机上滑动,从(startX,startY)到(endX,endY)。steps表示滑动的这个距离分为几步完成,数目越少,滑动幅度越大。
& & setOrientationLeft()
& & setOrientationRight()
& & ----将手机向相应方向旋转。
& & setOrientationNatural()
& & ----将手机旋转状态回归正常。
5.2、UiSelector对象
& & 这个对象可以理解为一种条件对象,描述的是一种条件,经常配合UiObject使用,可以得到某个(某些)符合条件的控件对象。
& & checked(boolean val)
& & ----描述一种check状态为val的关系。
& & className(className)
& & ----描述一种类名为className的对象关系
& & clickable(boolean val)
& & ----与checked类似,描述clickable状态为val的关系
& & description(desc)
& & ----不解释
& & descriptionContains(desc)
& & ----与description类似
& & focusable(boolean val)
& & ----与checked类似
& & index(index)
& & ----用当前对象在父对象集中的索引作为描述
& & packageName(String name)
& & ----用包名作为条件描述
& & selected(val)
& & ----描述一种选择关系
& & text(text)
& & ----最为常用的一种关系,用控件上的文本即可找到当前控件,需要注意,所有使用text属性找到的控件,必须是英文的。也就是说,不支持通过中文查找控件!
& & textContains(text)
& & ----与text类似
& & textStartsWith(text)
& & ----与text类似
5.3、UiObject对象
& & 这个对象可以理解为控件的对象。 一般一个UiObject对象可以通过一下形式得到:
& & UiObject mItem = new UiObject(new UiSelector().text(&English&));
& & 也就是配合一个UiSelector就可以得到一个控件。
& & click()
& & ----点击控件
& & clickAndWaitForNewWindow()
& & ----点击某个控件,并等待窗口刷新
& & longClick()
& & ----长按
& & clearTextField()
& & ----清除文本,主要针对编辑框
& & getChildCount()
& & ----这个方法可以看出,其实UiObject也可以是一个控件的集合
& & getPackageName()
& & ----得到控件的包名
& & getSelector()
& & ----得到当前控件的选择条件
& & getText()
& & ----得到控件上的Text
& & isCheckable()
& & isChecked()
& & isClickable()
& & isLongClickable()
& & isScrollable()
& & isScrollable()
& & isSelected()
& & ----判断是否具备某个属性
5.4、UiCollection对象
& & 这个对象可以理解为一个对象的集合。因为UiSelector描述后得到的有可能是多个满足条件的控件集合,因此可以用来生成UiCollection:
& & UiCollection mUiCollection = new UiCollection(new UiSelector().text(&Settings&));
& & getChild(selector)
& & ----从集合中再次通过UiSelector选择一个UiObject对象
& & getChildByDescription(childPattern, text)
& & ----从一个匹配模式中再次以text为条件选择UiObject
& & getChildByText(childPattern, text)
& & ----与上面类似。
& & getChildCount()
& & ----得到当前集合中控件的个数
5.5、UiScrollable对象
& & UiScrollable可以生成一个滚动动作的对象,其最大的作用就是可以实现滚动的查找某个元素。比如在“设置”菜单中,“语言和输入法”这个菜单比较靠下,需要滚动后才可以看到(找到),因此就用上了UiScrollable:
UiScrollable settingItems = new UiScrollable( new UiSelector().scrollable(true));
UiObject languageAndInputItem = settingItems.getChildByText(
new UiSelector().text(&Language & input&), &Language & input&,
& & 上面的形式就可以在滚动中查找显示有“Language & input”的控件,也就是“语言和输入法”的设置项。
5.6、等待操作和添加Log的方法
& & 如果是对于一个标准的UiObject对象,可以通过clickAndWaitForNewWindow的方法在点击之后主动等待一段事件,但是如果需要额外的等待一段时间,特别对于getUiDevice().pressHome();这种操作,可能需要很长的事件去为下一步操作获取更多的事件,此时我们可以使用线程的sleep方法去实现:
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
& & 而添加Log的方法也可以通过Java标准的println来实现:
& & System.out.println(&This used to print some log!!!& + setLanItem.getText());
& & 以上Log将会在jar被运行时通过CMD窗口打印出来。
六、一个相对完整的测试case
& & 下面就用一个相对连贯的测试用例来串一下上面的知识点,这个case用例要做的就是进入系统设置菜单,然后选择“语言和输入法”菜单,然后进入“语言设置”菜单,然后在第一项上点击,把当前语言设置为“简体中文”:
public void setChineseLan() throws UiObjectNotFoundException {
& & //进入操作前,先用Home键进入待机界面
& & getUiDevice().pressHome();
& & //进入“系统设置”菜单。也可以通过点击menu按键来实现
& & UiObject settingApp = new UiObject(new UiSelector().text(&Settings&));
& & settingApp.click();
& & //等待3秒
& & & & Thread.sleep(3000);
& & } catch (InterruptedException e1) {
& & & & e1.printStackTrace();
& & //用滚动的方式查找并进入“语言和输入法设置”菜单
& & UiScrollable settingItems = new UiScrollable(
& & & & & & new UiSelector().scrollable(true));
& & UiObject languageAndInputItem = settingItems.getChildByText(
& & & & & & new UiSelector().text(&Language & input&), &Language & input&,
& & & & & & true);
& & languageAndInputItem.clickAndWaitForNewWindow();
& & //找到“English”的可点击项(因为当前是英文环境)
& & UiObject setLanItem = new UiObject(new UiSelector().text(&English&));
& & setLanItem.clickAndWaitForNewWindow();
& & //Log输出
& & System.out.println(&setLanItem--&& + setLanItem.getPackageName());
& & //由于无法识别中文,因此我们这里使用坐标去选择“简体中文”项
& & getUiDevice().click(350, 250);
& & //点击返回键,回到待机界面
& & getUiDevice().pressBack();
& & getUiDevice().pressBack();
& & getUiDevice().pressBack();
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:201508次
积分:2620
积分:2620
排名:第11252名
原创:56篇
评论:83条
(1)(5)(1)(4)(5)(5)(3)(1)(8)(5)(2)(3)(1)(1)(2)(4)(1)(3)(1)Android测试支持库包含 UI自动化模块 ,它可以对Android应用进行自动黑盒测试。在API Level 18中引入了自动化模块,它允许开发者在组成应用UI的控件上模仿用户行为。
在这个教程中,我将展示如何使用此模块来创建和执行一个基本的UI测试,选择默认的计算器模块进行测试。
在使用前,需要具备以下条件:
最新版本的 Android Studio
运行Android 4.3或者更高版本的设备或者虚拟器
理解 JUnit
1. 安装依赖库
工程中使用UI自动化模块,需要编辑你的工程下 app 目录下的文件 build.gradle ,添加如下信任:
androidTestCompile 'com.android.support.test:runner:0.2'
androidTestCompile 'com.android.support.test:rules:0.2'
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.0'
现在屏幕上应该有 Sync Now 按钮了,但点击它时,会看到如下错误信息:
点击 Install Repository and sync project 链接来安装 Android Support Repository。
如果使用的是库 appcompat-v7 且其版本号是 22.1.1 ,你需要添加如下依赖以确保应用本身和测试应用都使用相同版本的com.android.support:support-annotations:
androidTestCompile 'com.android.support:support-annotations:22.1.1'
接下来,由于Android Studio自身的一个bug,你需要通过packagingOptions执行一个名为 LICENSE.txt 的文件。这个执行失败的话,在运行测试时将引起如下错误:
Execution failed for task ':app:packageDebugAndroidTest'.
Duplicate files copied in APK LICENSE.txt
File 1: ~/.gradle/caches/modules-2/files-2.1/org.hamcrest/hamcrest-core/1.1/678d1a754e259cdb14/hamcrest-core-1.1.jar
File 2: ~/.gradle/caches/modules-2/files-2.1/junit/junit-dep/4.10/64417b3bafdecd366afa514bd5beeae6c1f85ece/junit-dep-4.10.jar
在你的 build.gradle 文件底部增加如下代码段:
packagingOptions {
exclude 'LICENSE.txt'
2、创建测试类
创建一个新的测试类,CalculatorTester,通过在 androidTest 目录下创建名为 CalculatorTester.java 的文件实现。创建的UI自动化测试用例,必须继承自InstrumentationTestCase。
按 Alt+Insert后选择 SetUp Method 来重写setUp方法。
再次按 Alt+Insert 后选择 Test Method 来生成新的测试方法,命名为testAdd。到此CalculatorTester类定义如下:
public class CalculatorTester extends InstrumentationTestCase{
public void setUp() throws Exception {
public void testAdd() throws Exception {
3、查看Launcher UI
连接你的Android设备到电脑商,点击home按键,进入主界面。
返回到你的电脑,使用文件管理或者终端浏览你安装Android SDK的目录,进入到 tools 目录下,点击 uiautomatorviewer 。这个会启动 UI Automater Viewer ,你将看到如下界面:
点击上方手机图标来获取Android设备截屏。注意到此时获取到的截屏是可交互的。点击下面的Apps图标。在右方的 Node Detail 区域,你就可以看到根据选择图标的不同显示不同的详细信息,如下图所示:
与屏幕上的应用交互,UI自动化测试需要能唯一识别它们。在这个教程中,可以使用应用的text、content-desc或者class字段来唯一的区分。
从上图可以看到Apps图标没有text字段,但有content-desc。记下它的值,后面将用到这个值。
拿起Android设备,触摸Apps图标,进入设备安装的所有应用界面。使用 UI Automater Viewe 获取另外一张屏幕截图。因为要写一个计算器应用的测试,点击计算器图标查看详细界面。
这次content-desc是空的,但是text的值为Calculator,同样记住这个值。
如果你的Android设备运行不同的主界面或者不同的Android版本,界面和显示的细节会有所不同。这意味着后续代码中需要做一些修改,以匹配你的操作系统。
4、准备测试环境
返回到Android Studio,给setUp方法中添加代码。如同其名字,setUp方法是用来准备测试环境的。换句话说,这个方法是在真正测试之前指定具体需要执行什么动作的。
现在需要写代码来模拟刚才在Android设备上执行的几个动作:
1、按home键进入主界面
2、按Apps图标进入应用界面
3、点击计算器图标启动它
在你的类中声明类型为UiDevice的变量device。它代表你的Android设备,后续使用它来模拟用户行为。
private UiD
在setUp方法中,通过调用UiDevice.getInstance method来初始化device,传递Instrumentation实例,如下所示:
device = UiDevice.getInstance(getInstrumentation());
模拟点击设备home键,需要调用pressHome方法。
device.pressHome();
接下来,需要模拟点击Apps图标的动作。不能立即做这个动作,因为Android设备需要一个反应时间来加载界面。如果在屏幕显示出来之前执行这个动作就会引起运行时异常。
等待一些事情发生时,需要调用UiDevice实例的wait方法。等待Apps图标显示到屏幕,使用Until.hasObject方法。
识别Apps图标需要使用By.desc方法并传递值为 Apps 的参数。你还需要指定最长等待时间,单位为毫秒。此处设置为3000。
至此形成如下代码段:
// Wait for the Apps icon to show up on the screen
device.wait(Until.hasObject(By.desc("Apps")), 3000);
要获取Apps图标的引用,需要使用findObject方法。一旦有了Apps图标的引用,就可以调用click方法来模拟点击动作了。
UiObject2 appsButton = device.findObject(By.desc("Apps"));
appsButton.click();
和前面一样,我们需要等待一些时间,保证计算器图标显示到屏幕上。在之前的步骤中,我们看到可以通过text字段唯一的识别计算器图标。我们调用By.text方法来找到图标,传递参数为Calculator。
// Wait for the Calculator icon to show up on the screen
device.wait(Until.hasObject(By.text("Calculator")), 3000);
5、检查计算器UI
在你的Android设备上启动计算器应用,使用 UI Automater Viewer 来查看显示。获取到一个截屏后,点击不同的按钮来观察使用何值可以唯一的区分它们。
在本次测试用例中,使用计算器计算 9+9= 的值并确认结果是否为 18 。这意味着你需要知道怎么区分按键 9 、 + 和 = 。
在我的设备上,如下是我收集到的信息:
数字按键匹配text值
+ 和 = 使用content-desc值,分别对应 plus 和 equals
返回值显示在EditText控件中
如果你使用不同版本的计算器应用,请注意这些值有可能不一样。
6、创建测试类
在前面几步操作中,你已经学会了使用findObject方法通过By.text或者By.desc来获取屏幕上不同对象的引用。还学会了通过click方法来模拟点击对象的动作。下面的代码使用这些方法来模拟 9+9= 。添加这些到类CalculatorTester的方法testAdd中。
// Wait till the Calculator's buttons are on the screen
device.wait(Until.hasObject(By.text("9")), 3000);
// Select the button for 9
UiObject2 buttonNine = device.findObject(By.text("9"));
buttonNine.click();
// Select the button for +
UiObject2 buttonPlus = device.findObject(By.desc("plus"));
buttonPlus.click();
// Press 9 again as we are calculating 9+9
buttonNine.click();
// Select the button for =
UiObject2 buttonEquals = device.findObject(By.desc("equals"));
buttonEquals.click();
现在就等待运行结果。此处不能使用Until.hasObject,因为包含计算结果的EditText已经显示在屏幕上了。取而代之,我们使用waitForIdle方法来等待计算完成。同样,最长等待时间是3000毫秒。
device.waitForIdle(3000);
使用findObject和By.clazz methods方法获取EditText对象的引用。一旦有了此引用,就可以调用getText方法来确定计算结果是否正确。
UiObject2 resultText = device.findObject(By.clazz("android.widget.EditText"));
String result = resultText.getText();
最后,使用assertTrue来检验范围值是否为 18 。
assertTrue(result.equals("18"));
测试到此结束。
6、执行测试
执行测试,需要在Android Studio的工具栏中选择CalculatorTester,点击它右方的 play 按钮。
一旦编译结束,测试就成功运行完整。当测试运行时,在你的Android设备上就会看到UI自动化运行界面。
在这篇教程中,我们学会了如何使用UI自动化测试模块和 UI Automater Viewer 来创建用户界面测试。你也看到了使用Android Studio执行测试是如此简单。虽然我们测试了一个相对简单的应用,但可以将从中学到的概念用到几乎所有Android应用的测试中。
在文章中找不到问题答案?您还可以
热门栏目订阅2428人阅读
转载博文(20)
预防Windows应用程序挂起
预防Windows应用程序挂起预防Windows应用程序挂起
受影响的平台
&&&&&& 客户端 &- Windows 7
&&&&&& 服务器端 &- Windows Server 2008 R2
挂起 – 从用户角度来看
用户喜欢及时反馈的应用程序。当用户点击一个菜单的时候,他们希望应用程序可以及时反馈,即便应用程序正在进行处理。当他们在自己最喜欢的文字处理工具中保存一篇长文档时,他们希望在磁盘正在旋转的时候仍然能够继续打字。当应用程序不能及时响应用户的输入的时候,用户很容易不耐烦。
程序员可能很容易找到一个应用程序不能及时的对用户的输入进行反馈的合理原因。应用程序可能正忙于重新计算一些数据,或者正在等待磁盘的 I/O 读写完成。但是,通过对用户进行的调查,我们发现用户对于一个没有响应的应用程序等上几秒钟就会变得厌烦,沮丧。 5 秒钟之后,用户将会试着结束这个挂起的应用程序。与崩溃类似,当调用 Win32 应用程序的时候,应用程序挂起是用户中断的最主要原因。
有许多的不同的原因可能引起应用程序的挂起,但并不是所有的情况都会在 UI 上体现出来。但是, UI 没有反映的情况多数是由于应用程序挂起,这种情况通常可以得到系统的支持与恢复。 Windows 自动检测,收集调试信息,并最优化的终止或重新挂起应用程序。除此之外,用户可能不得不重启机器来恢复挂起的应用程序。
挂起 – 从操作系统角度来看
当一个应用程序(或者更准确定的说,一个线程)在桌面上创建一个窗口时,它会执行一个桌面窗口管理工具( Desktop Window Manager , DWM )的隐式的协议来及时的处理窗口消息。 DWM 回发消息(键盘 / 鼠标的输入以及来自于其他窗口或窗口本身的消息)到一个特定线程的( thread-specific )消息队列。该线程将会通过它的消息队列获取并发送这些消息。如果线程没有通过调用 GetMessage() 来为队列服务,则消息将不会被处理,并且窗口会挂起:此时,既不能刷新屏幕也不能接受任何来自于用户的输入。系统会通过向消息队列中的等待消息(
pending messages )加入一个计时器来监测到这一状态。如果一条消息 5 秒内没有被获取,那么 DWM 会声明窗口处于挂起状态。您可以使用 IsHungAppWindow() API 来查询特殊窗口的状态。
监测只是第一步。这时,用户仍然不能终止该应用程序 – 点击 X (关闭)按钮可能会引起 WM_CLOSE 消息,它会像其他消息一样被放入消息队列中。 DWM 会无缝的隐藏被挂起的窗口,并在原始窗口的位置上把它替换成一幅图片(并在标题栏上加上“没有响应”)。只要原始窗口的线程没有获取消息, DWM 会同时管理两个窗口,但是只允许用户与替换的窗口进行交互。使用替换的窗口的时候,用户只可以移动,最小化以及——最重要的——关闭没有响应的应用程序,但是不能改变其内部的状态。
整个替换效果如图所示:&
DWM 做的最后一件事是:它集成 Windows 错误报告( Windows Error Reporting ),允许用户不仅可以关闭并最优化的重启应用程序,还能够发送有价值的调试信息给 Microsoft 。您可以到 Winqual 网站注册以获得关于您的应用程序的挂起数据。
Windows 7 为之添加了新特性。系统会分析挂起的应用程序,在特定情况下,并给用户一个选择来取消阻止操作并使应用程序重新可用。当前的实现支持对于阻止的 Socket 调用执行取消操作;更多的操作都将会在未来的版本中变成用户可以取消的。
要将您的应用程序与挂起恢复进行集成并从中获得有用的数据,请按以下步骤进行:
确定您的应用程序注册了重启和恢复,以尽量减少挂起给用户带来的痛苦。一个正确注册了的应用程序可以在大多数未保存的数据没有丢失的情况下自动重启。这一操作对挂起和崩溃都有效。 从 Winqual 网站上获得您的挂起或崩溃的应用程序的调试数据以及发生频率的信息。在您的应用处于 Beta 阶段的时候,您可以使用这一信息来改善您的代码。请参看“介绍 Windows 错误报告”。 您可以调用 DisableProcessWindowsGhosting () 来禁用应用程序中的替换特性。但是,这会使用户无法关闭或重启一个挂起的应用程序,并常常会使机器重启。
挂起 – 从开发者的角度来看
操作系统定义了应用程序的挂起为一个超过 5 秒钟没有处理消息 UI 线程。显然 bug 会引起挂起,例如,一个线程等待一个从没有信号的( signaled )事件,以及两个线程互锁的情况。您可以修复这些 bug 而无需做过多的工作。但是,许多挂起并不是十分清晰。事儿, UI 线程是没有获得消息 – 但是它同样在忙于执行其他“重要的”工作并且将在最终完成后回来处理消息。
然而,用户认为这是一个 bug 。设计上应该适应用户的期望。如果由于应用程序的设计导致了应用程序没有反映,则设计就应该进行改变。最后,也同样重要的,没有响应就像是代码的 bug 一样不能被修复;它需要在设计时预先做好工作。试图翻新改进一个应用程序现有的基础代码来使 UI 可以响应,通常需要花费许多。下面的设计准则可能对您有所帮助:
把 UI 的响应放在需求的首位;用户应该始终可以感到可以控制您的应用程序 确保用户可以取消长于 1 秒的操作,或者操作可以在后台完成;如果需要的话,提供适当的进程 UI
队列长时间运行或者阻止操作使其变成后台任务(这需要设计细致的消息系机制,在工作完成的时候可以通知 UI 线程) 使 UI 线程部分的代码尽量简单;尽量多地去除对阻塞 API ( blocking API )的调用 只有当窗口和对话框已准备好并且完全的可以使用的时候,才显示他们。如果对话框需要显示信息,将会消耗许多运算资源,首先应该显示一些一般信息,然后在运行中当数据可以使用时更新这些信息。很好的一个例子就是文件夹属性对话框。它需要显示文件夹的总容量,但文件系统还没有准备好提供相关信息。对话框即时显示容量字段并通过工作线程逐渐更新容量信息:
不幸的是,并没有简单的方法来设计和编写一个及时反馈的应用程序。 Windows 没有提供一种简单的异步框架,该框架考虑到了简单时序的阻塞( blocking )或长时间运行的操作。 下面的部分将介绍一些关于预防挂起的最佳实践,以及常见的易犯的错误。
使 UI 线程简单化
UI 线程的主要职责是获取并发送消息。该线程的任何其他的工作可能会带来窗口挂起的风险。
将会引起长时间运行操作的消耗过多或不受控制的算法移动到工作线程( worker threads ) 识别尽量多的阻塞( blocking )方法,并将它们移动到工作线程;任何调用其他 DLL 的方法都应该被视作可疑的 移除工作线程中所有文件 I/O 和网络 API 调用。这些方法可以造成几秒钟甚至几分钟的阻塞。如果您需要在 UI 线程中做任何 I/O 操作,请考虑使用异步 I/O 注意您的 UI 线程正在服务于单线程单元( STA ) COM 服务器;如果您进行了阻塞调用,这些 COM 服务器将会失去响应,直到您再次为消息队列提供服务服务。
等待任何内核对象(比如,事件或 Mutex )的时间,超过一个很小的时间;如果您不得不等待,请考虑使用 MsgWaitForMultipleObjects() ,该方法将会在一个新消息到来的时候解除阻止。 使用 AttachThreadInput() 方法将一个线程的窗口消息队列与另一个线程共享。正确的同步访问队列不仅非常困难,并还将阻止 Windows 操作系统监测挂起的窗口。 在您的任何工作线程上使用 TerminateThread() 。 以这种方式终止一个线程将不能释放锁或给事件发信号,并且很容易产生孤立的同步对象。
从您 UI 线程中调用一些“未知的”代码。如果您的应用程序包含可扩展的模块,这种情况很有可能发生;这里不鼓励让第三方的代码遵照您的响应准则 进行任何类型的阻塞广播调用( blocking broadcast call ) ; SendMessage(HWND_BROADCAST) 会使您对每一个正在运行的编写不正确的应用程序毫无办法。
实现异步模式
从 UI 线程中去除长时间运行或阻塞的操作,需要实现一个异步的框架,该框架允许卸载这些操作到工作线程。
在您的 UI 线程中使用异步窗口消息 APIs ,特别是用一个对等的且没有阻塞的方法代替 SendMessage ,例如: PostMessage, SendNotifyMessage, or SendMessageCallback 使用后台线程来执行长时间运行或阻塞的任务。使用新的线程池 API 来实现您的工作线程。 为长时间运行的后台任务提供取消支持。对于阻塞 I/O 的操作,使用 I/O 取消,但这只是最后才采用的解决办法;对“正确的”操作进行取消并不容易。 使用 IAsyncResult 模式或事件,来为托管代码实现一个异步的设计。
聪明的使用锁
您的应用程序或 DLL 需要锁来同步访问内部数据结构。使用多种锁来增加并行性并使您的应用程序响应更加及时。但是,使用多种锁还可以增加以不同的顺序获得锁的可能性,以及引起您的线程死锁。如果两个线程都有一个锁并且还都试图获得另一个线程的锁,那他们的操作将会形成一个循环等待,该等待将会阻塞进程向前运行。您只能通过确保应用程序中的所有线程都按照同样的顺序获取所有的锁来避免这个死锁。但是,以“正确的”顺序获得锁并不是一件容易的事情。软件组件可以被构成( be composed ),但是锁获得( lock acquisitions
)不能。如果您的代码调用了一些其他的组件,这些组件的锁现在又变成了您的隐含的锁顺序( implicit lock order )的一部分 – 即便这些锁对您来讲是不可见的
由于锁操作包含比通常情况更多的方法——它们作用于关键部分( Critical Sections ), Mutexes , 以及其他传统的锁,这使得事情变得更加复杂了。任何跨线程边界的阻塞调用都有同步属性,这可能引起死锁。调用的线程执行一个包含“获取( acquire )”语义的操作,并不能解除阻塞,直到目标线程“释放”那调用。一少部分 User32 方法(例如 SendMessage ),以及很多阻塞 COM 调用都属于这一类。
更糟糕的是,操作系统有自己的内部的进程特殊( process-specific )的锁,在代码执行的时候会被锁住。当 DLLs 被加载到进程的时候获得了锁,因此被叫做“加载者锁”( loader lock )。 DllMain 方法总是在加载者锁的情况下执行的;如果您在 DllMain (您应该不能)中获取任何锁,您需要让加载者锁成为您锁顺序的一部分。调用特定的 Win32 APIs 也可能获得加载者锁——例如这些方法, LoadLibraryEx , GetModuleHandle 特别是 CoCreateInstance
为了更好的说明,请看下面的示例代码。这个方法获取了多个同步对象并隐式的定义了锁顺序,很明显有些地方并不是必要的。在方法的入口处,代码获取了一个关键部分( Critical Section )并且没有释放直到方法结束,因此使得他成为了锁层级结构中的顶节点。代码随后调用了 Win32 方法 LoadIcon() ,它将可能调用操作系统加载器以加载二进制代码。这一操作可能获得加载者锁,它也成为了锁层级结构的一部分(确定 DllMain 方法没有获得 g_cs 锁)。然后代码调用了 SendMessage()
,一个阻塞跨线程的操作,它将不会返回除非 UI 线程进行相应。在此确认 UI 线程从来没有获得 g_cs 。
bool foo::bar (char* buffer)
&&&&&&EnterCriticalSection(&g_cs);
&&&&&&// Get 'new data'icon
&&&&&&this.m_Icon= LoadIcon(hInst,MAKEINTRESOURCE(5));
&&&&&&// Let UI thread knowto update icon SendMessage(hWnd,WM_COMMAND,IDM_ICON,NULL);
&&&&&&this.m_Params= GetParams(buffer);
&&&&&&LeaveCriticalSection(&g_cs);
再看这段代码,可以清晰的看出,我们隐式的把 g_cs 放到了我们锁层级结构的顶层,即便我们只想同步访问类的成员变量。
设计一个锁层级结构并遵循它。添加所有必要的锁。还有很多像 Mutex 和 CriticalSections 的同步原语( synchronization primitives );它们都需要被包含。如果您需要在 DllMain() 中获得任何锁,请在锁层级结构中包含加载者锁。 与您的依赖在锁协议上达成一致。任何您的应用程序所调用的或者是调用您的应用程序的代码,都需要共享同样的锁层级结构。 锁数据结构而不是方法。把锁获取( lock acquisitions )从方法的入口点移走并只用锁来保护数据访问。如果减少代码操作,则死锁的几率也会降低。
分析您的错误处理中锁获取( lock acquisitions )和释放。当试图从错误状态中恢复时常常忽略锁的层次结构 Often the lock hierarchy if forgotten when trying to recover from an error condition 将嵌套的锁替换为引用计数器——它们不会死锁。列表中被独立锁住的元素和表都是不错的选择。 小心,当等待一个 DLL 的线程处理的时候。假设您的代码可以在有加载者锁的情况下被调用。最好利用引用计数的方式记录您的资源,并用工作线程来做自我清理(然后使用
FreeLibraryAndExitThread 终止清理)。 使用 Wait Chain Traversal API ,如果您想要分析您应用程序的死锁的话。
在 DllMain() 中做任何事情,除了非常简单的初始化工作。查看 DllMain 回调方法已获得更多信息。特别是不用调用 LoadLibraryEx 或 CoCreateInstance 。 写您自己的锁原语( primitives )。自定义同步代码会很容易在您的基础代码中产生细微的 bug 。可以使用操作系统同步对象中丰富的选项代替。 在构造函数和析构函数中对全局变量做任何事情,它们是在有加载者锁的情况下被执行的。
小心异常处理
异常处理允许将正常的项目流和错误处理进行分离。由于这种分离,可能很难知道对于异常优先的项目精确的状态,并且异常处理可能会错过处理有效状态的要紧步骤。 对于锁的获得( lock acquisitions )更是如此,它需要在处理程序中被释放掉,以预防未来可能出现的死锁。
下面的示例代码就说明了这个问题。不受控制的对“缓存”变量进行访问,将会偶尔引起访问冲突。访问冲突是由本机的异常处理引起的,但是如果在出现异常的时候关键部分已经被获得的话,并没有很好的方法来确定。(访问冲突可能已经在 EnterCriticalSection 代码中的某处发生了)
BOOL bar (char* buffer)
&&&BOOL rc =FALSE;
&&&__try {
&&&&&&EnterCriticalSection(&cs);
&&&&&&while (*buffer++ !='&') ;
&&&&&&rc= GetParams(buffer);
&&&&&&LeaveCriticalSection(&cs);
&&&} __except (EXCEPTION_EXECUTE_HANDLER)
&&&&&&return FALSE;
在任何可能的时候,去除 __try/__except ;不要使用 SetUnhandledExceptionFilter 如果您使用 C++ 进行异常处理,请把您的锁封装到自定义的 auto_ptr 式的模板( custom auto_ptr-like templates )。锁应该在析构函数中被释放。对于本机异常来说,应该在您的 __finally 代码段中进行释放。 小心在本机异常处理中执行的代码;异常可能会泄露很多锁,所以处理程序不应该获取
处理本地异常,如果不必要或 Win32 APIs 请求的话。如果您使用本机异常处理来在灾难性错误之后进行报告( reporting )或数据恢复,请考虑使用 Windows 错误报告的默认操作系统机制来代替。 与任何类型的 UI ( user32 )代码一起使用 C++ 异常处理;在回调中被抛出的异常将会穿过系统提供的 C 代码层。这些代码无法识别 C++ 展开语义( unroll semantics )
& 介绍 Windows 错误报告 &:& 异步设计 :&
异步 I/O:&AttachThreadInput():&auto_ptr:&DisableProcessWindowsGhosting
():&DllMain 回调 Function:&
事件 :&GetMessage():&I/O
取消 :&IsHungAppWindow():&
消息队列 :&MsgWaitForMultipleObjects():&
新建线程池 API:&PostMessage:&
重启与恢复 :&SendMessageCallback:&SendNotifyMessage:&
同步对象 :&TerminateThread():&Windows
错误报告 :&Winqual:&
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:86533次
排名:千里之外
转载:25篇
评论:10条
(1)(1)(2)(1)(1)(1)(1)(2)(1)(1)(5)(3)(2)(5)}

我要回帖

更多关于 linux线程挂起与唤醒 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信