Symbian手记【二】 —— Symbian对象构造

【二】 Symbian对象构造

C++的纯手工内存管理,确实是一个万恶之源。在对象构造时,有一个著名的内存泄漏隐患问题。比如一个类如下:
class A
{
public:
    A()
    {
        a1 = new T1();
        a2 = new T2();
        ...
        an = new Tn();
    }

private:
    T1 * a1;
    T2 * a2;
    ...
    Tn * an;
}
当你调用 new A() 进行分配的时候,一旦失败,可能导致内存的泄露。比如系统正吭哧吭哧分配到了a18,失败了,抛出异常了,或者返回空值了,前面a1 - a17个对象,就彻底成了没娘管的娃,一并泄漏了出去。一个解决策略是,管好每一个分配过的对象,一旦有问题,清空一切。比如 a18分配失败了,delete掉a1 - a17。且不说这么做有没有其他问题,但是这份苦力,估计就没多少人能够承受。

二阶段构造

为了解决对象分配的问题,Symbian琢磨了所谓的二阶段构造法,它是一个pattern,关键在于将对象中栈数据的初始化和堆对象的分配过程隔离开来。一个标准的二阶段构造类如下:
class A
{
public:
    ~A();
    static A * NewL();
    static A * NewLC();

private:
    A();
    void ConstructL();
}
其中内容,自动构造的每个Symbian C++类中都会有。在构造函数中,只能够执行赋值等操作,就是初始化栈中内容,整个操作不会涉及到堆中对象的分配。所有需要分配的堆中对象,推迟到ConstructL函数中进行。NewL和NewLC提供一个封装,将构造函数和二阶段构造函数封装一起。当然,仅通过这样的方式,无法解决内存泄漏的问题,一个核心机制,是清理栈,即CleanupStack。

清理栈

CleanupStack是单件的形式呈现在程序中,GUI的程序系统为你构造好了,Console的需要人肉一个。当你在一个函数中,new了一个对象,你需要先把它push到CleanupStack中,才能调用其带L的方法,并在调用完成后将它pop出CleanupStack。一旦L函数执行失败,Leave了,并在上层用TRAP宏抓到这个Leave错误,系统会自动释放存放在CleanupStack中,还没来得及pop的对象,以保证所有资源都不会泄漏。要做到这点,有两个需要解决的问题,一是如何不在人肉delete的情形下自动析构,第二个是如何知道析构栈中多少个对象。
解决第一个问题,关键就是利用栈对象的析构函数,每个push到CleanupStack中的对象,都被一个栈对象TCleanupItem封装了一下,作为一个成员变量TAny* iPtr存放起来。当这个栈对象被释放,会调用其析构函数,析构函数中包含delete iPtr的调用,如此,自动析构得以完成。当然,为了保持其通用性,TCleanupItem其实不是直接delete,而是通过一个TCleanupOperation的对象来实现的,这个对象负责在其析构函数中delete iPtr,当然,除了delete,不同的TCleanupOperation还可以是iPtr->close,iPtr->release之类的,这样可以将其机制轻松的扩展开来。
#define TRAP(_r, _s) \
{ \
TInt& __rref = _r; \
__rref = 0; \
{ TRAP_INSTRUMENTATION_START; } \
try { \
__WIN32SEHTRAP \
TTrapHandler* ____t = User::MarkCleanupStack(); \
_s; \
User::UnMarkCleanupStack(____t); \
{ TRAP_INSTRUMENTATION_NOLEAVE; } \
__WIN32SEHUNTRAP \
} \
catch (XLeaveException& l) \
{ \
__rref = l.GetReason(); \
{ TRAP_INSTRUMENTATION_LEAVE(__rref); } \
} \
catch (...) \
{ \
User::Invariant(); \
} \
{ TRAP_INSTRUMENTATION_END; } \
}
另一个问题解决之道,就是记录一个level,在函数执行前放入一个标记,一旦有错误,就消除在此标记后push进来的对象。这个机制的维系,隐藏在TRAP宏中。当你写TRAP(err, DoitL())时,TRAP会在调用DoitL()前,调用User::MarkCleanupStack()加入一个标记,并在调用结束后利用User::UnMarkCleanupStack检查并且消除该标记。放一个标记在这里,一旦你多push了少pop了,或者少push了多Pop了,都会触发异常,谨防顺手写错。而在执行函数DoitL()过程中,一旦发生Leave错误,在User::Leave()之类的函数中,都会找到最后标记的位置,清除标记后push的所有对象。
由于栈和函数调用都属于先进先出的,整个机制是可以嵌套进行的。只要你TRAP了Leave错误,所有资源都会被保证析构(如果没有TRAP,天皇老子都帮不了你...)。这种半自动半人肉的内存管理方式,虽然不能帮助复杂的内存对象生命周期的维护,但至少可以保证每一个资源在异常时正常释放,这一点在嵌入式系统中比一般系统显得更为重要(因为内存紧张,分配不成功是常态...)。但人肉方式总归是要人来解决的,不论CleanupStack多么的好,它只是一个pattern,它不能自动去做一些事情,还是需要开发人员主动的push,pop,leave,以及TRAD,少了哪一样,整个机制全部白搭。

Symbian的异常处理

Symbian的异常处理,就是著名的Leave机制,如果你打开TRAP宏,便惊奇的发现,所谓Leave,只不过老瓶装新酒,它只是给C++的异常机制,穿了个丁字裤,还是超节约布料型的。你可以将所谓的TRAD看成是catch,将Leave看成throw,将带L的函数,看成是throw exception的函数,再将err code当作是异常类型,整个Leave机制,就和C++的异常匹配上了。
当然,之所以称为老瓶装新酒,那么就有一些可以称为新的琐碎事。首先,就是对CleanupStack的维系。在TRAP宏和User::Leave中,包含了对CleanupStack的标记的管理和资源清理,没有它们,CleanupStack这套东东,就该另辟蹊径了。
而另一方面,就是对标准异常和无法估量的异常进行了分门别类的处理。C++和.Net不一样,异常都是不同根的,我们往往需要用catch(...)去处理一些杂类的状况。在TRAD中,对所以Symbian中的异常进行了分类。一类是派生自XLeaveException的异常,它们是整个Symbian的Cleanup以及Leave的管辖范围,只有在触发此类异常的时候,所谓的自动释放内存、Leave才能发挥作用;而其他所有的异常,都被归类异类,一旦发生,直接User::Invariant()来安乐死。所以,你明明是TRAP了,在读到空指针等错误发生的时候,它完全不起作用,程序直接崩溃,因为,这超出了它的能力范畴。
除此之外,Symbian开始支持标准的C++异常了,但对于一个合格的Symbian开发者而言,了解这些,还是有益无害的。。。

posted on 2009-05-28 02:26  duguguiyu  阅读(3772)  评论(5编辑  收藏  举报

导航