symbian学习笔记三(内存管理)

Aug 1st, 2011 | Posted by

本文内容非原创,属于网上资源的整理。

========================================

二阶段构造
问题1:为什么需要二阶段构造?
首先考虑如下的语句:
  1. CClassName* ptr = new (ELeave) CClassName();  
在内存有足够空间的情况下,代码首先在堆上分配一个CClassName类型的对象,并将地址赋给ptr指针,然后调用类的构造函数初始化这个对象。
这样,如果类的构造函数出现了异常,则会发生问题,这种异常发生时没有任何指针指向成功分配给CClassName对象的内存区域,因此这些内存成为孤立内存,发生内存泄漏。这就引出了symbian内存处理的一个重要规则:构造函数绝对不能异常退出。
问题2:为什么二阶段函数能够避免内存泄漏?
二阶段构造函数,顾名思义就是将一个对象的构造分为两个阶段:
第一个阶段是常规的的构造函数,在该构造函数中,没有可能导致异常退出的代码;
第二个阶段是可能会产生异常的构造阶段,实现为函数ConstructL();
这样,对象的构造过程就应当包括了如下的代码:
  1. CClassName* self = new (ELeave) CClassName();   
  2. CleanupStack::PushL(self);   
  3. self->ConstructL();   
  4. CleanupStack::Pop(self);  
这样的构造方式为什么就能够避免内存泄漏呢?下面我们来逐行分析代码:
  1. CClassName* self = new (ELeave) CClassName();  
重载的运算符new首先将内存分配给新的self实例,如果分配失败,那么程序异常退出,如果成功给新的对象分配了内存,那么接着执行不会异常退出的第一阶段构造函数;
  1. CleanupStack::PushL(self);  
接着我们将本地指针self推入清除栈,因为下面要调用可能发生异常的退出函数。
  1. self->ConstructL();  
如果该二阶段构造函数在执行时异常退出,那么新的CClassName的指针由清楚栈负责清楚,避免了内存泄漏;另外,如果该函数没有异常退出,则拥有了一个完全构造的CClassName实例。
  1. CleanupStack::Pop(self);  
安全的将本地指针从清除栈中弹出;
每实例化一个对象就要写上述代码确实有些啰嗦了,Symbian OS为了简化实例化的步骤,又引入了NewL(),NewLC()两个函数(其实也可以写成一个NewL(),然而大家都比较推崇同时创建NewL()和NewLC()),其具体的实现方式见问题3;
问题3:如何在新的类中创建二阶段构造函数?
.h头文件:
  1. Class CClassName : public CBase   
  2. {   
  3. public:   
  4.        static CClassName* NewL();   
  5.        static CClassName* NewlC();   
  6.        ~CClassName();   
  7. private:   
  8.        CClassName(); //第一阶段构造   
  9.        void ConstructL(); //第二阶段构造   
  10. ……   
  11. }  
cpp源文件:
  1. CClassName* CClassName::NewL()   
  2. {   
  3.        CClassName* self = CClassName::NewLC();   
  4.        CleanupStack::Pop(self);   
  5.        return self;   
  6. }   
  7. CClassName* CClassName::NewLC()   
  8. {   
  9.        CClassName* self = new (ELeave) CClassName();   
  10.        CleanupStack::PushL(self);   
  11.        self->ConstructL(); //二阶段构造   
  12.        return self;   
  13. }   
  14. void CClassName::ConstructL()   
  15. {   
  16. /**************可能产生异常的代码************/  
  17. }  
Why Memory Management
    Symbian OS本身就是为内存资源受限的设备开发的,应用程序运行过程中很可能碰到内存用光,或者硬件资源不可用的情况。而这种exceptions是通过修改程序无法解决的,所以遵守以下几条:
  • 尽量不要使用不必要的RAM
  • 尽早释放资源,如文件server等
  • 当你每次申请内存时,都须准备处理out-of-memory错误
  • 当 out-of-memory错误发生时,返回到一个stable的状态,并释放所有期间申请到的资源
 Stack and Heap
    Stack:默认大小8kb,自动删除,如 TInt i = 0;
    Heap :至少0.5Mb,由程序员手动删除,如 CMyObj* obj = new (ELeave) CMyObj;
Leaves
    首先介绍Conventional C++ Memory Management,在Symbian看来,这是非常低效率的。
  • NULL Pointer Checking  if ((myObj = new CMyObj( ) ) == NULL) { //Error Handling }
  • ANSI C++ Exeption Handling   try { //throw an Exception } catch (int e) { //Error Handling }
    在Symbian中推荐采用Leave,如果内存或者资源不能分配到,这个代码就会Leave,沿着Call Stack,直到操作系统或者在某个函数中被Handle掉。
    所有可能Leave的函数最好以L结尾,保证该函数的用户知道这个函数可能Leave。
    Leave的例子:
  • 动态内存分配: return new (ELeave) TUint8[1000];
  • 产生一个Leave:User::Leave(KErrNotFound);
  • 内存不足时Leave:User::LeaveNoMemory();
  • NULL的时候Leave:User::LeaveIfNull(aNotify);
  • 当发生错误时Leave:RFs fs; TInt err = fs.Connect(); User::LeaveIfError(err);
    处理Leave:
    操作系统有默认的处理Leave的方式:
  • 在程序启动过程中:直接关闭应用程序。
  • 应用程序启动后:显示一个错误消息。
    开发者可以通过trap装置来处理Leave。TRAP(_r, _s)和TRAPD(_r, _s),其中:
  • _r:是一个TInt类型的leave code,默认值为TErrNone。
  • _s:一系列可能Leave的C++ Statements。
  1. TRAPD(err, DoFunctionL());   
  2. if (err != KErrNone)   
  3.     { //Error Handling }   
  4. else  
  5.     { //Everything is well }  
  The Cleanup Stack
    Cleanup stack用于存储在leave发生后需要deallocating的局部变量(指针)。即:当一个函数leave了,所有在cleanup stack上的对象会被全部删除掉。
    Cleanup Stack的使用方法:
CleanupStack::PushL(ptr) :当发生leave时所有内存都会被释放
CleanupClosePushL(handle):当发生leave时这个句柄(handler)会被关闭
CleanupStack::Pop(pointer):第一个元素出栈
CleanupStack::PopAndDestroy(pointer):第一个元素出栈并释放内存
    如果一个函数可能leave,检查一下两种情况:
  • 如果leave了,是否所有在堆(heap)上的元素都在cleanup stack中了
  • 如果没有leave,你是否自己恰当地将他cleanup了
  1. CMyClass* CMyClass::NewL(TInt aBufSize)   
  2.    {   
  3.    CMyClass* self = new (ELeave) CMyClass;   
  4.    CleanupStack::PushL(self);   
  5.    self->ConstructL(aBufSize);   
  6.    CleanupStack::Pop(self);   
  7.    return self;   
  8.    }  
    如果某个函数会在cleanup stack上留下一个对象,那么他必须以C结尾。
Two Phase Construction
    C++构造函数一定不能leave。所有内存和资源的分配应该在第二阶段构造函数ConstructL( )中完成。
编码指南,所有用户定义的C类必须:
  • 定义NewL和NewLC函数为public static
  • 定义ConstructL和C++ Constructor为private
Best Practise
     Construction的规则:
  • 默认的C++构造函数中不能含有可能leave的代码
  • 可能发生leave的函数必须在ConstructL中被调用
  • 如果基类也有ConstructL,必须首先调用,不要忘了explicit scoping
    Destruction的规则:
  • C类必须在析构函数中删除它自己所包含的对象
  • 在删除一个对象后,把它的指针设为NULL
  • 不要删除不是本类所拥有的对象
  • 在reallocation前首先删除对象,并且将其指针设为NULL
    Further Discussion:
  • Preserve Stack Memory:每个进程只有8K,以引用的方式传递参数,大的对象放在堆上
  • Preallocation vs last moment allocation:一般的原则是只在使用前分配资源并且在使用后马上释放。但是preallocation的好处是节约处理时间,并且在没有内存的情况下照常运行(资源已经分配到了)
  • where to put trap harness:最基本的情况是依靠GUI应用程序的框架。根据应用的不同,可以自定义粒度。
  • Error Code Returns vs. leaving functions:在执行某个处理前检测是否会出现问题,如下代码:
                                    User::LeaveIfError(fs.Connect());
Memory Leaks
    如果你的程序有内存泄露,在模拟器上关闭时会crash。尽早发现并解决你的内存泄露,因为你可以追查到你可能导致内存泄露的代码改动。如果实在找不到,可用下面方法:
    Heap Balance Checking:
  • _UHEAP_MARK
  • _UHEAP_MARKEND
    用上述这两个宏放在你要检查的代码的开头和结尾,如果发生panic,则说明这段代码中发生了内存泄露。可以嵌套使用。
Panics 
    Panic是一个未经处理的exception,暗示着一个无法解决的错误。
一般程序有以下三类错误:
  • 程序错误:如引用一个超过数组范围的元素
  • 环境错误:内存、磁盘空间不够,或缺少其他资源等
  • 用户错误:输入错误数据
    可以使用trap和cleanup stack技术来解决环境和用户错误,但是对于第一类的程序错误,我们无法恢复,最好是使用User::Panic()函数,它带有两个参数,第一个是string,第二个是Tint。
内存管理的二十二条军规
1、C类必须有析构函数,这是CBase的一个虚函数。
2、C类的构造函数和ConstructL()必须为protect或private类型的成员函数
3、在C class中必须有一个NewLC()函数,除非它是嵌套类。NewL()是可选的,并且总是根据NewLC()来实现。
4、NewL()和NewLC()在c class中必须是static函数。
5、C类通过指针和引用来传递。
6、拷贝构造函数在symbian中没有用。
7、不要一定在析构函数中删除类的成员对象。(生命期结束即可删除)
8、析构函数中必须对对象进行if检查。即

  1. if(iObject) delete iObject;iObject = NULL;  

9、R类没有明确的构造、析构或拷贝构造函数以及赋值操作。
10、delete a;a=NULL;a=b;标准重新分配过程。
11、任何可能导致异常退出的函数皆加L后缀。
12、不要删除非拥有对象(也就是,那些仅仅只使用的对象)
13、分配动态数组前定义一个合适粒度。
14、把new换成new(ELeave).
15、if(函数不能异常退出&&要自己处理错误时)使用TRAP&&不要过多嵌套。
16、if(aObject被一个自动变量指针引用&&将进行一个可能在aObject生存期内Leave的操作)
CleanupStack::PushL(aObject);
17、决不能把一个i前缀的成员变量PushL入清理栈。
18、构造函数决不能Leave,把可能异常退出的语句放到ConstructL()中去。
19、Symbian的默认栈容量为8k,小心使用递归。
20、TBuf的长度最好不超16,必要的情况下用HBufC代替TBuf.
21、使用__UHEAP_MAEK 宏来检测你的内存状况。
22、尽可能早的删除一切失去使用价值的东西,不要等到函数尾部(自动变量)或在析构函数中才删除(成员变量)。

Tags:
No comments yet.