Symbian清除栈的深入分析

来源:互联网 发布:淘宝女鞋分类 编辑:程序博客网 时间:2024/06/05 15:51
 

Symbian清除栈的深入分析

从表面看起来清除栈的概念还是很容易理解的,使用起来也是比较方便。市面上关于Symbian的大部分书籍都对Symbian清除栈的使用有详细的介绍。可是,到现在,很少有资料详细涉及Symbian清除栈工作原理。我们接触最多的资料一般是《Symbian OS Explained Effective C++ Programming for Smartphones》这本书,但是这本书似乎也是给人一种戛然而止的感觉,使我们只能局限于Symbian应用程序的开发,却不能深入了解Symbian操作系统本身。于是,我想利用业余时间和更多的人一起探讨一下Symbian清除栈的工作原理。当然,也不仅限于此,后续我还会写更多的文章和大家一起探讨Symbian的核心工作原理,甚至一起对Symbian的内核进行分析。我希望能够吸引更多的人参与Symbian的学习和研究过程,来共同壮大Symbian开发阵营。

下面我们来探讨Symbian清除栈的工作原理,这是我写的第一篇关于Symbian操作系统工作原理的文章,所以还是有一些表述上的问题,希望大家多多给出意见。另外,也希望读者有一定的Symbian和C++基础,否则还是会影响阅读的。

清除栈实际上是一种半自动的内存回收机制,Symbian为了达到这个目的,做了很多工作,甚至付出了不少代价。与之相关的有清除栈本身的框架、TRAP宏及Leave机制。本文先从Symbian清除栈框架介绍,如果阅读过《Symbian OS Explained Effective C++ Programming for Smartphones》这本书的话,可以直接从本文图1以下的位置开始阅读。

1.1.1.   清除栈的框架:

清除栈保存了在发生异常退出时会被销毁的对象的指针,而这些对象是由TRAP宏来标定为不同的异常退出等级,即TRAP宏是可以嵌套的,每一级嵌套的TRAP宏之内如果发生异常退出,则只有该TRAP宏内推入清除栈的对象才会被销毁,下面的代码说明了这个问题:

CServer2* NewServerL(const TDesC& aServerName)

{

// Install the active scheduler;

    CActiveScheduler* scheduler = new(ELeave) CActiveScheduler;

    CleanupStack::PushL(scheduler);

    CActiveScheduler::Install(scheduler);

     

    CSmallServServer* server = CSmallServServer::NewLC();

    User::LeaveIfError(User::RenameThread(aServerName));

    RProcess::Rendezvous(KErrNone);

    TRAPD(r, server->StartL(aServerName);

if (r != KErrNone)

       {

       //…

       }

    CleanupStack::Pop(2, scheduler);

    return static_cast(server);

    }

   

TInt ThreadStart()

    {

    __UHEAP_MARK;

    TInt err = KErrNone;

   

    // set cleanup stack manually

    CTrapCleanup* cleanup = CTrapCleanup::New();

    CServer2* server = NULL;

    if (cleanup)

       {

       TRAP(err, server = NewServerL(KServer));

       }

    else

       {

       err = KErrNoMemory;

       }

if (err == KErrNone)

       {

       // start the active Scheduler

       CActiveScheduler::Start();

       User::InfoPrint(_L("after"));

       }

    delete server;

    delete CActiveScheduler::Current();

    delete cleanup;

   

    __UHEAP_MARKEND;

    return err;

    }  

在NewServerL函数中有TRAP宏存在,如果该宏的中StartL函数发生异常退出,则NewServerL已经推入清除栈的scheduler和server并不受到任何影响,但如果NewServerL函数中User::LeaveIfError产生异常退出,则已经推入清除栈的scheduler和server将由清除栈自动析构。

那么清除栈到底如何实现这些功能呢?我们先来看看清除栈的创建过程,在一般的GUI应用程序中,我们通常不关心清除栈的创建,可以直接使用,这是因为GUI的框架已经为我们创建好了。但有时候我们需要手动创建清除栈,比如创建一个新的非GUI进程,并且这个进程中的主线程必须用到清除栈。因此,Symbian提供了CTrapCleanup类用于清除栈的初始化。

我们继续从上面代码中来看看清除栈是如何初始化的,可以看出上面这段代码是一个单独的进程启动过程,其中有这样一段代码。

CTrapCleanup* cleanup = CTrapCleanup::New();

delete cleanup;

经典的清除栈框架对这段代码的描述是这样,当创建CTrapCleanup类对象的时候,CTrapCleanup::New()中发生以下事件:

1.线程当前的异常处理程序被保存起来。

2.在CTrapCleanup对象中创建一个名为iHandler的TCleanTrapHandler类对象(它持有一个包含实际清除栈实现代码的CCleanup对象)。

3.调用User;;SetTrapHandler(),将TCleanupTrapHanlder对象作为线程中新的异常处理程序。

 

图1 清除栈结构

当调用CleanupStack::PushL()或CleanupStack::Pop()时,这些静态函数会调用User::TrapHandler()来获取已经安装TCleanupTrapHandler对象,从而可以访问CCleanup对象(见图 1)。正如前面所说,CCleanup对象是实现清除栈代码的核心类,它真正的负责CleanupStack类中静态函数的实现。

现在我们来看CCleanup及其他一些辅助类如何实现清除栈的功能,以CleanupStack类中的三个推入函数为例来逐渐揭开清除栈的真相,这三个函数在Symbian应用开发过程中是再熟悉不过的了,分别是:

IMPORT_C static void PushL(TCleanupItem anItem);

IMPORT_C static void PushL(TAny* aPtr);

IMPORT_C static void PushL(CBase* aPtr);

这三个函数将分别调用CCleanup对象中的以下三个函数:

EXPORT_C void CCleanup::PushL(TCleanupItem anItem)

{

    …// check consistency

    iNext->Set(anItem);

    iNext++;

    if (iNext + 1 >= iTop)

       {

       …// reallocate memory to cleanup stack

       }

    }

EXPORT_C void CCleanup::PushL(TAny* aPtr)

{

    PushL(TCleanupItem(User::Free, aPtr);

    }

EXPORT_C void CCleanup::PushL(CBase* anObject)

{

    PushL(TCleanupItem(TCleanupOperation(doDelete), anObject);

    }

可以看到在CCleanup对象中也有类似的三个PushL重载函数,我们先来看看CCleanup::PushL(TCleanupItem anItem)函数,因为CCleanup::PushL(TAny* aPtr)和CCleanup::PushL(CBase* anObject)在内部实际上也是在调用它。

首先,该函数内部的iNext和iTop是用来控制清除栈指针的,这和数据结构中栈的处理是一样的,我们来看看e32base.h中iNext和iTop的定义:

  //Pointer to the top of the cleanup stack.

  TCleanupStackItem* iTop;

  //Pointer to the next availaible slot in the cleanup stack.

  TCleanupStackItem* iNext;

请注意注释部分,iNext原来就是指向清除栈的下一个空闲槽,该空闲槽的类定义是TCleanupStackItem,且该类对象的集合组成清除栈。那么CCleanup::PushL(TCleanupItem anItem)函数就是在清除栈中将所传过来的对象放入空闲槽(Set(anItem))并且“推入”清除栈(iNext++),即让iNext指向清除栈下一个空闲槽。

现在我们已经知道在Push过程中清除栈是如何被控制的,但是用户所传过来的对象是如何保存的呢,这就需要TCleanupItem类,该类是CCleanup::PushL(TCleanupItem anItem) 函数的参数类型,并且被放入清除栈的空闲槽(Set(anItem)),我们继续从e32base.h寻找TCleanupItem类的定义,很幸运,发现它是这样定义的:

typedef void (*TCleanupOperation)(TAny*);

class TCleanupItem

    {

public:

    inline TCleanupItem(TCleanupOperation anOperation);

    inline TCleanupItem(TCleanupOperation anOperation,TAny* aPtr);

private:

    TCleanupOperation iOperation;

    TAny* iPtr;

    friend class TCleanupStackItem;

    };

原来TCleanupItem将用户要推入清除栈的对象及其销毁处理的方法分别放入iPtr和iOperation函数指针中,在发生异常退出或是CleanupStack::PopAndDestroy就可以将所指向的对象“就地处理”了(后面还会详细介绍)。

说到这里,对清除栈的Push机制,我们就有了一个比较完整的了解,再来看CCleanup::PushL(TAny* aPtr)就比较容易理解了。该函数将User::Free作为对象销毁的处理方法和对象的指针一起放入TCleanupItem对象并推入清除栈的空闲槽,在发生异常退出或是CleanupStack::PopAndDestroy时就可以调用User::Free来释放TAny*指向对象的内存空间了。

同理,CCleanup::PushL(CBase* anObject)函数也是这样处理的,该函数将doDelete函数指针放入TCleanupItem对象并推入清除栈的空闲槽,在发生异常退出或是CleanupStack::PopAndDestroy时就可以调用doDelete来释放CBase*指向对象的内存空间了。那么doDelete又是如何处理的呢?有些读者可能已经猜出十之八九。不过,为了表示清楚,还是列出来:

LOCAL_C void doDelete(CBase* aPtr)

    {

    delete aPtr;

    }

原来就是这样简单,因为CBase的析构函数是虚函数,从CBase继承下来的C类,只要简单的delete就可以!

我们再看看CleanupStack::PopAndDestroy()或异常退出时是如何删除清除栈对象的,iNext会调用成员函数Cleanup(),该函数会执行下面一行代码:

(*iOperation)(iPtr);

其中iOperation的定义为

typedef void (*TCleanupOperation)(TAny*);

TCleanupOperation iOperation;

可以看出,iOperation指向的函数被调用,比如说,对于CBase继承下来的C类,就会调用刚讲过的doDelete,对于TAny*就会调用User::Free()来直接释放内存,以此类推。

1.1.2.   TRAP宏

这是Symbian SDK中关于TRAP宏的定义:

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; }                           /

}

这段代码看起来似乎并不重要,但就是这段看似不起眼的代码决定了Symbian整个清除栈及其异常退出的框架。

先来看Symbian是如何实现TRAP嵌套与清除栈挂钩的:

TTrapHandler* ____t = User::MarkCleanupStack();     /

_s;                                                 /

User::UnMarkCleanupStack(____t);                    /

这里User::MarkCleanupStack()实际上还是求助于CCleanup对象的代码实现,它调用CCleanup::NextLevel()函数。

EXPORT_C void CCleanup::NextLevel()

{

    iNext->MarkLevel();

    iNext++;

    }

看起来似乎有点让人费解,怎么又在操作iNext?其实,MarkLevel()函数会将iNext指向的下一个清除栈空闲槽TCleanupStackItem对象设定为一个标记,该标记将不再会放入任何用户推入对象,仅仅作为标记,然后iNext指向再下一个清除栈空闲槽,于是新的用户推入对象只会放入该标记之上,如图2所示:

 

图2 TRAP嵌套

同样,User::UnMarkCleanupStack()实际上也是求助于CCleanup对象的代码实现,它调用CCleanup::PreviousLevel()函数,这个函数就是将iNext减一,位置回退一位并将标志所占空闲槽改为可以推入对象的空闲槽,如下所示:

EXPORT_C void CCleanup::PreviousLevel()

{

    --iNext;

if (!iNext->IsLevelMarker())

       {

       Panic(ENotEmpty);

       }

    }

这里有人就要问了,为什么iNext需要判断是否为标记?而且如果不是,竟然会将整个程序Panic!那是什么原因造成这么严重的错误?其实,这就是防止Symbian开发新手经常犯的错误,即只Push对象进入清除栈却没有及时的将其Pop出清除栈。因此,TRAP宏执行到这里时就会检查清除栈该标记以上部分是否还有未清除的对象,如果有,就说明有对象忘Pop了,于是整个程序便Panic。需要提醒的是,实际清除栈的代码远比上面描述的要复杂,其中还有许多问题需要考虑,在此就不一一描述了。

我们再来看Symbian的异常退出(Leave),异常退出机制也是清除栈实现的重要组成部分,经常使用的异常退出函数如下:

IMPORT_C static void Leave(TInt aReason);

IMPORT_C static void LeaveNoMemory();

IMPORT_C static TInt LeaveIfError(TInt aReason);

IMPORT_C static TAny* LeaveIfNull(TAny* aPtr);

这些函数都实现了如下代码:

TTrapHandler* t = GetTrapHandler();

if (t)

t->Leave(aReason);

throw XLeaveException(aReason);

TTrapHandler对象的Leave函数最终又是求助于CCleanup对象的代码,CCleanup则是这样实现的,即清除该TRAP宏中所有已经推入的用户对象:

while(!(--iNext)->IsLevelMarker())

    {

    iNext->Cleanup();

}

可以看到,如果iNext指向的不是标记,则循环进行对象的清除工作。

到此为止,似乎清除栈的相关内容我们已经介绍完了,但还有一些问题需要解释,我们看到在异常退出的那些函数里,有这样一行代码:

throw XLeaveException(aReason);

而在TRAP宏里有这样的catch模块:

catch (XLeaveException& l)                              /

        {                                                   /

        __rref = l.GetReason();                             /

        { TRAP_INSTRUMENTATION_LEAVE(__rref); }             /

        }    

原来Symbian在实现TRAP和异常退出时,还是使用的标准C++异常机制。那么就有人要问了,为什么不让Symbian应用开发人员使用呢?这可能有历史遗留方面的原因,但有一个理由很明显,如果让Symbian应用开发人员使用标准C++异常机制,一旦发生标准C++异常,谁来清除清除栈内的对象呢?显然,不能让开发人员来维护这件事情,而使用TRAP宏就可以在发生异常时强制清除所需要清除的对象。所以,Symbian为清除栈真是费尽了心机啊。

原创粉丝点击