来源:
IT168.com
作者:
李祥敬
2009-09-27/14:03
无论是普通用户还是应用程序开发人员,在他们将应用程序运行到新的操作系统时,他们遇到的最普遍的应用程序兼容性问题是,应用程序检查操作系统版本失败所带来的应用程序对新操作系统的不支持。操作系统版本检查本来是为了确保应用程序所需要的某些操作系统特性确实存在,有了这些操作系统特性,应用程序才可以正常运行。当操作系统版本检查被滥用的时候,很多兼容性问题由此而产生。用户可能会遇到应用程序在加载的时候,悄无声息地退出而没有任何的信息提示。或者是,用户可能会看到一个类似“此应用程序必须在Windows XP或者是其后的操作系统上运行”的错误提示对话框。但是,事实上这台计算机的操作系统是Windows 7,当然是在Windows XP之后了。错误或者说是低劣的版本检查,会给用户带来极大的不便。
通常,应用程序因为操作系统版本检查而失败有两个原因:
• 在版本检查的代码中有Bug。在主版本号增加,次版本号减小的情况下,例如,将版本号从5.1变化到6.0,或者是某个期望的补丁(service pack)没有安装,甚至你在运行一个更新的操作系统(例如,从Windows XP SP3升级到Windows Vista SP1),这些情况都会导致版本检查会失败。
• 应用程序开发人员特意设计的版本检查行为,阻止应用程序在未经过足够测试的新操作系统上运行。但是,我们建议你不要阻止应用程序在更新的操作系统上运行。除非最终用户许可协议禁止应用程序在更新的操作系统上使用,否则应用程序不应在操作系统版本号增加的情况下无法运行。如果应用程序无法运行,则必须向用户发送消息,并向日志写入一条消息,然后正常退出。
当一个应用程序在“不兼容”的Windows版本上运行的时候,它可能会显示一个错误消息,也可能悄无声息地退出或者是无法正常工作。通常,如果我们解决了版本检查的问题,它就可以正常工作。当遇到这种问题,最终用户或者是IT管理员可以简单地使用Windows 7的XP兼容模式,或者是微软所提供的应用程序兼容性工具集(Application Compatibility Toolkit (ACT)),对应用程序进行操作系统版本“欺骗”,让应用程序认为他还是运行在较旧的Windows版本上,从而解决版本检查的问题,让应用程序在Windows 7上正常运行。这里需要注意的是,兼容模式仅仅适用于使用非托管代码进行的操作系统版本检查,对于托管代码中利用Environment.OSVersion或者是通过P/Invoke使用GetVersionEx进行的操作系统版本检查并不适用。
图1 Windows 7的兼容模式#p#副标题#e#
更好的版本检查
识别当前操作系统版本并不是一个确定某些操作系统特性是否存在的最好方法。但是,如果你的应用程序无法检查某项特性是否存在,唯一的办法就是通过版本检查来确定应用程序的兼容性。当你确定要进行版本检查的时候,可以考虑下面的实例代码。
对于非托管代码的应用程序,你必须确保你的版本检查能够应对更新版本的Windows操作系统。请一定记住:不能阻止版本变化!在下面这段代码中,我们使用了GetVersionEx来获取操作系统的版本。如果主版本号大于5(包括Windows Vista,Windows Server 2008 R2和Windows 7),版本检查将通过,如果等于5,那么次版本号应该大于或者等于1(包括Windows XP,Windows Server 2003)。
#include <windows.h>
#include <stdio.h>
void main()
{
OSVERSIONINFO osvi;
BOOL bIsWindowsXPorLater;
// 初始化OSVERSIONINFO结构体
ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
// 获得当前操作系统版本
GetVersionEx(&osvi);
// 通过版本号,判断是否是XP及其以后的操作系统
bIsWindowsXPorLater =
( (osvi.dwMajorVersion > 5) ||
( (osvi.dwMajorVersion == 5) && (osvi.dwMinorVersion >= 1) ));
if(bIsWindowsXPorLater)
printf("当前系统满足要求.\n");
else printf("当前系统不满足要求.\n");
}
在非托管代码中,我们还可以使用VerifyVersionInfo来检查操作系统版本,以实现对操作系统版本的最小要求(Windows XP SP2)。例如:
#include <windows.h>
// 检查操作系统最小版本需求
// 如果当前的操作系统版本在Windows XP SP2或者以后(例如,Windows Vista, Windows 7),
// 这个函数就返回TRUE
BOOL TestMinimumOSRequirement() {
OSVERSIONINFOEX osvi;
// 初始化OSVERSIONINFOEX结构体.
ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
osvi.dwMajorVersion = 5;
osvi.dwMinorVersion = 1; // Windows XP
osvi.wServicePackMajor = 2; // Service Pack 2
osvi.wServicePackMinor = 0;
// 初始化比较条件
// ULONGLONG VER_SET_CONDITION(ULONGLONG dwlConditionMask,
// DWORD dwTypeBitMask, BYTE dwConditionMask)
ULONGLONG comparisoninformation = 0;
BYTE conditionMask = VER_GREATER_EQUAL; // 需要大于或者等于,这里可以根据需要灵活设置
VER_SET_CONDITION(comparisoninformation, VER_MAJORVERSION, conditionMask);
VER_SET_CONDITION(comparisoninformation, VER_MINORVERSION, conditionMask);
VER_SET_CONDITION(comparisoninformation, VER_SERVICEPACKMAJOR, conditionMask);
VER_SET_CONDITION(comparisoninformation, VER_SERVICEPACKMINOR, conditionMask);
// 进行版本比较
return VerifyVersionInfo( &osvi,
VER_MAJORVERSION | VER_MINORVERSION |
VER_SERVICEPACKMAJOR | VER_SERVICEPACKMINOR,
comparisoninformation);
}
相对于非托管代码,托管代码的版本检查就简单的多了。我们可以使用Version对象的==, !=, <=, <, >, >=操作符来跟Environment.OSVersion.Version对象进行比较,以执行正确的版本检查。例如:
static void Main()
{
// 这段代码将检查当前系统是否是Windows XP或之后的操作系统
if (Environment.OSVersion.Version < new Version(5, 1))
{
MessageBox.Show("本应用程序需要运行在Windows XP或以后的操作系统.",
"不兼容的操作系统", MessageBoxButtons.OK,
MessageBoxIcon.Error);
return;
}
// 符合版本要求,继续运行
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
#p#副标题#e#
检查操作系统的特性
有一些应用程序需要某些特殊的操作系统特性支持才能正常运行,所以对操作系统版本有特殊的要求。但是如上所述,检查操作系统的版本并不是一个确保操作系统拥有某项特性的最好方法。这是因为操作系统可能有新的特性添加进来,相比于使用GetVersionEx来检查操作系统平台或者是版本号,更有效的方法是直接检查某项特性本身是否存在。例如,微软打算将Windows 7的两个新特性Direct2D/DirectWrite和Ribbon API引入到Windows Vista中,这样,虽然Windows Vista的主版本号没有变化,但是却拥有了新的特性。当我们的应用程序在判断操作系统是否具有Ribbon特性而决定是否使用更加绚丽的用户界面时,如果仅仅根据版本号来做出判断,就错过了操作系统所提供的新特性了。
如果可以,在这些特性不可用的情况下,你的应用程序也应该能够继续运行,虽然减少了功能或者是降低了性能。
非托管代码检查操作系统特性
对于Win32应用程序,我们可以使用下面这些技术来检查操作系统特性:
• 如果一个库没有加载到你的应用程序,可以使用LoadLibrary()加载这个库。如果你对一个已加载的库(例如,kernel32.dll)中的新函数感兴趣,可以调用GetModuleHandle()获得这个模块的句柄。
• 使用GetProcAddress()获得函数的指针。如果GetProcAddress()返回NULL,则表示这个函数不存在,我们需要提供一些备选方案。将这个指针转化为合适类型的函数指针就可以使用操作系统的特性了。对于一些特殊的函数,虽然他们确实存在,但是可能会返回一个“尚未实现”的错误。针对这种情况,需要特殊处理。
在Windows 2000中,有一个函数SetWaitableTimer可以设置比较精确的计时器,下面的代码就演示了如何检查操作系统是否具有这项特性:
// 定义函数指针类型
typedef BOOL (WINAPI *SetWaitableTimerExProc)(
__in HANDLE hTimer,
__in const LARGE_INTEGER *lpDueTime,
__in LONG lPeriod,
__in PTIMERAPCROUTINE pfnCompletionRoutine,
__in LPVOID lpArgToCompletionRoutine,
__in PREASON_CONTEXT WakeContext,
__in ULONG TolerableDelay
);
LARGE_INTEGER liDueTime;
liDueTime.QuadPart = 0;
nt period = 1000;
unsigned int tolerance = 1000;
HANDLE hTimer = // Get timer handle
REASON_CONTEXT reasonContext = {0};
reasonContext.Version = 0;
reasonContext.Flags = POWER_REQUEST_CONTEXT_SIMPLE_STRING;
reasonContext.Reason.SimpleReasonString = L"MyTimer";
// 获得已经加载模块的句柄
HMODULE hKernel32Module = GetModuleHandle(_T("kernel32.dll"));
if (hKernel32Module == NULL)
return FALSE;
// 获得函数地址
SetWaitableTimerExProc pFnSetWaitableTimerEx =
(SetWaitableTimerExProc) ::GetProcAddress(hKernel32Module,
"SetWaitableTimerEx");
// 检查函数是否存在
// 也就是判断某些特性是否存在
if (pFnSetWaitableTimerEx == NULL)
return FALSE;
// 调用函数
if (!pFnSetWaitableTimerEx(hTimer, &liDueTime, period, NULL, NULL,
&reasonContext, tolerance)
{ // 处理错误 }
这样,我们就可以直接检查操作系统是否具有某项特性,不会因为版本号而错过一些非常有用的操作系统特性了。#p#副标题#e#
托管代码检查操作系统特性
对于托管代码,我们可以通过P/Invoke包装Win 32 API从而对其实现调用。例如:
/// <summary>
/// 通过P/Invoke包装Win32 API函数
/// </summary>
internal class Win32
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateWaitableTimerEx(IntPtr securityAttrs,
string timerName,
TimerFlags timerFlags, SyncObjAccessFlags desiredAccess);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CancelWaitableTimer(IntPtr hTimer);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr handle, uint timeout);
/// <summary>
/// 激活计时器. 这个函数可以工作在Windows 2000及其以后的操作系统
/// </summary>
/// <param name="hTimer"></param>
/// <param name="dueTime"></param>
/// <param name="period"></param>
/// <param name="pCompletionRoutime"></param>
/// <param name="completionRoutineContext"></param>
/// <param name="resume"></param>
/// <returns></returns>
[DllImport("kernel32.dll", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetWaitableTimer(IntPtr hTimer,
ref long dueTime, int period, IntPtr pCompletionRoutime,
IntPtr completionRoutineContext, [MarshalAs(UnmanagedType.Bool)] bool resume);
/// <summary>
/// 通过制定允许的延迟激活计时器
/// 这是Windows 7新引入的函数
/// </summary>
/// <param name="hTimer"></param>
/// <param name="dueTime"></param>
/// <param name="period"></param>
/// <param name="pCompletionRoutime"></param>
/// <param name="completionRoutineContext"></param>
/// <param name="wakeContext"></param>
/// <param name="tolerableDelay"></param>
/// <returns></returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetWaitableTimerEx(IntPtr hTimer,
ref long dueTime, int period, IntPtr pCompletionRoutime,
IntPtr completionRoutineContext,
ref ReasonContext wakeContext, uint tolerableDelay);
}
现在,我们就可以使用这些Win 32 API函数了,如果操作系统不存在某项特性,则会抛出异常,通过处理EntryPointNotFoundException和DllNotFoundException异常,判断特性是否存在,并在不存在的情况下提高备用方案。
// 使用Win 32 API函数
public void Start(long dueTime, int period)
{
ReasonContext rc = new ReasonContext();
rc.Version = 0;
rc.Flags = 1;
rc.SimpleReasonString = "MyTimer";
try
{
// 首先尝试使用Windows 7所引入的增强版本SetWaitableTimerEx
if (!Win32.SetWaitableTimerEx(_hTimer,
ref dueTime, period, IntPtr.Zero, IntPtr.Zero, ref rc, 5000))
throw new Win32Exception(Marshal.GetLastWin32Error(),
"SetWaitableTimerEx设置计时器失败.");
IsCoalescingtimer = true;
}
catch (EntryPointNotFoundException)
{
// 捕获异常,也就是当前操作系统不是Windows 7,没有SetWaitableTimerEx这一特性
IsCoalescingtimer = false;
// 提供备用方案,调用以前就有的版本SetWaitableTimer
if (!Win32.SetWaitableTimer(_hTimer, ref dueTime, period,
IntPtr.Zero, IntPtr.Zero, false))
throw new Win32Exception(Marshal.GetLastWin32Error(),
"SetWaitableTimer设置计时器失败.");
}
_waiterThread = new Thread(WaiterThreadProc);
_waiterThread.Name = "Waiter thread for WaitableTimer";
_waiterThread.Start();
}