活动对象框架原理
一、概述:
Symbian OS是一个多任务的操作系统,那么为了实现多任务,同时使系统能够快速响应,高效的进行事件处理,并减轻应用程序员的工作负担(申请大多数耗时的操作(例如文件系统)由服务提供器来完成,服务提供器完成程序员提交的请求后,将会返回给程序员一个成功或失败的信号。),Symbian OS特意引入了活动对象的概念。
服务提供器API具有函数的异步和同步版本,供客户应用程序使用。所谓同步是指,客户提交请求后,处于等待状态,等待服务提供器返回成功或失败的信号后,然后在进行其他操作;所谓异步是指,请求完成,即返回信号之前,调用者也许会继续执行其他的处理,或者只是简单的等待。在这里的等待,也可以称为“阻塞”,信号就是一个事件,我们的代码就是事件驱动的。为了实现多任务,一般我们使用异步API。
一般操作系统为了实现多任务,往往使用多线程实现,当然,Symbian也是支持多线程的。但是,在同一个线程中运行的活动对象之间进行切换的代价要比线程上下文的切换代价低,这使得对于各种资源比较紧张的Symbian OS来说,使得活动对象更适合事件驱动多任务。
注意:
(1)线程间上下文切换和同一线程的活动对象之间传递控制权,在速度上的差别可能会有10倍之差,另外,一个线程大约在内核中有4KB的空间开销,在用户空间上有8KB的用于程序栈的空间开销,而一个活动对象的开销可能只有几百字节,甚至更小。
(2)虽然在一个线程内的活动对象是非抢占式地协同运行的,但在它们所在的线程却是抢占式调度的。
二、概念:
一个活动对象必须派生自基类CActive
class CActive : public CBase
{
public:
enum Tpriority
{
EPriorityIdle = -100;
EPriorityLow = -20;
EPriorityStandard = 0;
EPriorityUserInput = 10;
EPriorityHigh = 20;
}
public:
IMPORT_C ~CActive ();
IMPORT_C void Cancel ();//删除未完成请求的函数
……
IMPORT_C void SetPriority (TInt aPriority);
Inline TBool IsActive () const;
……
protected:
IMPORT_C CActive (TInt aPriority);
IMPORT_C void SetActive ();
virtual void DoCancel () = 0;//两个纯虚函数,继承类必须实现它们
virtual void RunL () = 0;//处理函数
IMPORT_C virtual TInt RunError (TInt aError);
public:
TrequestStatus iStatus;//代表请求状态
……
private:
TBool iActive;
……
}
通过上面的CActive声明可以看出:活动对象和线程类似,构造时也会有一个优先级值来决定它们如何被调度,通常为活动对象提供一个标准优先级EPriorityStandard。当活动对象响应的异步服务完成时,就会产生一个事件。活动调度器会侦测到事件,并决定每个事件对应的是哪个活动对象,然后调用恰当的活动对象去处理事件。当活动对象处理事件时,直到事件处理函数返回到活动调度器,该对象都是无法被抢占的,也就是说,RunL()事件处理函数是一个原子操作。
在Symbian OS中,活动对象相互协作并顺序的实现多任务,也不需要对共享的资源进行同步保护。另外,因为活动对象在同一个线程中运行,所以可以更容易地共享内存和对象,尽管活动对象存在于同一线程,但它们仍然是各自独立运行的,这就好像同一个进程中的线程是独立运行的一样。
三、关于活动对象基类CActive的几点说明:
参照上面CActive的声明
1、必须在发布异步请求后调用SetActive(),否则活动对象规划器在搜索已完成的活动对象时忽略它,从而导致错误。需要说明的是,在CActive这个基类中,并没有任何实际的函数用来发布异步请求,我们自己必须编写这种函数,通常取名为StartL()。
2、DoCancel()是个纯虚函数,必须实现该函数以提供未完成请求所需的功能。但是,需要注意:绝对不应该直接调用该函数,应该总是使用Cancel(),该函数调用DoCancel(),同时确保设置必须的标志,从而表明请求已完成。
3、RunL()是原子操作,当它被活动规划器调度后,相同线程里,其他任何RunL()都不可以运行,直到这一RunL()完成并返回,因此该方法必须简短,否则,用户就会感到等待事件较长,手机好像死机了一样。
4、如果RunL()异常退出,则调用RunError()(由活动规划器调用),它为活动对象提供处理自身错误的机会。如果能够处理错误,RunError()就应该返回KErrNone;否则,它应该只是返回作为参数传递的错误码,在这种情况下,将错误传递到活动规划器的Error()函数,默认行为是导致严重错误。
5、iStatus实际上只是一个封装的整数,用于表示异步服务提供器返回的状态或错误码。活动对象发出请求后,服务提供器的第一个任务是将iStatus设为KrequestPending,当请求的服务完成时,服务提供器将iStatus的值设为KErrNone(如果请求成功完成)或错误码。
四、活动对象的实现步骤:
1、构造
活动对象几乎总是要使用二阶段构造,因为活动对象通常需要连接到它们的异步服务提供器,这个连接过程可能失败。
void CCsvFileLoader::ConstructL (const TDesC& aFileName)
{
iFileName = aFileName;
//RFile iFile
User::LeaveIfError(iFile.open(iFs , iFileName , EFileRead));
//RTimer iTimeWaster
User::LeaveIfError(iTimeWaster.CreateLocal());
CActiveScheduler::Add(this);
//活动对象加到活动规划器中,这条语句也可以放在第一阶段构造函数中
}
选择最适合活动对象的优先级,该优先级只会影响活动对象在活动规划器的列表中的顺序,实际开发时,很少使用EPriorityStandard以外的优先级。
CCsvFileLoader::CCsvFileLoader(RFs& aFs , CElementList& aElementList , MCsvFileLoaderObserver& aObserver) : CActive (EPriorityStandard)
{
……
}
通过句柄RTimer和RFile链接到所需的两个异步服务器。
警告:必须将活动对象都添加到活动规划器中,并且只能添加一次。添加失败将产生请求迷失的严重错误。
2、启动活动对象
void CCsvFileLoader::Start( )
{
TInt delay = (iFileName.Size() % 10) * 100000;
iTimeWaster.After(iStatus , delay);
SetActive();//将iActive设为Etrue
}
异步服务提供器(运行在另一线程或进程中)将通过完成下面两件事来用信号通知线程(活动对象所属的线程)。
(1)增加线程的信号量(这个信号量,一般情况用不到。只是当活动对象所属的线程被挂起时,通过增加信号量重新唤醒这个线程,而线程的挂起通过函数WaitForAnyRequest())。
(2)将给定的TRequestStatus设置为不同于KRequestPending的值(如果一切运行良好,则很有可能是KErrNone)。
3、RunL()
这个函数比较复杂,它将执行大量任务:
(1)决定下一次迭代应该做什么(加载数据或浪费时间)
(2)检查最后一次迭代状态,并且将这个状态报告给观察器
(3)处理所有加载的数据
一般活动对象的这个RunL()方法希望完成上述的一项或几项工作。
在这里还可以再次调用Start(),也就是说可以再次发布异步服务请求。
4、在RunError()中处理错误
活动对象完全允许RunL()异常退出,结尾是“L”表明了这一点。如果该函数确实异常退出,则它异常退出时的错误码将被传递到RunError()。
TInt CCsvFileLoader::RunError(TInt aError)
{
iObserver.NotifyLoadCompleted(aError , *this);
return KErrNone;
}
这里的错误处理很简单,仅仅将错误传递给了观察器。
5、删除未完成的请求
所有的活动对象都必须实现一个DoCancel()方法,用于删除任何未完成的请求。
void CCsvFileLoader::DoCancel()
{
if(iWastingTime || !iHaveTriedToLoad)
{
iTimeWaster.Cancel();
}
}
CActive::Cancel调用DoCancel(),绝对不允许重写CActive::Cancel()自身(它无论如何都不是虚函数),因为该函数完成了大量重要的工作。
(1)检查活动对象是否实际上处于活动状态。如果不是,它就会返回,而不作任何事情。
(2)调用DoCancel()
(3)等待请求完成,这必须尽可能完成(原始请求可能在删除它之前就已经完成)
(4)将iActive设为假。
注意:可以重写DoCancel()方法,但不可以重写Cancel()方法,这个方法会自动调用DoCancel(),另外,我们也不能直接调用DoCancel()方法,这一点也是很重要的。
6、析构函数
CCsvFileLoader::~CCsvFileLoader()
{
Cancel();
iFile.close();
iTimeWaster.Close();
}
任何活动对象析构函数的第一步都是调用Cancel()删除任何未完成的请求。如果删除一个带有未完成请求的活动对象,则产生一个请求迷失的严重错误。
必须关闭异步服务提供器的任何句柄,从而避免资源泄漏。
基CActive析构函数将自动调用Deque(),从活动规划器列表中移除活动对象。
7、启动活动规划器
UI框架将自动创建、安装和启动活动规划器。因此,如果不打算编写.exe(控制台应用程序或Symbian OS服务器)或DLL(需要显式启动活动规划器),则可以省略这些步骤。
启动活动规划器前,必须完成如下步骤:
(1)实例化活动规划器
(2)将其安装到线程中
(3)创建一个活动对象并添加到活动规划器中
(4)发出一个请求
void DoExampleL()
{
CActiveScheduler* scheduler = new(ELeave) CActiveScheduler;
CleanupStack::PushL(scheduler);
CActiveScheduler::Install(scheduler);
CElementsEngine* elementEngine = CElementsEngine::NewLC(*console);
elementEngine->LoadFromCsvFilesL();//发出请求
CActiveScheduler::Start();//启动活动规划器
CleanupStack::PopAndDestroy(2 , scheduler);
}
五、常见的活动对象错误:
1、启动活动对象前忘记调用CActiveScheduler::Add()
2、在发布或重新发布异步请求后没有调用SetActive()
3、将相同的iStatus同时传递给两个服务提供器(因此在相同的活动对象上有多个未完成的请求)
08-16