編寫安全的Symbian C++遊戲代碼

                                                                           本文作者: 馮兆麟(Simba) ([email protected])

        本文獻給使用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 file
CAknQueryDialog* m_noMemoryDlg;
// somewhere in ConstructL
TBuf<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 here 
 default:
  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 iImage
 if (iImage->DisplayMode() != EColor4K)
 {
  // Create 4k color image
  CFbsBitmap* image = new (ELeave) CWsBitmap();
  CleanupStack::PushL(image);
  User::LeaveIfError(image->Create(iSize, EColor4K));

  // Create device
  CFbsBitmapDevice* device = CFbsBitmapDevice::NewL(image);
  CleanupStack::PushL(device);
  CFbsBitGc* gc;
  User::LeaveIfError(device->CreateContext(gc));
  CleanupStack::PushL(gc);

  // Bitblt to new color depth
  gc->BitBlt(TPoint(0,0), iImage);

  // Destroy context and device;
  CleanupStack::PopAndDestroy(); // gc
  CleanupStack::PopAndDestroy(); // device
  CleanupStack::Pop();  // image
  delete 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 image
  TUint16* addr = (TUint16*)aBmp->DataAddress(); // target bmp
  TInt line = aBmp->ScanLineLength(
   aBmp->SizeInPixels().iWidth,
   EColor4K) / 2;
  TInt line2 = iImage->ScanLineLength(  // line length in 16bit word
   iImage->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 interest
  TUint16* 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 display
 if (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 DSA
 iDirectScreenAccess->StartL();

 // Get graphics context for it
 iGc = iDirectScreenAccess->Gc();
 
 // Get region that DSA can draw in
 iRegion = iDirectScreenAccess->DrawingRegion();
 
 // Set the display to clip to this region
 iGc->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 bitmap
 iGc->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 underflow
  iBaseSamplesPlayed = iSamplesWritten;

  // Stop and restart
  iStream->Stop();
  Cancel();
#ifdef RATE_16K
  iStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate16000Hz,
          TMdaAudioDataSettings::EChannelsMono);
#else
  iStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate8000Hz,
          TMdaAudioDataSettings::EChannelsMono);
#endif
  iStream->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錯誤。
經過上面的處理,我的程序已經可以安全的應對來電、短信、切換程序等特殊情況了。


作者簡介:
姓名:馮兆麟
網民:Simba
E-mail:[email protected]
個人主頁:www.fsgame.net  cosoft.org.cn/projects/fslib

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章