本文作者: 冯兆麟(Simba) (kingsimba@tom.com)
本文献给使用Nokia Symbian 60 SDK各个版本开发游戏软件的程序员。虽然本文主要是针对游戏软件,但是大部分内容对一般应用软件也同样适用。
1.1.声明
为了避免良心的谴责,首先我必须承认一点,我本人并不是靠Symbian C++糊口。除了forum.nokia.com上的文章和SDK,我也没有看过任何关于Symbian的书籍。只是偶然的,我在天津猛犸游戏公司(www.mammothworld.com)认识并接触了Symbian。我从零起步,写出了一个蹩脚的Symbian游戏引擎并在3650、7650上开发了一些游戏。所以我对Symbian的掌握完全是出于自己的猜测和理解,虽然本文缺乏权威,但至少都是经验之谈,容易理解。
1.2.概述
Symbian游戏是运行在手机上的游戏,它不能干扰手机正常的通讯功能,对操作系统和其它应用程序必须友善。而在首次编写Symbian C++游戏时,我遇到了无数奇怪的问题,其中大部分问题出在内存极端不足,打开太多应用程序,屏幕保护探出,接到短信、电话等特殊情况下。其实如果养成严谨的代码风格,进行足够的错误处理,大部分问题本可以避免。为了解决它们,我很是花了一番功夫,所以在此把我的一些教训、经验写出,希望大家能避免犯同样的错误。如果你不是业余爱好者,而是为一个认真的开发商工作,特别是如果你的产品需要通过Symbian Signed认证(www.symbiansigned.com),你就必须更加小心的对待本文提出的问题。Symbian Signed是一个针对Symbian应用程序的认证,想要通过它,你的应用程序必须通过一系列严格的测试。认证对应用程序的文件管理、内存使用、系统事件响应、网络、资费和私人数据等都有一定的要求。如果想了解Symbian Signed认证的详细内容,可以去它们的网站下载白皮书。
1.3.异常处理
虽然我们都知道任何一个new (ELeave)或者带有L后缀的函数都可能抛出异常,但是很多业余的爱好者还是会忽视Symbian C++中异常处理的重要性。虽然有些函数只有在极其罕见的情况下才会抛出异常。但不是危言耸听,如果你不写代码捕捉并处理它们,应用程序就会遇到"系统错误"。普通C++使用throw抛出异常。异常抛出后,栈会不停回滚,直到遇到最近一层catch为止。Symbian C++中的异常处理不使用try-catch和throw。但是它的处理机制和标准C++很是类似,区别仅仅是它只能抛出一个整数错误码,而不是一个任意对象。我将从异常的抛出、捕捉、处理三方面讲解这部分内容。
1.3.1.抛出异常Symbian C++中,有下面几种情况下会抛出异常:使用静态函数User::Leave抛出异常。这个函数就是最基本的异常产生函数。下面讲的其它抛出方式都可以转化为User::Leave。使用静态函数User::LeaveIfError把错误码转化为异常。有些函数比如CFbsBitmap::Create( )有一个TInt的返回值。如果遇到错误,这些函数就会返回非KErrNone的错误值。此时,可以使用LeaveIfError把这个返回值转化为异常。比如:User::LeaveIfError(bmp->Create(iSize, EColor4k); 其实LeaveIfError就是if (returnValue != KErrNone) User::Leave(returnValue);使用new (ELeave)申请内存。如果没有足够内存可用,此操作产生一个KErrNoMemory异常。比如TText8* p = new (ELeave) TText8[20]; 相当于TText8* p = new TText8[20]; if (p == NULL) User::Leave(KErrNoMemory);调用带有L后缀的函数。Symbian系统的命名规范中要求,每一个可能Leave的函数都要有后缀L。包含有带L的内层函数调用的外层函数也必须加上L。这类函数中最常见的就是NewL, NewLC和ConstructL。这个规范比你想象的要重要。因为它给其他程序员一个暗示,提示他们对这些函数进行保护。
1.3.2.捕捉异常类似标准C++的catch语句,Symbian C++的TRAP关键字可以对一个可能产生异常的函数进行保护,并且捕获到异常值。比如:TInt errorCode;TRAP(errorCode, SomeDangerousFuncL( ) );// 保护执行SomeDangerousFuncL( )函数if (errorCode != KErrNone){// 捕捉到了一个异常,在这里添加处理异常的代码}类似的TRAPD省去了你声明一个局部变量的麻烦。头两行代码可以简写成:TRAPD(errorCode,SomeDangerousFuncL( ) );
1.3.3.处理异常对于不同的异常当然有不同的处理方法(废话:-))。我们以最常见的捕获到代表内存不足的KErrNoMemory异常为例讲解。注意在Container,AppUi等类的构造过程中,你不需要加入对内存不足的保护。因为这一切系统已经为你做好了。系统会弹出一个对话框报告内存不足。根据你的操作系统版本不同,这可能是中文的,也可能是英文或者其它语言的。如果你不信,可以在AppUi或者Container的 ConstructL中写一行User::Leave(KErrNoMemory)试试看。我试验的结果如下:
除了上面说的特殊情况,你可以简单的弹出一个对话框,告诉用户没有足够的内存运行程序,并且安全的关闭程序。比如我的游戏程序就是这样处理的:void CStageManager::DoGameFrame(){TRAPD(error, DoGameFrameProtectedL());if (error == KErrNoMemory){StopGame();m_noMemoryDlg->ExecuteLD(R_KEY_INVALID_DIALOG);Exit();// Call CAknAppUi::RunAppShutter( )}else if (error != KErrNone){User::Panic(_L("Some other error."), error);}}其中noMemoryDlg是直接或者间接在Container的ConstructL中创建的:// in header fileCAknQueryDialog* m_noMemoryDlg;// somewhere in ConstructLTBuf<128> errMsg;_LIT(formater, "Not enough memory. Please close some applications.");errMsg.Copy(formater);m_noMemoryDlg = new (ELeave) CAknQueryDialog(errMsg, CAknQueryDialog::EErrorTone);当然,你也可以制作一个精美的图片来报告内存不足,等待用户按任意键再退出。不过载入这个图片也可能会失败,所以至少在这个图片成功载入之前,你还是需要系统对话框来报告的。值得一提的是,你不一定需要退出程序,或者你可以稍后重试申请内存,幸运的话,没准第二次就能成功。这是因为Symbian系统会在内存不足时自动关闭一些应用程序。我觉得这是Symbian系统一个比较奇怪的设计。通常应用程序在AppUi的HandleCommandL中会响应EEikCmdExit消息,并且调用CAknAppUi::Exit( )函数(如下代码)。这使得应用程序可以在应用程序管理器中用C键结束掉。这也使得Symbian操作系统有机会在内存不足时通过这个渠道自动关闭一些应用程序。// ----------------------------------------------------// CFlyAppUi::HandleCommandL(TInt aCommand)// takes care of command handling// ----------------------------------------------------//
void CFlyAppUi::HandleCommandL(TInt aCommand){switch ( aCommand ){case EEikCmdExit:Exit();break;// TODO: Add Your command handling code heredefault:break;}}坦白说我没有尝试过重试申请内存这个办法,不过我想是可行的。
1.3.4.栈回滚和对象的安全析构上面说到在遇到某些异常时,你可以选择弹出对话框并且结束程序,其实这会比你想象的要困难一些。因为C++可不像Java那样有托管堆进行垃圾收集。不过好在C++栈会自动回滚,栈上的对象会被销毁。如果你此时调用CAknAppUi::RunAppShutter( )结束程序,那么AppUi,Container的析构函数会依次被调用,引起你自己创建对象的析构函数也依次被调用。那么堆上的对象也要被销毁。可是,请记住,异常随时随处可能发生,使对象处于一种"半构造"的状态。此时析构函数被调用可能会造成对无效指针的访问错误。请看下面这个例子,它犯了两个常见的错误:class BadExample{protected: TText8* m_pBuf; TText8* m_pBuf2;public: static BadExample* NewL() { BadExample* self = new (ELeave) BadExample(); self->ConstructL(); return self; }
void DeleteBuf() { delete m_pBuf; }
void RebuildBufL() { m_pBuf = new (ELeave) TText8[256]; }
private: BadExample(); ~BadExample() { delete m_pBuf; delete m_pBuf2;
} void ConstructL() { m_pBuf = new (ELeave) TText8[256]; m_pBuf2 = new (ELeave) TText8[256]; }};假设我们在AppUi的ConstructL中使用BadExample::NewL( )来构造对象,在AppUi的析构函数中delete这个对象。
下面我们分析一下可能遇到的问题:首先,在函数NewL中,self指针没有被保护,试想如果self->ConstructL( )一句抛出异常。那么这个self指针指向的对象就没有return给外界(也就是AppUi),这个对象就永远"丢失了",造成了内存泄露。正确的做法是使用CleanupStack对它进行保护。CleanupStack至少能保证在程序退出时压入其中的对象都能销毁。static BadExample* NewL(){BadExample* self = new (ELeave) BadExample();CleanupStack::PushL(self);self->ConstructL();CleanupStack::Pop();return self;}但是注意,此处还有一个微妙的内存泄露。仔细看看CleanupStack::PushL( )的声明:IMPORT_C static void PushL(TAny* aPtr);IMPORT_C static void PushL(CBase* aPtr);IMPORT_C static void PushL(TCleanupItem anItem);如果传入的指针是CBase指针,那么CBase的虚析构函数(virtual ~CBase( ))就能保证对象在销毁时正确的调用析构函数。可是本例中BadExample不是从CBase中派生,那么对象只能做很有限的销毁,根本不会调用析构函数。所以,如果ConstructL是由于第二个内存申请m_pBuf2失败,那么m_pBuf申请的内存就永远不会回收。所以正确的做法是,让 BadExample从CBase派生。class BadExample : public Cbase其次,我们并没有为 m_pBuf和m_pBuf2赋初值,在Release版中他们的值是随机的。那么,如果m_pBuf2的申请失败,析构函数还是会执行delete m_pBuf2,试图删除一个无效指针。正确的做法是在构造函数中为m_pBuf和m_pBuf2赋初值NULL。因为标准C++规定,delete一个空指针不做任何操作。不过实际上,如果对象从CBase派生,这一步是没有必要的,因为CBase能保证派生类的成员变量在构造时自动清零。最后,动态的使用DeleteBuf和RebuildBufL是不安全的。如果你先用DeleteBuf删除了这个对象,那么m_pBuf就是一个坏指针。可是紧接着的RebuildBufL可能会失败。此时如果析构函数被调用,还是会产生delete无效指针的错误。正确的做法是,在DeleteBuf 中,把m_pBuf设为NULL。总结上面说到的几点,完整的安全的代码是:class BadExample : public CBase{protected:TText8* m_pBuf;TText8* m_pBuf2;public:static BadExample* NewL(){BadExample* self = new (ELeave) BadExample();CleanupStack::PushL(self);self->ConstructL();CleanupStack::Pop();return self;}
void DeleteBuf(){delete m_pBuf;m_pBuf = NULL;}
void RebuildBufL(){m_pBuf = new (ELeave) TText8[256];}
private:~BadExample(){delete m_pBuf;delete m_pBuf2;}void ConstructL(){m_pBuf = new (ELeave) TText8[256];m_pBuf2 = new (ELeave) TText8[256];}};
1.4.安全的图像引擎
Symbian C++游戏的2D图像显示部分一般由下面几个类组成:图像: 封装了一个CWsBitmap。是基本的图片资源。支持图像之间的各种贴图和混合操作。双缓冲: 一个和屏幕分辨率、色深相等的图像。直接写屏支持: 复合一个CDirectScreenAccess对象,实现MDirectScreenAccess接口。负责直接写屏的安全处理。比如来电、屏保时适时的停止和开启直接写屏与游戏逻辑。绘图类: 负责在图像中绘图。它不是对Gc的封装,而是通过直接修改图像内存区进行绘图。位图字体类: 使用预先创建的位图资源写字。如下图就是一个预先创建的位图资源。优点是速度快,缺点是无法支持大字符集合,比如中文。
字体缓冲区类: 还是使用Gc的DrawText函数绘制文字。但是同时用一张位图作为一个缓冲区存储最近绘制的文字。既能支持大字符集合,速度也很快。如果需要学习图形和直接写屏的基础,请参考Programming Games in C++ v1.0(www.forum.nokia.com/main/1.6566.21.00.html)。本文主要针对图像类和直接写屏类讲几个容易被忽略的问题。
1.4.1.图像类的直接内存访问
贴图是2D游戏最主要的画面操作。为了实现快速的贴图,或者实现某种混合效果,就不能再使用CFbsBitGc的BitBlt或者BitBltMasked进行贴图,而必须自己得到图片的内存地址,直接读写其中的数据。在读写图片内存地址的过程中,有几点需要加以注意。首先,只有当源图片和目标图片色深相等时,才更容易进行贴图操作。所以,再载入图片的过程中,我习惯把非4k色的图片转化为4k色。之所以选择4k色是因为它也是后台缓冲区的色深。下面的代码通过转换可以保证iImage是4k色的图像。// Make sure that we have a 4K color depth image in iImageif (iImage->DisplayMode() != EColor4K){// Create 4k color imageCFbsBitmap* image = new (ELeave) CWsBitmap();CleanupStack::PushL(image);User::LeaveIfError(image->Create(iSize, EColor4K));
// Create deviceCFbsBitmapDevice* device = CFbsBitmapDevice::NewL(image);CleanupStack::PushL(device);CFbsBitGc* gc;User::LeaveIfError(device->CreateContext(gc));CleanupStack::PushL(gc);
// Bitblt to new color depthgc->BitBlt(TPoint(0,0), iImage);
// Destroy context and device;CleanupStack::PopAndDestroy();// gcCleanupStack::PopAndDestroy();// deviceCleanupStack::Pop();// imagedelete iImage;iImage = image;}其次,Symbian系统在内存匮乏时会进行碎片整理。所以如果简单的用CFbsBitmap::DataAddress获取内存首地址并开始读写,那么可能在你读写的过程中,图片已经被悄悄的移动了位置,你读写的就是一块无效的内存区域。解决这个问题的办法是在获取首地址前,必须先锁定图像内存区域。在高版本的60系列SDK 中(比如2.0,2.1),有LockHeap和UnlockHeap函数可以完成这个操作。但是在低版本的SDK中(比如0.9,1.0),这两个函数是私有的。我们必须通过TBitmapUtil锁定内存。但是不一定必须使用TBitmapUtil的SetPixel和GetPixel函数进行位操作。下面是最基本的没有关键色和Alpha通道的简单贴图代码。void CImage::RenderToBitmapL(CFbsBitmap* aBmp, TPoint aPos, const TRect& aRect){// 在此计算贴图目标矩形区域// 代码略去
// 没有关键色和蒙板的最简单、最快情况if (!iKey && iMask == NULL){// 锁定TBitmapUtil bmpUtil1(ImageL());TBitmapUtil bmpUtil2(aBmp);bmpUtil1.Begin(TPoint(0,0));bmpUtil2.Begin(TPoint(0,0), bmpUtil1);
// 获取首地址TUint16* addr2 = (TUint16*)ImageL()->DataAddress();// source imageTUint16* addr = (TUint16*)aBmp->DataAddress();// target bmpTInt line = aBmp->ScanLineLength(aBmp->SizeInPixels().iWidth,EColor4K) / 2;TInt line2 = iImage->ScanLineLength(// line length in 16bit wordiImage->SizeInPixels().iWidth,EColor4K) / 2;
// 计算扫描持续量和跳跃量TInt jump = line - rectw;TInt lasting2 = rectw;TInt jump2 = line2 - lasting2;
// 获取贴图首地址TUint16* p = addr + fromY * aBmp->SizeInPixels().iWidth + fromX;TUint16* p2 = addr2 + line2 * recty + rectx;
// The first pixel out of interestTUint16* p2end = p2 + line2 * (toY - fromY - 1) + lasting2 + jump2;
// 开始扫描while(p2 != p2end){// 开始一个扫描行TUint16* p2endline = p2 + lasting2;while(p2 != p2endline){// 复制一个像素*p = *p2;// 移动到下一个像素p++; p2++;}// 跳到下一行p += jump; p2 += jump2;}
// 解锁bmpUtil2.End();bmpUtil1.End();
return;}
// 其它情况。有关键色等等.// ...
最后告诉大家几个优化的小窍门:使用While循环直接把指针的比较作为循环结束条件。不要再多用一个整数来控制循环。贴图是个两重循环,如果你的代码需要判断是否支持关键色和Alpha通道等,尽量把判断外移到循环之外。每个象素都进行好几个if判断的开销太不值得。比如上面的代码,处理最简单的情况时,while循环内一个if都没有。4k色时,RGB内存排列如下图。所以未被使用的4位正巧可以用来存储alpha通道。
1.4.2.直接写屏和特殊系统事件
游戏软件一般用CDirectScreenAccess进行直接写屏。大家都知道,WindowServer会在需要停止直接写屏时回调 MDirectScreenAccess::AbortNow接口函数,在可以重新启动时回调MDirectScreenAccess::Restart 接口函数。可是具体在这两个函数中做什么,SDK没有过多的介绍。我在此说一下我的做法。如果你合理的处理了这两个函数,就可以轻松应对来电、屏保、程序切换等事件。我们先说AbortNow,它的处理比较简单。你之需在其中停止驱动游戏逻辑的计时器(一般是个CPeriodic对象),停止声音模块(一般是一个CActive任务)就可以了。值得费些力气的是Restart函数,它并不是在应用程序回到前台,并且可以进行全屏直接写屏时才被回调。所以不能在此时武断的恢复游戏逻辑,开始游戏。首先,你要调用CDirectScreenAccess::StartL( )恢复直接写屏。但是必须给这个函数加上TRAP保护。因为它很可能抛出KErrNotReady异常。如果遇到这个异常,那你就直接返回好了,因为直接写屏此时并不能开始。接下来你需要检查一下绘图区域,看是否整个屏幕都可以被使用。如果不是,那也无需启动游戏逻辑,只需要用最后保留的后台缓冲区的内容更新直接写屏区域即可。第三种情况,如果直接写屏成功启动,并且整个屏幕都可以被绘制,才启动游戏逻辑,启动声音等其它模块。完整的代码如下:void CEngine::AbortNow(RDirectScreenAccess::TTerminationReasons /*aReason*/){// Cancel timer and displayif (iGameTimer->IsActive())iGameTimer->CancelTimer();if (!iGameWorldPaused){iGameWorldPaused = ETrue;iGameWorld->PauseGame();// Pause audio stream etc.}iPaused = ETrue;}
void CEngine::Restart(RDirectScreenAccess::TTerminationReasons /*aReason*/){TRAPD(error, SetupDirectScreenAccessL());switch(error){case KErrNone:break;case KErrNotReady:if (iDirectScreenAccess->IsActive())iDirectScreenAccess->Cancel();if (iGameTimer->IsActive())iGameTimer->CancelTimer();if (!iGameWorldPaused){iGameWorldPaused = ETrue;iGameWorld->PauseGame();}return;default:User::Panic(_L("Setup DSA Error"), error);}
if(iPaused){if(iGameDawingArea == iRegion->BoundingRect()){iPaused = EFalse;if(!iGameTimer->IsActive()){iGameWorldPaused = EFalse;iGameWorld->ResumeGame();iGameTimer->Restart();}}else{PauseFrame();}}else{if(!iGameTimer->IsActive()){iGameTimer->Restart();}}}
void CEngine::SetupDirectScreenAccessL(){// Initialise DSAiDirectScreenAccess->StartL();
// Get graphics context for itiGc = iDirectScreenAccess->Gc();
// Get region that DSA can draw iniRegion = iDirectScreenAccess->DrawingRegion();
// Set the display to clip to this regioniGc->SetClippingRegion(iRegion);}
void CEngine::PauseFrame(){// Force screen update: this is required for WINS, but may// not be for all hardware:iDirectScreenAccess->ScreenDevice()->Update();
// and draw from unchanged offscreen bitmapiGc->BitBlt(TPoint(0,0), &(iDoubleBufferedArea->GetDoubleBufferedAreaBitmap()));
iClient.Flush();}};
1.5.声音处理
我的引擎中使用CMdaAudioOutputStream和MMdaAudioOutputStreamCallback完成声音播放功能。它主要有三个类组成:CAudioStreamPlayer。它复合CMdaAudioOutputStream,继承CActive,实现MMdaAudioOutputStreamCallback接口。我们需要小心的维持缓冲区的大小以获得低延迟播放。CActive不断的建立新的任务,在RunL函数中估算缓冲区中的剩余数据,向其中追加适当的数据,维持缓冲区的预期大小。CSimpleMixer。它实现CAudioGenerator接口。因为CMdaAudioOutputStream是一个单一的流式播放器,所以需要写一个混音器进行波形混合。这里波形混合就是简单的数据相加。混音器有许多的声道(channel)。每个channel记录了其中的CAudio指针和当前播放位置。CAudio。包含一个音频缓冲区。对每个声音文件,我们还需要一个类把它载入到内存缓冲区中。我不会在此讲解如何实现音频播放,那需要单独的一篇文章。如果你也使用这种方法实现声音播放,我只想在此和大家讨论两个问题。需要学习声音基础的话,可以参考www.newlc.com/article.php?id_article=113。(可惜我当时学习声音时那篇文章和代码找不到了)
1.5.1.声音的关闭和开启因为整个音频系统是一个拉的结构,音频流从混音器那里拉数据,混音器从音频缓冲区中拉数据。所以,只要把CMdaAudioOutputStream和写数据的CActive对象delete掉,声音播放就全部停止了。在我的实现中,也就是delete CAudioStreamPlayer对象即可。再想要开启声音,只需要重新创建这个对象。这个实现的好处是程序的其它部分不需要保存声音是否开启这个状态。因为CAudio和CSimpleMixer对象是存在的,CAudio就可以把自己插入到Mixer的channel中,觉得自己好像在播放一样。其实因为CAudioStreamPlayer根本没有从Mixer向外拉数据,声音设备是完全停止的。但是在恢复声音播放时有一点需要注意,恢复前需要清空混音器中的声音数据。因为经过了长时间的运行,混音器中的各个channel中已经塞满了各种声音。如果此时突然打开,会传出各种延迟了的杂音。
1.5.2.特殊错误处理MMdaAudioOutputStreamCallback接口中的几个回调函数MaoscOpenComplete、MaoscBufferCopied和MaoscPlayComplete都有一个错误码参数。你不能忽略这个参数。比如MaoscPlayComplete函数,是在音频停止播放时被调用。停止播放的原因可能是多种多样的。我们都知道要处理KErrUnderflow这个情况,这个错误吗意味着混音器没有及时的供给它音频数据。此时需要重新启动声音流。但是还有一些情况比如KErrDied和KErrInUse很容易被忽略。KErrDied发生在接听电话时,此时声音线程已经死了,那么就需要重建整个音频系统。KErrInUse发生在收到短信时,此时声音设备被抢占,用来播放短信提示音。此时你也需要重建整个声音系统,但是此时不能立刻重建,否则还是一样的结果。你应该等待几秒钟之后才重建它。上面说的重启声音流和重建声音系统深度不同。重启声音流在稍后的代码中可以看到。其中RunAudioL向音频流写入了第一个声音缓冲区。重建声音系统在我的实现中就是指先delete 再NewL创建CAudioStreamPlayer对象。这三个错误的处理代码如下:// Audio stream API callback: Called when playback has finished.void CAudioStreamPlayer::MaoscPlayComplete(TInt anError){if (m_bInDelay)return;// If we finish due to an underflow, we"ll need to restart playback.// Normally KErrUnderlow is raised at stream end, but in our case the API// should never see the stream end -- we are continuously feeding it more// data! Many underflow errors mean that the latency target is too low.if ( anError == KErrUnderflow ) {iObserver->MasoMessage(_L("Play Underflow"));// The number of samples played gets resetted to zero when we restart// playback after underflowiBaseSamplesPlayed = iSamplesWritten;
// Stop and restartiStream->Stop();Cancel();#ifdef RATE_16KiStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate16000Hz,TMdaAudioDataSettings::EChannelsMono);#elseiStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate8000Hz,TMdaAudioDataSettings::EChannelsMono);#endifiStream->SetVolume(iStream->MaxVolume() / 4);TRAPD(error, RunAudioL());if ( error != KErrNone ) {User::Panic(KPlay, error);}
return;}else if ( anError == KErrDied ){m_bInDelay = ETrue;m_RebuildDelay = 0;// no delay}else if ( anError == KErrInUse ){m_bInDelay = ETrue;m_RebuildDelay = 3000;// delay 3 second}else if ( anError != KErrNone ) {// Some other error, panic!User::Panic(KPlayComplete, anError);}}由外界发现m_RebuildDelay标志,重建CAduioStreamPlayer这个对象。除了MaoscPlayComplete,我在MaoscBufferCopied中忽略了KErrUnderflow和KErrAbort错误。在MaoscBufferCopied和MaoscOpenComplete也处理了KErrInUse错误。经过上面的处理,我的程序已经可以安全的应对来电、短信、切换程序等特殊情况了。
作者简介:姓名:冯兆麟网民:SimbaE-mail:kingsimba@tom.com个人主页:www.fsgame.net
