SymbianOS是一个非常依靠异步操作的系统。事实上,所有的系统服务都是通过服务方提供的,这是为了在他自己的过程中高可靠地操作。(一个过程就是一个存储保护单元,它把程序线程与别的过程隔离开。)服务提供方API典型地对于他们客户端应用程序可获得的工程有异步和同步版本,但为了避免阻塞应用程序的用户接口,你将经常使用异步版本。大部分对时间有要求的操作都被作为请求发给某个异步服务提供者,如文件系统(这将在下一节介绍)。服务需要函数立即返回,但是请求本身在后台被处理。这意味着程序需要在以后请求完成后接收某个通知,而且同时需要能同时与别的任务对象用户的输入响应或者更新显示协同工作。
这样的异步系统在现在的计算中很普遍,而且有很多方式实现它:
一个流行的方式是在有优先权的系统中使用多线程:产生一个新的执行线程来处理每个异步任务,发出请求和等待完成—通常通过选举请求提供者来看请求是否已经完成。虽然这个方法可能在SymbianOS系统里,但是效率很低,而且非常让人失望。
[注意]:更多实现多线程的信息能在SDK文档种关于RThread的API描述部分找到,但是这个方法仅仅被使用,更多地解释超出的本书的范围。
另外一种供选择的方法,而且受到SymbianOS应用程序青睐的是使用协作多任务。在有优先权的系统中,操作系统的内核决定了现在的线程得到了足够的处理器的使用,而且允许另外一个线程先占它,通常在它完成处理之前。协作(或者无优先权)多任务系统要求当前执行的任务放弃处理器的使用,而且允许其他的任务来执行。所有的应用程序处理都发生在一个单独的线程里,所以多任务必须协作。
协作多任务通常实现像某种等待或者消息循环,而且是SymbianOS的情况。
这个循环从等待开始—一个简单的应用程序线程被阻塞住,直到一个服务请求完成,而且服务提供者(“server”),一个在单一线程(或者进程)执行的,表明已经完成,而且增加了一个信号量。SymbianOS不采取投票的方式,因为这样浪费处理器的循环时间和电池的能量。也注意到非常重要的是在开始循环之前至少有一个请求,否则等待信号不能被唤起!
一旦等待循环队列被唤起,就可以继续执行。系统通过每一个明显的任务请求来寻找,直到它找到了一个完整的任务然后执行它的处理方法。然后这个循环等待下一个请求的完成。
如果在差不多同时多请求完成了,然后第一个完成的请求发现不是被唤起的任务。然而,当所有的完成的任务将会增加它的信号量,这将可能在下一个循环时处理—事件完成要以队列的形式排队和处理,但是在一个循环中仅仅处理一个请求的完成。
为了更容易的运行SymbianOS的程序,这种程序通常由一个进程中的线程组成,来发出多个异步请求而且在任何当前的请求完成的时候唤起,需要提供一个支持的框架。协作多任务通过两种类型的对象来实现:活动调度表作为等待循环(每个线程一个—提供的静态方法总是针对当前的线程),活动对象作为每个任务—它压缩请求和相应的处理方法(调用来处理请求完成)。
注意所有的在GUI应用程序的处理发生在一个活动对象中,但是他的大部分都是伴随着活动调度表的创建,藏在应用框架里。
在一个控制台程序或者动态库中,你必须创建和安装你自己的活动调度表。
活动调度表
活动调度表通过CActiveScheduler类实现,或者从这个继承来的类实现。它实现了有一个单一线程的等待循环,发现异步事件的完成和定位相关的活动对象来处理它们。
活动调度表维持一个系统里所有活动对象的列表,以优先级为顺序。相同的优先级的活动对象的排序就没有说明了。
完成的事件通过一个User::WaitForAnyRequest()的调用来侦测。这个调用挂起线程,直到一个异步请求已经完成。当线程发出信号,活动调度表通过他的活动对象列表以优先级的顺序来循环查找到已经完成的活动对象。每个活动对象将检查是否已经发出一个请求(设置iActive标志位),和是否请求已经完成(iStatus设置成一个值,不同于KRequestPending—注意请求可能已经返回一个错误)。如果这两个条件已经满足了,活动对象第一个被设置成非活动态(它的iActive标志不设置),所以下次就不会再找到了,有一个完成的请求(除非这个请求已经被重新发出)--记住活动调度表维持所有的活动对象的列表,无论是他们当前是否请求一个服务。然后他的处理方法被调用了。
处理活动对象的方法叫RunL()。注意这个方法可能退出,活动调度表通过一个trap harness调用这个方法。如果退出发生,会尝试调用活动对象自己的异常处理方法RunError()。如果已经返回了一个错误,活动调度表的错误处理程序就会被调用,默认地给线程应急处理。
一旦完成的活动对象出现,循环继续等待。如果活动调度表找到它的列表末尾没有发现完成的活动对象,然后这个表示活动对象给出的请求不在队列中,导致未知事件的应急处理。
活动对象
活动对象表现异步服务请求,概括为:
--一个数据成员代表请求的状态(iStatus)。
--异步服务提供者上的处理(通常R-类对象)。
--在构建的时候连接服务提供者。
--发出(或重发)异步请求(用户定义)的方法
--当请求完成,活动调度表调用处理方法(RunL)
--取消未解决的请求的方法(Cancel())。
这里是所有活动对象抽象基类的定义部分,CActive:
Class CActive:public CBase
{
public:
~CActive();
void Cancel();
inline TBool IsActive() const:
protected:
CActive(Tint aPriority);
Void SetActive();
Virtual void DoCancel()=0;
Virtual void RunL()=0;
Virtual Tint RunError(Tint aError);
public:
TRequestStatus iStatus;
};
首先注意公共成员:当你想取消请求时调用Cancel()方法,而且一个方法(IsActive())检查活动对象是否激活。你获得的活动对象的公共析构方法应该总调用Cancel()来确定在活动对象释放前,任何没有解决的请求被取消—如果没有未解决的请求,Cancel()什么也不做,所以没有必要去直接检查。注意到iStatus成员是公共的—将在这章后来涉及。
值得指出的是在基类里没有虚函数给出异步请求—让你自己去在你自己的类里定义一个。通常叫做Start()。
受保护的构造器阻止栈实例化和带有一个优先级的参数。这个被用来在活动调度列表里给活动对象排序。优先级越高,活动对象越接近列表的顶部,而且在多请求事件里在别的对象完成时更容易被处理。通常一个对象得到一个标准的优先级EPriorityStandard。
[注意]:强调一点,无论优先级高低,活动对象不能相互占用。
最后四个至关重要的方法:
SetActive()必须被调用,一旦请求发出,否则活动调度表在寻找一个完整地活动对象的时候将忽略它,导致错误。
DoCancel()是一个必须要实现的纯虚函数,提供必要的功能来取消未完成的请求,但是注意不应该被直接调用—总使用Cancel(),它来调用DoCancel(),也要保证设置必要的标志位来指出完成的请求。
RunL()是异步事件处理方法。这是在未完成的请求已经完成的时候活动调度表调用的方法。它所作的明显完全依靠被请求的服务的特性—无论一旦请求完成的时候处理是否需要。你将看到一个活动对象用来实现一个状态自动机—在这种情况下,每次RunL()调用的时候,一系列复杂的相互依赖的异步事件将开始下一步执行。
RunError()被调用(活动调度表),如果RunL()推出—它给了活动对象机会去处理自己的错误。如果能处理错误,RunError()应该返回KErrNone。否则将简单的以提示地方时返回错误。在这种情况下,错误将传给活动调度表的Error()方法,默认地产生一个不处理的行为。
记住一系列互动对象属于一个单一的线程,相互协作—一个活动对象的RunL()不能抢占另外一个的RunL()。实际上是这个意思,一旦活动调度表已经调用了一个RunL(),没有别的(在同一个线程里)可以执行,直到这个完成并返回。这意味着所有的RunL()方法必须尽可能的少占用时间,而且这个在任何GUI程序中都很关键,因为所有的保持能作出响应的UI的代码也执行在同一个线程的别的活动对象里。如果你的RunL()占用10秒完成,你的程序将在这个时间里被封锁—没有按键可以被处理,动画将停止等等。作为thumb的一个一般规则,没有RunL()能占用超过1/10秒的时间来完成—否则你的程序的响应将要受阻。
最后注意TRequestStatus iStatus成员。基本上是个短整型,用来表示状态或者异步服务提供者返回的错误代码。对活动对象的iStatuse成员的引用被传递给服务提供者(通过内核),当请求发起的时候。服务提供者的第一个任务是把它设置成KRequestPending。当请求的服务完成了,服务提供者把iStatus的值设置成KErrorNone(如果请求成功完成)或者错误代码。
实现活动对象
接下来涉及到写一个活动对象的实用性。首先提供给你一个调用的任务的目录概要,然后通过一个完整的怎样应用这些信息来创建一个简单的基于活动对象的定时器的例子来引入。最后,当使用活动对象的一般可能发生的缺陷。
活动对象目录
写一个活动对象其实十分的简单。简单概括以下几步:
1. 创建一个类,继承CActive。
2. 压缩合适的异步服务提供者处理方式(通常R-类)如类成员数据。
3. 调用CActive构造器,指定合适的任务优先级—通常默认的优先级。
4. 在你的ConstructL()方法中连接到服务提供者。
5. 在ConstructL()中调用CActiveScheduler::Add()(除非有什么原因不去调用—它必须在某个地方去调用 )。
6. 像通常一样实现NewL和NewLC()方法。
7. 实现你的异步请求函数,经常调用Start()或者StartL()。它应该在服务提供者处理(R-类)上调用合适的异步服务方法,指定iStatus作为TRequestStatus&的变量。不要忘记了调用SetActive()!
8. 实现你的RunL()方法来处理任何必须的工作,一旦请求完成。这可能包括处理数据,恢复完成请求或者重发请求的观察者。
9. 实现DoCancel()来处理请求的取消。这经常是一种在服务提供者处理上调用合适的取消函数的事情。
10.可选的重载RunError()来处理任何从RunL()的退出。默认的实现将导致活动调度表作出应急处理。
11.实现析构函数调用Cancel(),关闭在 服务提供者上的处理。
使用活动对象然后简单的是应用下列步骤地过程:
1. 通过NewL()或者NewLC(),恰当的实例化。
2. 调用Start(),或者无论什么你实现的方法来初始化请求。
3. 如果你希望在完成前取消请求,只要调用Cancel()就可以了。
一个实际的例子
例子程序Elements显示活动对象的使用。它从不同的三个CSV(Comma-Separated Value)文件载入元素数据列表,文件使用异步接口,接入到文件服务器。活动对象控制数据的读取,数据按每次一行读取,把潜在的长时间执行的任务划分到更小的时间片。这也让所有的文件更有效的“同时”读取。
例子的这部分设计来示范异步处理,但是就像SymbianOS如此有效,异步请求的第二种类型,计时器,不得不减慢系统速度,为了解释说明异步载入过程!文件装载器类象个简单状态自动机一样工作:它在一个伪随机的延迟里或者载入数据或者浪费时间。
注意SymbianOS已经提供了一个简单的计时器类,CTimer,但是更有效果的是看有没有别的方法去实现它。
文件装载器类调用CCsvFileLoader。这里是类的部分定义;注意从CActive类里提供:
class CCsvFileLoader : public CActive
{
public:
void Start();
private:
CCsvFileLoader(RFs& aFs, CElementList& aElementList, MCsvFileLoaderObserver& aObserver);
void ConstructL(const TDesC& aFileName);
//From CActive
void RunL();
TInt RunError(TInt aError);
void DoCancel();
private:
TFileName iFileName;
RFile iFile;
TBool iWastingTime;
RTimer iTimeWaster;
};
构造
两个阶段构造对活动对象几乎都需要,因为他们经常需要连接到他们的异步服务提供者,它可能会失败。这里是对文件装载器的第一阶段构造器的实现:
CCsvFileLoader::CCsvFileLoader(RFs& aFs, CElementList& aElementList, MCsvFileLoaderObserver& aObserver)
: CActive(EPriorityStandard),
…
{
}
注意CCsvFileLoader()调用有硬件编码优先级的CActive构造器,这种情况下为EPriorityStandard。标准优先级定义在e32base.h里,如:
enum TPriority
{
EPriorityIdle=-100,
EPriorityLow=-20,
EPriorityStandard=0,
EPriorityUserInput=10,
EPriorityHigh=20,
};
任意选择一个合适你的活动对象的,记住,它影响的是在活动调度列表里的顺序。很少见到,在程序代码里活动对象有一个不同于EPriorityStandard的优先级—
如果你必须担心明确的优先级,然后就可能你的设计有错误!供参考,UI程序系统任务的优先级在coemain.h的类TActivePriority里定义。
这里是第二阶段文件装载器的构造器:
void CCsvFileLoader::ConstructL(const TDesC& aFileName)
{
iFileName = aFileName;
User::LeaveIfError(iFile.Open(iFs, iFileName, EFileRead));
User::LeaveIfError(iTimeWaster.CreateLocal());
CActiveScheduler::Add(this); // Not done automatically!
}
它设置文件名和连接两个异步服务提供者,通过他们的处理类来请求:RTimer和RFile。如果对计时器没有足够的内存,或者文件不存在,然后ConstructL()(所以NewL()或者NewLC())就会退出。
最后,这个活动对象被加到活动调度列表里。这个不会自动发生,所以通常表现为在第二阶段构造器的最后一行,在构造已经成功以后。
[注意]每一个活动对象必须加到活动调度表一次而且仅一次。添加失败将导致一个未知请求的应急处理。
注意,构造通常包在NewL()或者NewLC()方法里—这些已经在这里忽略了。
开始活动对象
这里是Start()的实现:
void CCsvFileLoader::Start()
{
//Wait for a randomish amount of time before doing anything else
TInt delay = (iFileName.Size() % 10) * 100000; //"random" enough!
iTimeWaster.After(iStatus, delay);
SetActive();
}
这是个方法,被调用来开始初始的异步请求—在这种情况下,它做的是在RunL()方法被调用前等待一段时间。记住,虽然Start()方法本身将几乎立刻返回—RunL()将在以后某些地方被调用,一旦异步计时器请求完成。
一个“随机”延迟时间的计算,基于文件名字的长度(不太随机,但是已经足够了!)。类成员iTimeWaster是一个RTimer实例—
在异步服务提供者上的处理。After()方法有两个方面—
微妙级的时间延迟(千分之一秒)和TRequestStatus。
注意TRequestStatus的存在表示一个异步方法。SymbianOS的所有的异步方法都有TRequestStatus&,被用来恢复自己实现的活动对象。
在所给的时间以后,异步服务提供者(在另外一个线程/过程)将要给线程发出信号,做两件事情:
~增加线程信号量。
~将TRequestStatus设置到某些事情上,不是KRequestPending(如果好的话,就是KerrNone)。
增加信号量重新唤起线程,如果挂起。(记住WaitForAnyRequest()在等待循环顶部唤起当前的线程。)改变TRequestStatus的值(iStatus)将让活动调度表告诉哪个活动对象已经完成,和这将导致RunL()方法被调用。
[注意]:一旦请求发出调用SetActive()失败将导致一个未知请求的应急处理,当请求完成的时候。
RunL()
专门的活动对象的RunL()方法相当复杂,因为它实现了很多的任务:
~它决定下一步的重复要做什么(读取数据或者浪费时间)。
~它检查最后重复的状态和报告到观察者那里。
~它处理任何读入的数据。
任何的一般活动对象的RunL()将可能想做上面的至少一个任务—
一些可能想要做全部的任务。
这里是RunL()的前几行代码:
void CCsvFileLoader::RunL()
{
if (!iHaveTriedToLoad)
{
//Fill the read buffer if we haven't yet tried to do so...
FillReadBufferL();
return;
}
换句话说:如果这是RunL()被调用,然后需要先读取一些数据。这就很明显后来的为什么那么重要了。
FillReadBufferL()看起来像这样:
void CCsvFileLoader::FillReadBufferL()
{
iHaveTriedToLoad = ETrue; // Only check readbuffer size in RunL() if we've tried loading before!
// Seek to current file position. We're not bothered if this is past the EOF,
// in that case Read() returns a zero-sized descriptor via iReadBuffer(), tested in RunL().
User::LeaveIfError(iFile.Seek(ESeekStart, iFilePos));
//R ead from the file into the buffer. iStatus will be completed by the fileserver once done.
iFile.Read(iReadBuffer, iReadBuffer.MaxSize(), iStatus);
SetActive();
}
首先,你需要设置iHaveTriedToLoad为真,因为你现在已经尝试着去载入一些东西。
接着,在文件中寻找合适的位置。iFile成员是一个RFile对象。
接下来有一些重要的步骤。RFile::Read()是另外一个异步方法。你也可以通过TRequestStatus对象iStatus来说明这个方法。和以前一样,一旦操作完成,这个调用将使RunL()被调用。调用SetActive()来保证这个调用操作的完成。
这里是RunL()的下面一个部分,处理错误报告:
if ((iStatus.Int() != KErrNone) || (iReadBuffer.Size() == 0))
{
iObserver.NotifyLoadCompleted(iStatus.Int(), *this);
return;
}
注意if条件语句的第一个判断条件。TRequestStatus::Int()返回TRequestStatus的整型错误值。这个错误值是异步服务提供者提供的,作为一种指示活动对象的操作是否已经成功完成的方式。
[注意]一般来说,你应该一直检查iStatus成员的错误值,来判断异步方法是否成功。要强调一点的是RunL()总要被调用,不论异步调用是否成功。
上面的例子,如果异步调用失败,或者如果文件中没有数据能读取,你唤起了观察状态的客户端。如果是没有可读取的数据的情况,这表示到达文件末尾。在另外一种情况下,向用户返回完成状态,而且你返回不需要重发请求。那是基本地活动对象有效生命周期的末尾(除非Start()再次被调用)。
假设这里没有错误和一些读进来的一些数据,下一步是决定重复是否简单的消耗时间。如果是这样,你就像第一次一样重发时间延迟请求:
if (iWastingTime)
{
//Just wait a randomish amount of time
TInt delay = (iFilePos % 10) * 100000;
iTimeWaster.After(iStatus, delay);
SetActive();
iWastingTime = EFalse; //Don't waste time next time around!
return;
}
这里你能看出来,文件的位置用来作为随机的种子,但是不同于在Start()里的基本相同的代码。另外,iWastingTime被设置成假,阻止在下一个重复中执行时间消耗。
RunL()最后一部分在缓冲区里处理数据和重发载入请求,通过重新调用FillReadBufferL()来实现。设置iWastingTime为真,在下一个重复时保证有个时间消耗的步骤,来减慢一点时间:
//Extract and convert the buffer
TPtrC8 elementBuf8 = ExtractToNewLineL(iReadBuffer);
HBufC* elementBuf16 = HBufC::NewLC(elementBuf8.Length()); //elementBuf16 left on cleanup stack
TPtr elementPtr16 = elementBuf16->Des();
elementPtr16.Copy(elementBuf8); //Copy the 8 bit buffer to the 16 bit buffer, with alternate zero padding
//(i.e. convert from ASCII to UNICODE!)
//Read the element from the buffer
iElementList.AppendL(*elementBuf16); //(could equally well have used elementPtr16)
//Report the element that's just been loaded to the observer
TInt lastElementLoadedIndex = iElementList.NumberOfElements() - 1; //-1 since zero based index!
iObserver.NotifyElementLoaded(lastElementLoadedIndex);
//Increment the file position
iFilePos += elementBuf16->Length() + KTxtLineDelimiter().Length(); //Start next read from after the newline
//NB the use of overloaded () operator
//to cast a TLitC to a TDesC
//Re-issue the request
FillReadBufferL();
//Cleanup
CleanupStack::PopAndDestroy(elementBuf16);
//Waste some time next time round before processing the file input (see comment earlier)...
iWastingTime = ETrue;
在RunError()中,处理错误
完全允许RunL退出—后缀“L”显示了这个意思。如果退出了,退出时候的错误代码将传递给RunError():
TInt CCsvFileLoader::RunError(TInt aError)
{
//Notify the observer of the error
iObserver.NotifyLoadCompleted(aError, *this);
return KErrNone; //KErrNone indicates we have dealt with the error so the scheduler won't panic
}
你仅仅唤起判断载入是否完成的观察者(虽然不成功,也会有合适的错误代码),如果返回KerrNone表明错误已经自己处理了。任何非零的错误将要传给活动调度表的Error()方法里。
另外的错误处理可能作更多的复杂的事情,比如重试或者类似的。但是一般他们将要做一些上面例子里类似的事情,和错误通知。
取消未完成的请求
所有的活动对象必须实现DoCancel()方法来取消任何未完成的请求。这里是CCsVFileLoader的实现:
void CCsvFileLoader::DoCancel()
{
if (iWastingTime || !iHaveTriedToLoad)
{
iTimeWaster.Cancel();
}
//Cannot cancel a file read!
}
如果计时器在执行的话,你所需要作的是调用RTimer::Cancel()。因为没有取消未完成的RFile请求的方式,所以你不需要在这种情况下做什么。
DoCancel()被CActive::Cancel()调用。CActive::Cancel()本身不应该被重载(这是非虚的),因为它为你做了很多重要的事情:
~它检查是否活动对象是否真的被激活的—如果不是,它只返回,不做任何事情。
~它调用DoCancel()。
~它等待请求完成—者必须尽可能快地完成。(注意原来的请求可能在它取消前就完成了。)
~它把iActive设置成假。
理解Cancel()等待未完成的请求完成是很重要的。这意味着RunL()不能像你希望的调用Cancel()的样子去调用—所以任何需要的Cancel()的地方都必须在DoCancel()里处理,不是在RunL()。
[注意]你不应该直接调用活动对象的DoCancel()方法。如果你想取消请求,只要调用Cancel()—它会调用DoCancel(),也保证RunL()不会被调用。
析构方法
析构方法的实现在这里:
CCsvFileLoader::~CCsvFileLoader()
{
Cancel(); // All AOs should cancel when they are deleted
iFile.Close();
iTimeWaster.Close();
}
在活动对象的析构函数里面第一件事情调用Cancel()取消任何未完成的请求。如果活动对象被一个未完成的请求删除,一个未知请求应急处理(E32USER—CBase 40)就是结果。
对异步服务提供者的任何处理都必须被关闭来避免资源泄露。
基础CActive析构方法自动调用Deque()来从活动调度列表里删除活动对象。
开始活动调度表
UI框架将自动地为你创建,安装和开始活动调度表,所以如果你不打算去写.exe(平台程序或者SymbianOS 服务器),或者DLL就可以忽略它—这些需要活动调度表直接开始。然而,提供了一些额外的窥探,活动对象和活动调度表怎么交互的。
在活动调度表开始前,许多步骤需要去做:
~活动调度表必须被实例化。
~必须被安装到线程中。
~活动对象必须被创建和加到活动调度表中。
~请求必须被发出(当等待循环挂起线程)。
只有这样调度表才可以开始。
所有这些步骤可以在下面的代码中找到:
void DoExampleL()
{
//construct and install the active scheduler
CActiveScheduler* scheduler = new (ELeave) CActiveScheduler;
CleanupStack::PushL(scheduler);
CActiveScheduler::Install(scheduler);
//construct the new element engine
CElementsEngine* elementEngine = CElementsEngine::NewLC(*console); //remains on cleanup stack
elementEngine->LoadFromCsvFilesL(); //Issue the request...
CActiveScheduler::Start(); //...then start the scheduler
CleanupStack::PopAndDestroy(2, scheduler); //elementEngine, scheduler
}
前三行相当明显—他们实例化一个新的CActiveScheduler对象,添加到清除堆栈上,然后安装这个对象作为线程的当前活动调度表—在每个线程中只能安装一个活动调度表。
下面两行创建CElementsEngine,它拥有活动对象(三个CCsvFileLoader对象)和开始他们。
这里现在有一些未解决的请求,活动调度表能够开始。CActiveScheduler::Start()方法的调用直到相应的从活动对象的RunL()方法里产生CActiveScheduler::Stop()方法的调用时才返回。当这样做了以后,活动调度表和引擎被删除,然后线程退出。
有许多事情非常重要—CactiveScheduler::Start()方法恰当的涵盖了整个线程的生命周期。为了能更深入的讨论这些问题,考虑活动调度表的生命周期。
四个“对象”(执行,活动调度表,活动对象和服务提供者)显示为垂直圆柱。实践从顶到底。
当可执行的主线程开始,它实例化和安装了活动调度表。然后,至少一个活动对象必须被创建和添加到活动调度表里。一旦作了,活动对象必须发起一个请求。异步服务提供者(在另外一个线程里,通常另外一个进程)通过把活动对象的iStatus成员设置成KRequestPending开始。服务提供者能开始服务请求。
一旦有个请求未完成,Start()活动调度表就会很安全。这导致了活动调度表进入了等待循环,调用WaitForAnyRequest()来开始,减少线程的信号量,然后挂起线程。
这是为什么在开始活动调度表前必须有个为解决的请求。没有这个请求,就不能让线程被重新唤醒—没有什么将增加线程的信号量,而且线程将保持持久地挂起。
服务提供者完成请求和给出信号,通过增加请求线程的信号量和设置活动对象iStatus成员的合适的错误代码来实现。调度表能够通过它的列表搜索来发现已经完成的活动对象(是激活状态和有iStatus不等于KRequestPending)和执行RunL()。
接下来发生的依赖于RunL()。它处理请求的完成和可能重发请求或者产生另外一个活动对象—在这些情况下循环重复(虽然活动调度表不会再开始)。
作为选择,活动对象的RunL()可能调用CActiveScheduler::Stop()。这导致活动调度表退出它的等待循环,从调用CActiveScheduler::Start()返回控制。这样的执行将经常清除退出。记住如果你创建了一个UI程序,活动调度表将被框架创建和控制,而且你不需要为主活动调度表循环调用CActiveScheduler::Stop()—实际上你不应该做这个,让程序框架处理这个。
[注意]第二次调用CActiveScheduler::Start()创建了一个嵌套的等待循环—这用来为了模型处理。例如,模型对话框需要“暂停”应用线程的主处理过程,但是仍必须处理输入等等。调用CActiveScheduler::Stop()将返控制到初始化等待循环。这个是个高级的话题,不在本书的范围呢。
如你能看到的,活动调度表的生命周期和拥有的执行线程很相似。当然这是UI框架程序,框架创建活动调度表和移入一些维系UI和用户输入等等的活动对象。
再一次说,几乎所有开发者写的代码里反映的UI程序都有活动对象的RunL()在执行,所以就需要尽可能的让系统能够及时响应外界,减少程序的敏感度。
一般的活动对象的缺陷
活动对象的最一般的问题是从活动调度表里的未知事件应急处理。这通常是由下面一种或者多种原因造成的:
~在开始活动对象前忘记调用CActiveScheduler::Add()。
~在发出或者重发一个异步请求后,没有调用SetActive()。
~同时传递一样的iStatus给两个服务提供者。(因此,在同一个活动对象上有多个为解决的请求)
不要直接调用DoCancel()。它应该是私有的—总是调用Cancel(),并且不调用Cancel()如果没有DoCancel()!注意Cancel()总在你获得的类的析构函数里调用。如果在基类CActive析构函数调用时有未解决的请求,E32USER-CBase 40应急处理将使用。
别忘了活动对象使用协同多任务—记住没有活动对象能先占另外一个,也没有RunL()占用超过1/10秒的时间来完成。非常长时间的执行RunL()可能导致一个ViewServerTimeOut应急处理(ViewSrv 11)—这在程序不能在给定的时间里响应系统的时候发生(大约10秒)。
如果你的活动对象重复重发请求,然后如果在重复之间没有足够的时间ViewServerTimeOut应急处理可能发生—你的活动对象可能“扭曲“活动调度表和不允许更低优先级的活动对象来完成。如果你想要一个活动对象被重复调用,使用类似于例子里强调的技术,使用一个计时器来给一个合理的延迟(至少百分之几)。这个延迟不必在每个请求以后使用—在一个游戏程序里,例如,你可能想要丢失某一针来允许别的处理发生。
注意如果你正在获得,或者使用CTimer来创建延迟,你需要Add()它到活动调度表—这不是在基类里默认实现的!更具体的描述可以在SDK文档里找到。
不要忘记了如果你写一个平台程序或者DLL,一个活动调度表不是默认提供的。没有一个活动调度表安装上,任何活动对象的使用都会导致E32USER—Cbase 44应急处理。
这样的异步系统在现在的计算中很普遍,而且有很多方式实现它:
一个流行的方式是在有优先权的系统中使用多线程:产生一个新的执行线程来处理每个异步任务,发出请求和等待完成—通常通过选举请求提供者来看请求是否已经完成。虽然这个方法可能在SymbianOS系统里,但是效率很低,而且非常让人失望。
[注意]:更多实现多线程的信息能在SDK文档种关于RThread的API描述部分找到,但是这个方法仅仅被使用,更多地解释超出的本书的范围。
另外一种供选择的方法,而且受到SymbianOS应用程序青睐的是使用协作多任务。在有优先权的系统中,操作系统的内核决定了现在的线程得到了足够的处理器的使用,而且允许另外一个线程先占它,通常在它完成处理之前。协作(或者无优先权)多任务系统要求当前执行的任务放弃处理器的使用,而且允许其他的任务来执行。所有的应用程序处理都发生在一个单独的线程里,所以多任务必须协作。
协作多任务通常实现像某种等待或者消息循环,而且是SymbianOS的情况。
这个循环从等待开始—一个简单的应用程序线程被阻塞住,直到一个服务请求完成,而且服务提供者(“server”),一个在单一线程(或者进程)执行的,表明已经完成,而且增加了一个信号量。SymbianOS不采取投票的方式,因为这样浪费处理器的循环时间和电池的能量。也注意到非常重要的是在开始循环之前至少有一个请求,否则等待信号不能被唤起!
一旦等待循环队列被唤起,就可以继续执行。系统通过每一个明显的任务请求来寻找,直到它找到了一个完整的任务然后执行它的处理方法。然后这个循环等待下一个请求的完成。
如果在差不多同时多请求完成了,然后第一个完成的请求发现不是被唤起的任务。然而,当所有的完成的任务将会增加它的信号量,这将可能在下一个循环时处理—事件完成要以队列的形式排队和处理,但是在一个循环中仅仅处理一个请求的完成。
为了更容易的运行SymbianOS的程序,这种程序通常由一个进程中的线程组成,来发出多个异步请求而且在任何当前的请求完成的时候唤起,需要提供一个支持的框架。协作多任务通过两种类型的对象来实现:活动调度表作为等待循环(每个线程一个—提供的静态方法总是针对当前的线程),活动对象作为每个任务—它压缩请求和相应的处理方法(调用来处理请求完成)。
注意所有的在GUI应用程序的处理发生在一个活动对象中,但是他的大部分都是伴随着活动调度表的创建,藏在应用框架里。
在一个控制台程序或者动态库中,你必须创建和安装你自己的活动调度表。
活动调度表
活动调度表通过CActiveScheduler类实现,或者从这个继承来的类实现。它实现了有一个单一线程的等待循环,发现异步事件的完成和定位相关的活动对象来处理它们。
活动调度表维持一个系统里所有活动对象的列表,以优先级为顺序。相同的优先级的活动对象的排序就没有说明了。
完成的事件通过一个User::WaitForAnyRequest()的调用来侦测。这个调用挂起线程,直到一个异步请求已经完成。当线程发出信号,活动调度表通过他的活动对象列表以优先级的顺序来循环查找到已经完成的活动对象。每个活动对象将检查是否已经发出一个请求(设置iActive标志位),和是否请求已经完成(iStatus设置成一个值,不同于KRequestPending—注意请求可能已经返回一个错误)。如果这两个条件已经满足了,活动对象第一个被设置成非活动态(它的iActive标志不设置),所以下次就不会再找到了,有一个完成的请求(除非这个请求已经被重新发出)--记住活动调度表维持所有的活动对象的列表,无论是他们当前是否请求一个服务。然后他的处理方法被调用了。
处理活动对象的方法叫RunL()。注意这个方法可能退出,活动调度表通过一个trap harness调用这个方法。如果退出发生,会尝试调用活动对象自己的异常处理方法RunError()。如果已经返回了一个错误,活动调度表的错误处理程序就会被调用,默认地给线程应急处理。
一旦完成的活动对象出现,循环继续等待。如果活动调度表找到它的列表末尾没有发现完成的活动对象,然后这个表示活动对象给出的请求不在队列中,导致未知事件的应急处理。
活动对象
活动对象表现异步服务请求,概括为:
--一个数据成员代表请求的状态(iStatus)。
--异步服务提供者上的处理(通常R-类对象)。
--在构建的时候连接服务提供者。
--发出(或重发)异步请求(用户定义)的方法
--当请求完成,活动调度表调用处理方法(RunL)
--取消未解决的请求的方法(Cancel())。
这里是所有活动对象抽象基类的定义部分,CActive:
Class CActive:public CBase
{
public:
~CActive();
void Cancel();
inline TBool IsActive() const:
protected:
CActive(Tint aPriority);
Void SetActive();
Virtual void DoCancel()=0;
Virtual void RunL()=0;
Virtual Tint RunError(Tint aError);
public:
TRequestStatus iStatus;
};
首先注意公共成员:当你想取消请求时调用Cancel()方法,而且一个方法(IsActive())检查活动对象是否激活。你获得的活动对象的公共析构方法应该总调用Cancel()来确定在活动对象释放前,任何没有解决的请求被取消—如果没有未解决的请求,Cancel()什么也不做,所以没有必要去直接检查。注意到iStatus成员是公共的—将在这章后来涉及。
值得指出的是在基类里没有虚函数给出异步请求—让你自己去在你自己的类里定义一个。通常叫做Start()。
受保护的构造器阻止栈实例化和带有一个优先级的参数。这个被用来在活动调度列表里给活动对象排序。优先级越高,活动对象越接近列表的顶部,而且在多请求事件里在别的对象完成时更容易被处理。通常一个对象得到一个标准的优先级EPriorityStandard。
[注意]:强调一点,无论优先级高低,活动对象不能相互占用。
最后四个至关重要的方法:
SetActive()必须被调用,一旦请求发出,否则活动调度表在寻找一个完整地活动对象的时候将忽略它,导致错误。
DoCancel()是一个必须要实现的纯虚函数,提供必要的功能来取消未完成的请求,但是注意不应该被直接调用—总使用Cancel(),它来调用DoCancel(),也要保证设置必要的标志位来指出完成的请求。
RunL()是异步事件处理方法。这是在未完成的请求已经完成的时候活动调度表调用的方法。它所作的明显完全依靠被请求的服务的特性—无论一旦请求完成的时候处理是否需要。你将看到一个活动对象用来实现一个状态自动机—在这种情况下,每次RunL()调用的时候,一系列复杂的相互依赖的异步事件将开始下一步执行。
RunError()被调用(活动调度表),如果RunL()推出—它给了活动对象机会去处理自己的错误。如果能处理错误,RunError()应该返回KErrNone。否则将简单的以提示地方时返回错误。在这种情况下,错误将传给活动调度表的Error()方法,默认地产生一个不处理的行为。
记住一系列互动对象属于一个单一的线程,相互协作—一个活动对象的RunL()不能抢占另外一个的RunL()。实际上是这个意思,一旦活动调度表已经调用了一个RunL(),没有别的(在同一个线程里)可以执行,直到这个完成并返回。这意味着所有的RunL()方法必须尽可能的少占用时间,而且这个在任何GUI程序中都很关键,因为所有的保持能作出响应的UI的代码也执行在同一个线程的别的活动对象里。如果你的RunL()占用10秒完成,你的程序将在这个时间里被封锁—没有按键可以被处理,动画将停止等等。作为thumb的一个一般规则,没有RunL()能占用超过1/10秒的时间来完成—否则你的程序的响应将要受阻。
最后注意TRequestStatus iStatus成员。基本上是个短整型,用来表示状态或者异步服务提供者返回的错误代码。对活动对象的iStatuse成员的引用被传递给服务提供者(通过内核),当请求发起的时候。服务提供者的第一个任务是把它设置成KRequestPending。当请求的服务完成了,服务提供者把iStatus的值设置成KErrorNone(如果请求成功完成)或者错误代码。
实现活动对象
接下来涉及到写一个活动对象的实用性。首先提供给你一个调用的任务的目录概要,然后通过一个完整的怎样应用这些信息来创建一个简单的基于活动对象的定时器的例子来引入。最后,当使用活动对象的一般可能发生的缺陷。
活动对象目录
写一个活动对象其实十分的简单。简单概括以下几步:
1. 创建一个类,继承CActive。
2. 压缩合适的异步服务提供者处理方式(通常R-类)如类成员数据。
3. 调用CActive构造器,指定合适的任务优先级—通常默认的优先级。
4. 在你的ConstructL()方法中连接到服务提供者。
5. 在ConstructL()中调用CActiveScheduler::Add()(除非有什么原因不去调用—它必须在某个地方去调用 )。
6. 像通常一样实现NewL和NewLC()方法。
7. 实现你的异步请求函数,经常调用Start()或者StartL()。它应该在服务提供者处理(R-类)上调用合适的异步服务方法,指定iStatus作为TRequestStatus&的变量。不要忘记了调用SetActive()!
8. 实现你的RunL()方法来处理任何必须的工作,一旦请求完成。这可能包括处理数据,恢复完成请求或者重发请求的观察者。
9. 实现DoCancel()来处理请求的取消。这经常是一种在服务提供者处理上调用合适的取消函数的事情。
10.可选的重载RunError()来处理任何从RunL()的退出。默认的实现将导致活动调度表作出应急处理。
11.实现析构函数调用Cancel(),关闭在 服务提供者上的处理。
使用活动对象然后简单的是应用下列步骤地过程:
1. 通过NewL()或者NewLC(),恰当的实例化。
2. 调用Start(),或者无论什么你实现的方法来初始化请求。
3. 如果你希望在完成前取消请求,只要调用Cancel()就可以了。
一个实际的例子
例子程序Elements显示活动对象的使用。它从不同的三个CSV(Comma-Separated Value)文件载入元素数据列表,文件使用异步接口,接入到文件服务器。活动对象控制数据的读取,数据按每次一行读取,把潜在的长时间执行的任务划分到更小的时间片。这也让所有的文件更有效的“同时”读取。
例子的这部分设计来示范异步处理,但是就像SymbianOS如此有效,异步请求的第二种类型,计时器,不得不减慢系统速度,为了解释说明异步载入过程!文件装载器类象个简单状态自动机一样工作:它在一个伪随机的延迟里或者载入数据或者浪费时间。
注意SymbianOS已经提供了一个简单的计时器类,CTimer,但是更有效果的是看有没有别的方法去实现它。
文件装载器类调用CCsvFileLoader。这里是类的部分定义;注意从CActive类里提供:
class CCsvFileLoader : public CActive
{
public:
void Start();
private:
CCsvFileLoader(RFs& aFs, CElementList& aElementList, MCsvFileLoaderObserver& aObserver);
void ConstructL(const TDesC& aFileName);
//From CActive
void RunL();
TInt RunError(TInt aError);
void DoCancel();
private:
TFileName iFileName;
RFile iFile;
TBool iWastingTime;
RTimer iTimeWaster;
};
构造
两个阶段构造对活动对象几乎都需要,因为他们经常需要连接到他们的异步服务提供者,它可能会失败。这里是对文件装载器的第一阶段构造器的实现:
CCsvFileLoader::CCsvFileLoader(RFs& aFs, CElementList& aElementList, MCsvFileLoaderObserver& aObserver)
: CActive(EPriorityStandard),
…
{
}
注意CCsvFileLoader()调用有硬件编码优先级的CActive构造器,这种情况下为EPriorityStandard。标准优先级定义在e32base.h里,如:
enum TPriority
{
EPriorityIdle=-100,
EPriorityLow=-20,
EPriorityStandard=0,
EPriorityUserInput=10,
EPriorityHigh=20,
};
任意选择一个合适你的活动对象的,记住,它影响的是在活动调度列表里的顺序。很少见到,在程序代码里活动对象有一个不同于EPriorityStandard的优先级—
如果你必须担心明确的优先级,然后就可能你的设计有错误!供参考,UI程序系统任务的优先级在coemain.h的类TActivePriority里定义。
这里是第二阶段文件装载器的构造器:
void CCsvFileLoader::ConstructL(const TDesC& aFileName)
{
iFileName = aFileName;
User::LeaveIfError(iFile.Open(iFs, iFileName, EFileRead));
User::LeaveIfError(iTimeWaster.CreateLocal());
CActiveScheduler::Add(this); // Not done automatically!
}
它设置文件名和连接两个异步服务提供者,通过他们的处理类来请求:RTimer和RFile。如果对计时器没有足够的内存,或者文件不存在,然后ConstructL()(所以NewL()或者NewLC())就会退出。
最后,这个活动对象被加到活动调度列表里。这个不会自动发生,所以通常表现为在第二阶段构造器的最后一行,在构造已经成功以后。
[注意]每一个活动对象必须加到活动调度表一次而且仅一次。添加失败将导致一个未知请求的应急处理。
注意,构造通常包在NewL()或者NewLC()方法里—这些已经在这里忽略了。
开始活动对象
这里是Start()的实现:
void CCsvFileLoader::Start()
{
//Wait for a randomish amount of time before doing anything else
TInt delay = (iFileName.Size() % 10) * 100000; //"random" enough!
iTimeWaster.After(iStatus, delay);
SetActive();
}
这是个方法,被调用来开始初始的异步请求—在这种情况下,它做的是在RunL()方法被调用前等待一段时间。记住,虽然Start()方法本身将几乎立刻返回—RunL()将在以后某些地方被调用,一旦异步计时器请求完成。
一个“随机”延迟时间的计算,基于文件名字的长度(不太随机,但是已经足够了!)。类成员iTimeWaster是一个RTimer实例—
在异步服务提供者上的处理。After()方法有两个方面—
微妙级的时间延迟(千分之一秒)和TRequestStatus。
注意TRequestStatus的存在表示一个异步方法。SymbianOS的所有的异步方法都有TRequestStatus&,被用来恢复自己实现的活动对象。
在所给的时间以后,异步服务提供者(在另外一个线程/过程)将要给线程发出信号,做两件事情:
~增加线程信号量。
~将TRequestStatus设置到某些事情上,不是KRequestPending(如果好的话,就是KerrNone)。
增加信号量重新唤起线程,如果挂起。(记住WaitForAnyRequest()在等待循环顶部唤起当前的线程。)改变TRequestStatus的值(iStatus)将让活动调度表告诉哪个活动对象已经完成,和这将导致RunL()方法被调用。
[注意]:一旦请求发出调用SetActive()失败将导致一个未知请求的应急处理,当请求完成的时候。
RunL()
专门的活动对象的RunL()方法相当复杂,因为它实现了很多的任务:
~它决定下一步的重复要做什么(读取数据或者浪费时间)。
~它检查最后重复的状态和报告到观察者那里。
~它处理任何读入的数据。
任何的一般活动对象的RunL()将可能想做上面的至少一个任务—
一些可能想要做全部的任务。
这里是RunL()的前几行代码:
void CCsvFileLoader::RunL()
{
if (!iHaveTriedToLoad)
{
//Fill the read buffer if we haven't yet tried to do so...
FillReadBufferL();
return;
}
换句话说:如果这是RunL()被调用,然后需要先读取一些数据。这就很明显后来的为什么那么重要了。
FillReadBufferL()看起来像这样:
void CCsvFileLoader::FillReadBufferL()
{
iHaveTriedToLoad = ETrue; // Only check readbuffer size in RunL() if we've tried loading before!
// Seek to current file position. We're not bothered if this is past the EOF,
// in that case Read() returns a zero-sized descriptor via iReadBuffer(), tested in RunL().
User::LeaveIfError(iFile.Seek(ESeekStart, iFilePos));
//R ead from the file into the buffer. iStatus will be completed by the fileserver once done.
iFile.Read(iReadBuffer, iReadBuffer.MaxSize(), iStatus);
SetActive();
}
首先,你需要设置iHaveTriedToLoad为真,因为你现在已经尝试着去载入一些东西。
接着,在文件中寻找合适的位置。iFile成员是一个RFile对象。
接下来有一些重要的步骤。RFile::Read()是另外一个异步方法。你也可以通过TRequestStatus对象iStatus来说明这个方法。和以前一样,一旦操作完成,这个调用将使RunL()被调用。调用SetActive()来保证这个调用操作的完成。
这里是RunL()的下面一个部分,处理错误报告:
if ((iStatus.Int() != KErrNone) || (iReadBuffer.Size() == 0))
{
iObserver.NotifyLoadCompleted(iStatus.Int(), *this);
return;
}
注意if条件语句的第一个判断条件。TRequestStatus::Int()返回TRequestStatus的整型错误值。这个错误值是异步服务提供者提供的,作为一种指示活动对象的操作是否已经成功完成的方式。
[注意]一般来说,你应该一直检查iStatus成员的错误值,来判断异步方法是否成功。要强调一点的是RunL()总要被调用,不论异步调用是否成功。
上面的例子,如果异步调用失败,或者如果文件中没有数据能读取,你唤起了观察状态的客户端。如果是没有可读取的数据的情况,这表示到达文件末尾。在另外一种情况下,向用户返回完成状态,而且你返回不需要重发请求。那是基本地活动对象有效生命周期的末尾(除非Start()再次被调用)。
假设这里没有错误和一些读进来的一些数据,下一步是决定重复是否简单的消耗时间。如果是这样,你就像第一次一样重发时间延迟请求:
if (iWastingTime)
{
//Just wait a randomish amount of time
TInt delay = (iFilePos % 10) * 100000;
iTimeWaster.After(iStatus, delay);
SetActive();
iWastingTime = EFalse; //Don't waste time next time around!
return;
}
这里你能看出来,文件的位置用来作为随机的种子,但是不同于在Start()里的基本相同的代码。另外,iWastingTime被设置成假,阻止在下一个重复中执行时间消耗。
RunL()最后一部分在缓冲区里处理数据和重发载入请求,通过重新调用FillReadBufferL()来实现。设置iWastingTime为真,在下一个重复时保证有个时间消耗的步骤,来减慢一点时间:
//Extract and convert the buffer
TPtrC8 elementBuf8 = ExtractToNewLineL(iReadBuffer);
HBufC* elementBuf16 = HBufC::NewLC(elementBuf8.Length()); //elementBuf16 left on cleanup stack
TPtr elementPtr16 = elementBuf16->Des();
elementPtr16.Copy(elementBuf8); //Copy the 8 bit buffer to the 16 bit buffer, with alternate zero padding
//(i.e. convert from ASCII to UNICODE!)
//Read the element from the buffer
iElementList.AppendL(*elementBuf16); //(could equally well have used elementPtr16)
//Report the element that's just been loaded to the observer
TInt lastElementLoadedIndex = iElementList.NumberOfElements() - 1; //-1 since zero based index!
iObserver.NotifyElementLoaded(lastElementLoadedIndex);
//Increment the file position
iFilePos += elementBuf16->Length() + KTxtLineDelimiter().Length(); //Start next read from after the newline
//NB the use of overloaded () operator
//to cast a TLitC to a TDesC
//Re-issue the request
FillReadBufferL();
//Cleanup
CleanupStack::PopAndDestroy(elementBuf16);
//Waste some time next time round before processing the file input (see comment earlier)...
iWastingTime = ETrue;
在RunError()中,处理错误
完全允许RunL退出—后缀“L”显示了这个意思。如果退出了,退出时候的错误代码将传递给RunError():
TInt CCsvFileLoader::RunError(TInt aError)
{
//Notify the observer of the error
iObserver.NotifyLoadCompleted(aError, *this);
return KErrNone; //KErrNone indicates we have dealt with the error so the scheduler won't panic
}
你仅仅唤起判断载入是否完成的观察者(虽然不成功,也会有合适的错误代码),如果返回KerrNone表明错误已经自己处理了。任何非零的错误将要传给活动调度表的Error()方法里。
另外的错误处理可能作更多的复杂的事情,比如重试或者类似的。但是一般他们将要做一些上面例子里类似的事情,和错误通知。
取消未完成的请求
所有的活动对象必须实现DoCancel()方法来取消任何未完成的请求。这里是CCsVFileLoader的实现:
void CCsvFileLoader::DoCancel()
{
if (iWastingTime || !iHaveTriedToLoad)
{
iTimeWaster.Cancel();
}
//Cannot cancel a file read!
}
如果计时器在执行的话,你所需要作的是调用RTimer::Cancel()。因为没有取消未完成的RFile请求的方式,所以你不需要在这种情况下做什么。
DoCancel()被CActive::Cancel()调用。CActive::Cancel()本身不应该被重载(这是非虚的),因为它为你做了很多重要的事情:
~它检查是否活动对象是否真的被激活的—如果不是,它只返回,不做任何事情。
~它调用DoCancel()。
~它等待请求完成—者必须尽可能快地完成。(注意原来的请求可能在它取消前就完成了。)
~它把iActive设置成假。
理解Cancel()等待未完成的请求完成是很重要的。这意味着RunL()不能像你希望的调用Cancel()的样子去调用—所以任何需要的Cancel()的地方都必须在DoCancel()里处理,不是在RunL()。
[注意]你不应该直接调用活动对象的DoCancel()方法。如果你想取消请求,只要调用Cancel()—它会调用DoCancel(),也保证RunL()不会被调用。
析构方法
析构方法的实现在这里:
CCsvFileLoader::~CCsvFileLoader()
{
Cancel(); // All AOs should cancel when they are deleted
iFile.Close();
iTimeWaster.Close();
}
在活动对象的析构函数里面第一件事情调用Cancel()取消任何未完成的请求。如果活动对象被一个未完成的请求删除,一个未知请求应急处理(E32USER—CBase 40)就是结果。
对异步服务提供者的任何处理都必须被关闭来避免资源泄露。
基础CActive析构方法自动调用Deque()来从活动调度列表里删除活动对象。
开始活动调度表
UI框架将自动地为你创建,安装和开始活动调度表,所以如果你不打算去写.exe(平台程序或者SymbianOS 服务器),或者DLL就可以忽略它—这些需要活动调度表直接开始。然而,提供了一些额外的窥探,活动对象和活动调度表怎么交互的。
在活动调度表开始前,许多步骤需要去做:
~活动调度表必须被实例化。
~必须被安装到线程中。
~活动对象必须被创建和加到活动调度表中。
~请求必须被发出(当等待循环挂起线程)。
只有这样调度表才可以开始。
所有这些步骤可以在下面的代码中找到:
void DoExampleL()
{
//construct and install the active scheduler
CActiveScheduler* scheduler = new (ELeave) CActiveScheduler;
CleanupStack::PushL(scheduler);
CActiveScheduler::Install(scheduler);
//construct the new element engine
CElementsEngine* elementEngine = CElementsEngine::NewLC(*console); //remains on cleanup stack
elementEngine->LoadFromCsvFilesL(); //Issue the request...
CActiveScheduler::Start(); //...then start the scheduler
CleanupStack::PopAndDestroy(2, scheduler); //elementEngine, scheduler
}
前三行相当明显—他们实例化一个新的CActiveScheduler对象,添加到清除堆栈上,然后安装这个对象作为线程的当前活动调度表—在每个线程中只能安装一个活动调度表。
下面两行创建CElementsEngine,它拥有活动对象(三个CCsvFileLoader对象)和开始他们。
这里现在有一些未解决的请求,活动调度表能够开始。CActiveScheduler::Start()方法的调用直到相应的从活动对象的RunL()方法里产生CActiveScheduler::Stop()方法的调用时才返回。当这样做了以后,活动调度表和引擎被删除,然后线程退出。
有许多事情非常重要—CactiveScheduler::Start()方法恰当的涵盖了整个线程的生命周期。为了能更深入的讨论这些问题,考虑活动调度表的生命周期。
四个“对象”(执行,活动调度表,活动对象和服务提供者)显示为垂直圆柱。实践从顶到底。
当可执行的主线程开始,它实例化和安装了活动调度表。然后,至少一个活动对象必须被创建和添加到活动调度表里。一旦作了,活动对象必须发起一个请求。异步服务提供者(在另外一个线程里,通常另外一个进程)通过把活动对象的iStatus成员设置成KRequestPending开始。服务提供者能开始服务请求。
一旦有个请求未完成,Start()活动调度表就会很安全。这导致了活动调度表进入了等待循环,调用WaitForAnyRequest()来开始,减少线程的信号量,然后挂起线程。
这是为什么在开始活动调度表前必须有个为解决的请求。没有这个请求,就不能让线程被重新唤醒—没有什么将增加线程的信号量,而且线程将保持持久地挂起。
服务提供者完成请求和给出信号,通过增加请求线程的信号量和设置活动对象iStatus成员的合适的错误代码来实现。调度表能够通过它的列表搜索来发现已经完成的活动对象(是激活状态和有iStatus不等于KRequestPending)和执行RunL()。
接下来发生的依赖于RunL()。它处理请求的完成和可能重发请求或者产生另外一个活动对象—在这些情况下循环重复(虽然活动调度表不会再开始)。
作为选择,活动对象的RunL()可能调用CActiveScheduler::Stop()。这导致活动调度表退出它的等待循环,从调用CActiveScheduler::Start()返回控制。这样的执行将经常清除退出。记住如果你创建了一个UI程序,活动调度表将被框架创建和控制,而且你不需要为主活动调度表循环调用CActiveScheduler::Stop()—实际上你不应该做这个,让程序框架处理这个。
[注意]第二次调用CActiveScheduler::Start()创建了一个嵌套的等待循环—这用来为了模型处理。例如,模型对话框需要“暂停”应用线程的主处理过程,但是仍必须处理输入等等。调用CActiveScheduler::Stop()将返控制到初始化等待循环。这个是个高级的话题,不在本书的范围呢。
如你能看到的,活动调度表的生命周期和拥有的执行线程很相似。当然这是UI框架程序,框架创建活动调度表和移入一些维系UI和用户输入等等的活动对象。
再一次说,几乎所有开发者写的代码里反映的UI程序都有活动对象的RunL()在执行,所以就需要尽可能的让系统能够及时响应外界,减少程序的敏感度。
一般的活动对象的缺陷
活动对象的最一般的问题是从活动调度表里的未知事件应急处理。这通常是由下面一种或者多种原因造成的:
~在开始活动对象前忘记调用CActiveScheduler::Add()。
~在发出或者重发一个异步请求后,没有调用SetActive()。
~同时传递一样的iStatus给两个服务提供者。(因此,在同一个活动对象上有多个为解决的请求)
不要直接调用DoCancel()。它应该是私有的—总是调用Cancel(),并且不调用Cancel()如果没有DoCancel()!注意Cancel()总在你获得的类的析构函数里调用。如果在基类CActive析构函数调用时有未解决的请求,E32USER-CBase 40应急处理将使用。
别忘了活动对象使用协同多任务—记住没有活动对象能先占另外一个,也没有RunL()占用超过1/10秒的时间来完成。非常长时间的执行RunL()可能导致一个ViewServerTimeOut应急处理(ViewSrv 11)—这在程序不能在给定的时间里响应系统的时候发生(大约10秒)。
如果你的活动对象重复重发请求,然后如果在重复之间没有足够的时间ViewServerTimeOut应急处理可能发生—你的活动对象可能“扭曲“活动调度表和不允许更低优先级的活动对象来完成。如果你想要一个活动对象被重复调用,使用类似于例子里强调的技术,使用一个计时器来给一个合理的延迟(至少百分之几)。这个延迟不必在每个请求以后使用—在一个游戏程序里,例如,你可能想要丢失某一针来允许别的处理发生。
注意如果你正在获得,或者使用CTimer来创建延迟,你需要Add()它到活动调度表—这不是在基类里默认实现的!更具体的描述可以在SDK文档里找到。
不要忘记了如果你写一个平台程序或者DLL,一个活动调度表不是默认提供的。没有一个活动调度表安装上,任何活动对象的使用都会导致E32USER—Cbase 44应急处理。