简介

为系统挂起与恢复而进行的应用准备步骤

曾几何时,当您正要通过应用提交或发布一些重要数据时,突然遇到一些急事需要处理,而且会耽误很长时间。当您完成任务回到电脑前时,发现电脑已经自动进入 了挂起状态,或是完全关机。您可能因此丢失了部分或全部重要数据,而这仅仅是因为应用没能在停止执行前“保存”数据。相信拥有类似经历的人不在少数。现 在,应用开发人员设计出了一种专门的应用,来帮助我们避免发生这种情况。该应用可在系统挂起或休眠之前,通过操作系统发送相应消息与事件通知用户。

本文通过案例描述了 Windows* 环境下系统准备挂起时的状态,以及挂起操作和从挂起状态恢复操作。我们将对消息和事件的发送目的进行讲解,提供一些有关用法与计时方面的信息,并给出一些 高效使用建议,助您避免潜在的数据丢失。此外,我们还会深入探讨应该通过何种类型的应用来实现对此类消息与事件的收发。我们将重点研究 Windows XP* Professional 操作系统上的消息传递,所提供的代码实例将用来讲解如何准备和从挂起状态恢复。此外,本文还提出一些建议,帮您避免挂起或是绕过当前的种种局限。本文的讨 论内容面向对 Windows 消息收发有一定认识基础的读者。


定义

WM_POWERBROADCAST – 通过 WindowProc() 函数(告知电源管理事件已经/正在运行)向应用广播的消息。

PBT_APMQUERYSUSPEND – 需要经许可才能挂起的事件

PBT_APMQUERYSUSPENDFAILED – 通知挂起请求失败的事件

PBT_APMRESUMEAUTOMATIC – 自动从挂起状态恢复时所收到的事件

PBT_APMRESUMECRITICAL – 通知从未知或不稳定状态恢复的事件

PBT_APMRESUMESUSPEND – 通知从挂起状态恢复的事件

PBT_APMSUSPEND – 系统挂起前收到的事件


推动因素

应用程序的开发是一项技巧性很强的工作。随着移动环境的到来,这种情况也会变得日益凸显。首先,我们 需要确定哪些应用可能会因系统挂起而丢失数据,哪些应用则不会。需要考虑的应用包括:任何会受到操作系统环境变化影响的应用,如网络连接、USB 连接或 Firewire* 连接,任何操作完成前不能中断的应用以及任何无需用户交互就能在显示器上显示信息的应用。

假如应用正在使用网络中的某个文件时系统进入挂起状态,将会出现何种情况?此时,即使执行了恢复操作,之前的网络连接也不再可用,除非应用能预先“意识” 到您将遭遇困境。如果情况发生在通过 Firewire 或 USB 连接刻录 CD/DVD 盘片时,您将如何应对?如果真是这样,那么很不幸,您的盘片只能当杯垫用了。假如您在向一位重要客户进行演示,中途停下来进行讲解或问答时却出现系统挂 起、显示中止的尴尬局面?这时,您不得不花费宝贵的时间来重启电脑,打开演示并找到此前位置,继续进行演示。由此可见,防止应用挂起是多么的重要。

众多事例都证明了一个观点:开发一种依靠系统消息来防范挂起的应用是很有必要的。当系统无法从挂起状态恢复、需要重新启动时,能够对电源消息做出回应的应 用将显得更为灵活和可靠。通过创建备份文件,您可更轻松地检索挂起前存储的数据,更省力实现挂起恢复。


系统挂起

系统挂起进程主要用于节省移动设备的能耗或在移动某一设备前关闭硬盘。该进程一般会关联到用户设置, 用来关闭特定时间内未被使用的设备。如果电源设置适当,还可通过关闭笔记本电脑顶盖来调用系统挂起进程。为了展示与系统挂起相关的消息模式,我们以远程控 制方式连接到了测试系统,并在测试系统上调用 Spy++,用来检查和捕捉由测试系统广播的 WM_POWERBROADCAST 消息和事件。通常,应用收发的消息模式如下:

S WM_POWERBROADCAST (long)dwPowerEvent:PBT_APMQUERYSUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001] SWM_POWERBROADCAST (long)dwPowerEvent:PBT_APMSUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

(注:以字母“S”开头的命令行表示消息需要通过 SendMessage() 函数发送。而以字母“R”开头的命令行表示来自应用的消息返回值。) 上述模式表明:系统向应用发送一个包含 PBT_APMQUERYSUSPEND 事件的 WM_POWERBROADCAST 消息并请求应用许可,如果得到允许则挂起系统。该消息和事件会发送给系统内当前运行的所有应用、线程及进程。在所有事件中,该事件通常会首先引起系统挂 起。收到消息后,应用应当在返回前做好关闭准备。这些准备工作根据不同应用而异,但应当包含诸如将数据库表存储到硬盘、保存文件到硬盘、释放内存或共享资 源、断开网络连接或关闭任何其它有利于确保应用安全退出的任务。一般情况下,系统最多能从应用的消息队列中提取 20 秒长的消息。因此,任何必要的关闭操作都应当在收到事件时立即开始。本案例中,应用返回值为“TRUE(真)”,表示可以继续执行挂起。

系统挂起前发送的消息为带有 PBT_SPMSUSPEND 事件的 WM_POWERBROADCAST。而返回值“TRUE(真)”由应用返回。


拒绝挂起请求

如果应用不准许或“不希望”系统执行挂起,就会通过某种方式通知系统请求已被应用拒绝。为此,应用会 在收到 PBT_APMQUERYSUSPEND 事件时返回 BROADCAST_QUERY_DENY 值。这时系统就会“知道”:某一应用或进程不能或“不允许”挂起操作。在应用拒绝挂起的情况下,消息模式如下:

S WM_POWERBROADCAST (long) dwPowerEvent:PBT_APMQUERYSUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:424D5144]

S WM_POWERBROADCAST (long)dwPowerEvent:PBT_APMQUERYSUSPENDFAILED

请注意,在该案例中,lResult 中的应用返回值为 424D5144,即 BROADCAST_QUERY_DENY 的值。随后,系统发出 PBT_APMQUERYSUSPENDFAILED 事件,通知所有应用:挂起请求失败,继续执行应用内的正常操作。

此外,我们也可以使用 SetThreadExecutionState() 函数防止系统挂起。该函数用来向系统发出其正在被使用的通知,并完全防止系统发送用来执行挂起的 WM_POWERBROADCAST 消息和事件(正如我们的测试所表明的那样)。但系统会继续发出用来处理电池和电源状态变化的事件,因此这些事件仍然可被监测到。有 3 个标记与此函数存在关联:ES_SYSTEM_REQUIRED——表明某些操作正在执行;ES_DISPLAY_REQUIRED——表明某些操作需要 进行显示;ES_CONTINUOUS——通知系统在发生变化前保持伴随状态。我们建议:像传真服务器、应答机、备份代理以及网络管理这样的应用都应当使 用 ES_SYSTEM_REQUIRED,而多媒体应用(如视频播放器和演示应用等)应当使用 ES_DISPLAY_REQUIRED。如未使用 ES_CONTINUOUS,空闲计时器将被完全重置,因此需要定期调用该函数来重置计时器。但也要注意,该函数不会阻止因关闭笔记本电脑顶盖造成的系统 挂起。从根本上阻止系统挂起的唯一办法就是拒绝上文中提到的 PBT_APMQUERYSUSPEND 事件。此外,该函数不会一直阻止屏保的运行。另外还有许多其它方式也可以阻止挂起,如事先定义好一个时间值,然后用 SystemParametersInfo() 函数来查询和重置屏保超时数值。


从挂起状态恢复

对应用而言,离开挂起状态并继续执行操作与进入挂起状态这一系统行为同等重要,因此应用开发人员在处 理进程执行中收到消息时应尤其注意。恢复内存、磁盘状态以及数据库表这些案例仅代表了应用中的部分元素,而应用离开挂起状态时还需要进行刷新和重新配置。 如果这些元素未处于正常状态,那么即使是正常的应用行为也会造成数据的破坏和丢失。在挂起过程中,WM_POWERBROADCAST 消息用于向应用发出正在/已经完成恢复操作的告警。一般来讲,下列消息和事件都会由操作系统发出:

S WM_POWERBROADCAST (long)dwPowerEvent:0012

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

S WM_POWERBROADCAST (long)dwPowerEvent:PBT_APMRESUMESUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

第一个事件为 0x12,对应 PBT_APMRESUMEAUTOMATIC。该事件仅用于向应用发出正在执行自动恢复操作的通知。通常情况下,该事件无需回应,但也有一个例外——这 一点会在下文中讲到,设计应用时要注意这个问题。下面要发出的事件为 PBT_APMRESUMESUSPEND。只有在执行挂起进程的过程中发送 PBT_APMSUSPEND 的情况下,该事件才会被发出。这表明:系统已经安全完成了恢复操作,应用可以继续执行。如果应用在挂起时释放内存、数据库表或其它重要信息,开发人员就必 须对这些应用内的运行元素重新进行初始化或分配。上文已经提到,使应用保持正常运行状态十分重要。

其它能够收到的恢复事件包括:PBT_APMQUERYSUSPENDFAILED 和 PBT_APMRESUMECRITICAL。上文已经说过,如果某些其它进程已经拒绝了系统发出的挂起请求,系统就会收到表明应用可以继续执行正常操作 的PBT_APMQUERYSUSPENDFAILED 事件。PBT_APMRESUMECRITICAL 会在部分或全部应用没有收到 PBT_APMSUSPEND 事件(如在电池耗尽后的情况)时发出。消息是非常重要的,因为如未收到消息,那么先前可用的资源或数据可能会变得无法使用。而对于异常脆弱的网络连接,应 用更应提前保存位于网络上的任何文件或数据。总之,任何应用都应当在收到这一事件时恢复到最佳状态,保证最理想的效能。


休眠

系统关闭时,休眠功能会完全保留系统状态,以便在电源重新接通时系统能够恢复到休眠前的状态。但是, 当系统由挂起状态转入休眠状态,然后再回到先前状态时,消息模式中会出现一件十分有趣的反常现象。下面的例子便描述了该模式下的异常状态:

即将挂起

S WM_POWERBROADCAST (long)dwPowerEvent:PBT_APMQUERYSUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

S WM_POWERBROADCAST (long)dwPowerEvent:PBT_APMSUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

即将休眠

S WM_POWERBROADCAST (long)dwPowerEvent:0012

S WM_POWERBROADCAST (long)dwPowerEvent:PBT_APMQUERYSUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

S WM_POWERBROADCAST (long)dwPowerEvent:PBT_APMSUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

S WM_POWERBROADCAST (long)dwPowerEvent:PBT_APMSUSPEND

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

按下开机按钮

S WM_POWERBROADCAST (long)dwPowerEvent:0012

R WM_POWERBROADCAST fSucceeded:True [lResult:00000001]

S WM_POWERBROADCAST (long)dwPowerEvent:0012

系统进入休眠状态时,事件顺序发生了变化,当我们再次启动系统后,接收到了 PBT_APMAUTOMATICRESUE,0x12 事件。与正常顺序比较后我们发现:PBT_APMRESUMESUSPEND 事件不见了,这表明系统执行了恢复操作。测试后我们得出结论:有些情况下,只能接收到一条 0x12 代码;而有些情况下则会收到两条。无论怎样测试,从休眠状态恢复时我们都没有收到 PBT_APMRESUMESUSPEND 事件,即使文档记录中提示该事件将发出的情况下,我们也未收到。也许我们需要在 PBT_APMRESUMESUSPEND 和 PBT_APMAUTOMATICRESUME 事件之间进行协调,以确保应用能在挂起操作之后保持正常的运行状态。


代码实例

为便于读者透彻了解如何处理与挂起和恢复操作相关的消息和事件,我们特举如下范例说明:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message,

WPARAM wParam, LPARAM lParam)

{

int wmId, wmEvent;

PAINTSTRUCT ps;

HDC hdc;

switch (message)

{

.

.

.

case WM_POWERBROADCAST:

switch (wParam)

{

case PBT_APMQUERYSUSPEND:

PrepToSuspend(TRUE);

return (TRUE);

case PBT_APMQUERYSUSPENDFAILED:

ReInitVars();

break;

case PBT_APMRESUMESUSPEND:

ReInitVars();

break;

case PBT_APMSUSPEND:

break;

case PBT_APMRESUMECRITICAL:

ReInitVarsCritical();

break;

case PBT_APMBATTERYLOW:

MessageBox(hWnd,”Battery is Critical”,”Low Battery”,MB_OK);

break;

case PBT_APMPOWERSTATUSCHANGE:

AdvisePowerChange();

Break;

case PBT_APMOEMEVENT:

break;

case PBT_APMRESUMEAUTOMATIC:

break;


}

.

.

.

default:

return DefWindowProc(hWnd, message, wParam, lParam);

}

return 0;

}


这个方法可以处理每个与 WM_POWERBROADCAST 消息存在关联的事件,但绝对不是唯一的方法。请注意,在收到 PBT_APMQUERYSUSPEND 事件并返回 TRUE(真)后,应用对 PrepToSuspend(TRUE) 的调用也随之发生。如果应用需要拒绝挂起请求,只需返回一个 BROADCAST_QUERY_DENY,而不必像实例中那样调用函数。还需注意的是,每个非关键恢复相关消息都会调用 ReInitVars() 函数,而关键恢复事件则会调用 ReInitVarsCritical() 函数。函数内容的差异可以通过一个更为强大且面向关键恢复的初始化过程(前文已经提到)反映出来。

上面的实例中还列出了本文并未提及的 3 个事件。PBT_APMOEMEVENT 并不是一个统一事件,OEM 厂商会根据自己的判断来尝试捕捉各自事件。多数情况下,该事件无需回应。如果系统标出了这一状态,PBT_APMBATTERYLOW 就会被收到。必要时还会采取其它适当行动,而不仅仅是通知用户电池电量不足。PBT_POWERSTATUSCHANGE 属于事件,由应用处理。该事件会标示出机器中电源状态的变化,并及时报告电池电量信息及其它应用所需的重要数据。具体细节还很多,就留待各位读者自己去发 现吧。


结论

每次审视应用的新功能、确定其是否与特定产品相关时,我们都会提出“我们为什么要这样编写应用?”这 个老问题。在解决挂起这个问题上,随着越来越多的人选择购买笔记本电脑并将其作为家用和办公环境中的主要系统,如何避免因挂起导致的数据丢失便成了摆在应 用开发人员面前的一个日益紧迫的问题。能够自由的挂起或恢复至运行状态、将错误和问题降至历史最低点、像欣赏艺术那样体验应用挂起——这就是用户心中的理 想应用。需要依赖网络中的文件或内容,需要在无需用户交互的情况下确保长时间的显示和运行,需要能够捕捉用户输入用于存储或日后调用的信息,需要存储大量 数据等等——只有符合这些标准的应用才能可靠地处理此类消息和事件。软件架构师和战略家能够设计出更为可靠、更加优秀的应用,帮助人们应对移动办公和生活 方式带来的挑战。

如今,要满足用户对移动计算的需求,就必须对软件需求进行一次重大改造。而操作系统可以利用 WM_POWERBROADCAST 消息来标示系统运行过程中即将出现的各种变化。众所周知,挂起功能可以节约电池能耗,今后势必将给应用带来更为深远的影响。而应用设计也应更加重视能耗问 题,想方设法延长电池的使用时间。通过合理安排消息与事件,我们终将实现降低应用能耗的目标。