×
Namespaces

Variants
Actions

Qt和Symbian C++的混合编程

From Nokia Developer Wiki
Jump to: navigation, search
Article Metadata

本文讲解如何使用PIMPL模式来清晰区隔Qt和Symbian C++代码。

此外,文章讲述了如何编写代码来安全地融合两种环境下的异常处理机制、编码样式和惯例、字符串、几何图形、容器、图像,及数据等。每小节都对某个特定任务在Symbian C++ 和Qt中如何完成,如何融合两种编码惯例,作了高度概括。文中也提供了对一些重要文档的链接,以对这些实现方法作更为详细的解释。

所附范例代码(File:Qtbluetoothdiscoveryexample.zip)实现了找到远程蓝牙设备的Qt API(及相关对话框)。范例使用PIMPL模式获取底层平台信息,同时说明了Qt和SymbianC++混合编程中如何安全地融合不同的异常处理机制、编码样式和字符串。

Contents

简介

Symbian平台是针对移动终端的开源软件平台。目前上亿部的手机终端基于Symbian平台(被称为S60和Symbian OS)的一些早期版本,Symbian平台也被全球100多家网络运营商所接受。

图 1: Symbian平台上的Qt
Qt是跨平台的应用和用户界面框架,它能让开发伙伴们所编制的应用部署到桌面、移动,及嵌入式操作系统中,而无需重写源代码。自Qt 4.6起,Qt也将Symbian平台作为编译目标;Qt应用可以运行于Symbian平台终端及一些早期的S60终端(自S60 3rd Edition FP1)上。如图1所示,针对Symbian 平台的Qt架构于本地Symbian C++平台APIs及其标准C/C++兼容层(Open C及Open C++)之上。

虽然Qt拥有丰富的APIs和开发工具,但某些开发伙伴还是无可避免地需要用到一般Qt或标准C++ APIs都不提供的平台级功能。当目标设备为移动终端时更是如此,目前使用某些重要的移动设备功能(如照相、蓝牙、名片夹等)的APIs还不存在。新Qt APIs已开始应对一些通常意义上的移动用例,但某些开发伙伴还是需要或希望使用本地操作系统级的功能。

在Qt不能提供所需API的场合,建议使用平台特定功能,即编写一个通用跨平台的封装API,并为封装API提供特定的私有平台实现。这一方案使得应用更易于移植, 方案细节将在#平台特定实现一节中讨论。

对终端特定功能的调用并非就是使用大家熟悉的标准C++ APIs和语法去调用手机特定APIs这么简单。Symbian的本地编程语言是Symbian C++,这是C++的一个变体,经过演化后能应对资源受限设备的需求。Symbian C++使用一些编程语法和框架以促进代码的强壮性和高效性,有时候则会牺牲一些可用性。它拥有其自己的异常处理机制,并在创建时省略了一些用不到(或被认为过分重量级)的标准C和C++库。

来自C++不同变体的代码可以融合,但却要小心对待,同时对Symbian C++要有所了解。本文讲解了如何协调对不同的异常处理机制、字符串、几何图形、容器、图像、数据,及多任务方案。相关每一节都简要介绍了在不同平台上的做法(且给出一些重要的参考资源链接),接下来则是融合方面的一些范例和讨论。Qt和Symbian C++开发伙伴们应能很好地了解如何使用其现在所掌握的技术,也能了解在哪里获取进一步的信息。

开始讨论跨平台兼容机制之前,我们在下面小节中对一些范例代码作概要讲解,以展示一些要点。

蓝牙发现范例

概述

这个范例代码包括BluetoothLibrary.dll,及调用该dll的一个测试应用testbluetoothdiscovery.exe

BluetoothLibrary.dll导出一个Qt API (BluetoothDiscovery) 用于发现附近的蓝牙设备。BluetoothLibrary.dll具有其私有的Symbian C++实现,也具有针对其它平台的stub实现。BluetoothLibrary.dll还会导出一个使用方便的Qt对话框,用户可从中搜寻并选择一个或多个设备。

这个测试应用是GUI主视窗应用,它显示一个空屏幕作为起步。它具有功能键菜单选项用于启动单选和多选对话框。

下图展示诺基亚5800上针对单个和多个设备的选择对话框,及显示各主要组件间关系的架构图。

API

图 5: 主类

BluetoothDiscovery提供了一些槽,用于启动及终止对附近设备的搜索,也提供了一些信号以便在启动或终止搜索时、当探测到某个新设备时,以及出错时,通知所连接的客户端。

搜寻过程一旦启动就会一直持续到底层平台确定不存在更多的设备;这时它会发出信号,表示发现过程已经终止。当出现错误事件时发现过程也会停止,这时一个本地定义的枚举出错码被作为信号发向客户端。

对话框QBluetoothRemoteDeviceDialog 类给出了两个静态方法用于启动对话并返回用户选择:第一个方法返回一个单选,而第二个方法则允许用户选择多台设备:

static QBluetoothAddress QBluetoothAddress::getRemoteDevice ( QWidget * parent = 0);
static QList<QBluetoothAddress> QBluetoothAddress::getRemoteDevices ( QWidget * parent = 0);

除了对话类,还创建了一些相关的Qt类,如用于设备地址的QBluetoothAddress,用于代表一个远程设备的QBluetoothRemoteDevice,及用于枚举定义可能的设备类型和相关服务的QBluetooth等。

图5是简化了的公共API图。请注意其中并没有出现QBluetoothAddressQBluetoothRemoteDevice类。

该范例的对话框是可扩展的;它并不允许过滤掉所出现的设备(基于设备类型、服务类型或服务发现协议SDP),也不允许过滤掉用于搜寻目的的本地设备规范,它忽略了传递给自己的构造函数的窗口标志,也不会显示表示设备类型的图标。

范例的编译和运行

编译本范例前你应该已经按 Qt快速起步中的要求配置好了自己的开发环境和配置终端。

向集成开发环境中导入范例的最方便方法就是通过PRO文件:\QtBluetoothDiscoveryExample\qtbluetoothdiscoveryexample.pro,它指定范例的PRO文件和测试代码,也规定了编译构建顺序。本范例既构建于Symbian平台(模拟器和移动终端),也构建于Windows平台,但须注意的是:在Windows上并不能搜索终端,因为范例未提供Windows平台的蓝牙实现。

该范例DLL和测试exe文件通过测试代码安装文件(testbluetoothdiscovery.sisx) 向终端部署。通过选择应用文件夹中的testbluetoothdiscovery 图标在Symbian平台上运行可执行程序。

文件清单

范例包中具有下列目录结构(位于\qtbluetoothdiscoveryexample\文件夹下):

文件夹 文件 说明
BluetoothLibrary\ bluetoothdiscovery (.cpp/.h) BluetoothDiscovery 公共API头文件和源文件
bluetoothdiscovery_stub (.cpp/.h) 针对非Symbian平台目标的私有实现(stub)头文件和源文件(BluetoothDiscoveryPrivate)
bluetoothdiscovery_symbian (.cpp/.h) 私有Symbian平台实现头文件和源文件(BluetoothDiscoveryPrivate)
BluetoothLibrary.pro 针对BluetoothLibrary.dll的工程项目文件
globalbluetooth.h 全局 #defines,用于定义DLL导入/导出的exports开关
qbluetooth.h QBluetoothQBluetooth头文件(被其它类使用的公共枚举值)
qbluetoothaddress (.cpp/.h) QBluetoothAddress公共头文件和源文件
qbluetoothaddressdata.cpp QBluetoothAddress数据实现(用于隐式共享)
qbluetoothremotedevice (.cpp/.h) QBluetoothRemoteDevice公共头文件和源文件
QBluetoothRemoteDeviceDialog.cpp QBluetoothRemoteDeviceDialog公共头文件和源文件
QBluetoothRemoteDeviceDialog.ui QBluetoothRemoteDeviceDialog Qt designer文件
eabi\ bluetoothlibraryu.def DLL的def定义
testbluetoothdiscovery\ main.cpp 测试应用的主入口
testbluetoothdiscovery (.cpp/.h) 测试代码公共源文件和头文件
testbluetoothdiscovery.pro 测试代码项目工程文件
testbluetoothdiscovery.ui 测试代码Qt designer文件

已知问题

该范例中还存在着一些已知问题:

  • 本文编撰时标准Qt进度条不会动(Qt的bug)。
  • 在N95上,对话框的设备列表不会有焦点。

特定于平台的实现

虽然Qt提供了丰富的API集,当撰写本文时,Symbian开发伙伴们却还看不到调用蓝牙或红外的Qt APIs,看不到能访问照相机、位置传感器、加速度计、生物特征测量仪或其他传感器的Qt APIs,也没有能发送短信或彩信,或读写如日历或名片夹等用户数据的Qt APIs。Qt开发框架正致力于通过Qt Mobility项目为这些问题提供跨平台的APIs。但如果你现在希望使用这些功能,也许需要Qt和Symbian C++的混合编程。毫无疑问今后出现的其它功能也会出现同样问题。

对于Qt未能提供所需API的场合,建议创建一些具有特定私有平台独立实现的公共的Qt APIs,从而用于访问平台特定功能。这一方案使大家能比较方便地采用友好的Qt编程语法来编写大量的代码,同时仍然能方便地进行平台移植。事实上,这正是Qt用来对底层操作系统进行抽象的解决方案,它使开发者无需关心每一个平台的底层编程语法、APIs、安装机制、构造系统和其它各种限制。

有许多种设计模式可用于构建你的代码。在下一节中,我们将讨论Pimpl (pointer to implementation) 惯用法,其中特定于(私有)平台的一些实现被隐藏于公共API的某个指针身后。这项技术又有一些变化/替代名,包括:“Handle/Body”, “Compiler Firewall”, “the Bridge”, “Opaque Pointers”,及“Cheshire Cat”。

也可以使用其他一些模式或技术。例如,大部分的QPixmap API是以某种通用的源文件实现的,但QPixmap::grabWindow()却以一些平台特定的源文件实现。这个方案是可接受的,只要这种平台特定实现不“泄漏”到Qt API中。

此外,#平台特定方法小节也介绍了一些展露某些平台特定细节信息的用例。

“指针到实现”模式

概述

图 6: PIMPL类图
PIMPL是Handle-Body模式的一个变形,在其中的公共API中含有一个指向其私有实现类的指针。这个指向私有实现的指针在头文件中被前向声明(而非#included)因此对该公共API为非透明。Pimpl一词由Herb Sutter在Pimpls - Beauty Marks You Can Depend OnThe Joy of Pimpls (或, More About the Compiler-Firewall Idiom)中提出。

如果这个私有类需要调用位于公共类中的方法,我们就把对该公共类的一个指针/引用传递给私有类的构造函数。如果它需要调用公共类的某些私有方法(如为了发出信号)我们也可将其做成公共类的类。图6中的类图展示了它们之间的关系,说明了拥有一个私有实现类QMyClassPrivate的公共类QMyClass

该公共类的实现负责构造(并销毁)那个私有实现。为达此目的它需要知道QMyClassPrivate的定义,所以它基于平台定义来#include针对当前平台的头文件,如Q_OS_SYMBIAN(出自qglobal.h):

//qmyclass.cpp
...
#ifdef Q_OS_SYMBIAN
#include "qmyclass_symbian.h" //Symbian definition of private class
#else
#include "qmyclass_stub.h" //Stub for all other platforms
#endif
...

注意:在项目工程文件中的平台特定代码段,相关私有类头文件和源文件需要明确指定,请见下面的#工程文件一节。

私有类的定义和实现几乎完全由开发者自己决定。唯一的硬性限制就是:所有平台都必须使用同样的名字(否则我们就需要在公共头文件中为每一个平台前向定义或友元声明私有类)。典型地,私有类还具有一些相同的函数,这使我们能实现非平台绑定的公共类:

// Implementation of a public class slot
void QMyClass::mySlots()
{
d_ptr->mySlots();
}

私有类可以具有任意的继承关系。

{{Note|虽然各种私有类常常继承自QObject ,但并非一定必需(如果你希望你的实现具备信号和槽,这也是有用的)。要注意的是,Qt代码行经常会将QObjectPrivate用于私有实现。由于这并非公共API的一部分,第三方应尽力避免。

关于Symbian私有实现有两种基本的类设计,分别如图7和图8所示。在第一种方案中,私有实现类是一个Symbian类,继承自{{Icode|CBase。而在第二种方案中,私有类使用了一个或多个能回调该私有类的Symbian类。

如用第一种机制,开发者投入的精力会较少些,因为你无需额外间接创建/调用Symbian类,也无需获取回调类的完成通知。然而这种方案却产生了比较复杂的对象结构(理由请见下文) ,而且更难以在概念级别上对Qt和Symbian C++代码进行分离。下面几节中讲解的范例代码就用到了这种方案。

使用第二种方案往往更好。它导致两种编程语法的清晰分离。针对私有类实现需要使用一些Symbian类的情况,这同样也是较好的解决方案。#回调APIs 一节详细讲解了第二种方法。

蓝牙范例公共API

图 9: BluetoothDiscovery 类图
公共BluetoothDiscovery类如下所示。它具有一个指向私有实现友元类BluetoothDiscoveryPrivate的指针。

类图(图9) 展示了既针对Symbian实现的私有类,也展示了针对其他平台stub实现的私有类。这些私有类共享同样的名字,并具有该公共类中槽和方法的超集。请注意,它们并不重新实现公共类中的信号,因为这些都由工具链解释实现,私有类要做的是:通过其构造时所获得的指针来调用公共类的emit

//Forward declarations
class BluetoothDiscoveryPrivate;
 
class BluetoothDiscovery: public QObject
{
Q_OBJECT
public: //enums
enum BluetoothDiscoveryErrors
{ BluetoothNotSupported, BluetoothInUse, BluetoothAlreadyStopped,
BluetoothNotReady, DiscoveryCancelled, UnknownError };
public:
BluetoothDiscovery(QObject *parent = 0);
virtual ~BluetoothDiscovery();
 
public slots:
void startSearch();
void stopSearch();
 
signals:
void newDevice(const QBluetoothRemoteDevice remoteDevice);
void discoveryStopped();
void discoveryStarted();
void error(BluetoothDiscovery::BluetoothDiscoveryErrors error);
 
private: // Data
BluetoothDiscoveryPrivate *d_ptr; //private implementation
 
private: // Friend class definitions
friend class BluetoothDiscoveryPrivate;
};

工程文件

源文件针对平台的特定编译通过工程文件得到控制。

公共类头文件和源文件都在一般描述段中指定。平台特定头文件/源文件则在平台特定代码块中指定,如下所示。

...
HEADERS += qbluetoothaddressdata.h # public header
SOURCES += bluetoothdiscovery.cpp # public class implementation
...
 
symbian {
...
HEADERS += bluetoothdiscovery_symbian_p.h # Symbian private class header
SOURCES += bluetoothdiscovery_symbian_p.cpp # Symbian private class source code
LIBS += -lesock \
-lbluetooth
TARGET.CAPABILITY = LocalServices \
NetworkServices \
ReadUserData \
UserEnvironment \
WriteUserData
}
else {
HEADERS += bluetoothdiscovery_stub_p.h //private class declaration for other platforms
SOURCES += bluetoothdiscovery_stub_p.cpp //private class source for other platforms
}

平台特定代码块列出了代码所要链接的那些平台库,在本例中就是esock.dllbluetooth.dll

各种Symbian实现必须规定可执行代码所需要的capabilities(更多信息请参阅:平台安全(Symbian C++基础)一节)。本例中我们仅指定能授予自签名应用的那些capabilities。

公共类实现

公共类实现需要私有类头文件,以便构造私有类。我们根据当前平台有条件地导入头文件:

//bluetoothdiscovery.cpp
...
#ifdef Q_OS_SYMBIAN
#include "bluetoothdiscovery_symbian_p.h" //Symbian definition of BluetoothDiscoveryPrivate
#else
#include "bluetoothdiscovery_stub_p.h" //Stub for all other platforms
#endif
构造

公共类在其构造器中创建私有实现类的一个实例,如下所示:

BluetoothDiscovery::BluetoothDiscovery(QObject *parent)
: QObject(parent)
{
#ifdef Q_OS_SYMBIAN //Symbian specific compilation
QT_TRAP_THROWING(d_ptr = BluetoothDiscoveryPrivate::NewL(this));
#else
d_ptr = new BluetoothDiscoveryPrivate(this);
#endif
}

请注意,我们在公共类实现中已选择使用平台特定构造方案,这是因为私有类是一个继承自Symbian CBase的类。这是可以的,因为Symbian类实现在这个公共API中不可见。然而,一般说来,我们还是推荐将尽可能多的平台特定实现放入私有类中。

NewL()是标准的Symbian静态工厂类,用于创建BluetoothDiscoveryPrivate 型对象,它确保该对象被成功分配创建,或者当出现异常时能被适当地清除掉。由于它会出现异常,我们将其封装到一个QT_TRAP_THROWING中以便将Symbian异常转换成一个Qt异常抛出。#异常和出错处理一节中讲解了如何处理Symbian和Qt代码间的异常。

如果你用一个继承自CBase的类来实现这个私有类,那么构造过程就比较简单,而且可以具有一个通用的公共类实现。

//Public class
BluetoothDiscovery::BluetoothDiscovery(QObject *parent)
: QObject(parent)
{
d_ptr = new BluetoothDiscoveryPrivate(this);
}
 
//Private class
BluetoothDiscoveryPrivate::BluetoothDiscoveryPrivate(QObject *parent)
: d_ptr(parent)
{
QT_TRAP_THROWING(symbianMember = CBluetoothDiscovery::NewL(this));
}

也可以用其他方法来分配创建Symbian对象。不管使用哪种方法,重要的是记住:

  1. Symbian C类重载了继承自CBase的new方法,new方法并不会抛出异常。因此如果采用new方法构造,就需要使用q_check_ptr来检查这个指针(并当其值为Null时抛出)。
  2. 确保当构造失败时该对象能被恰当地清除。

如果某个继承自CBase的对象在使用前无需初始化,上述第一点适用。在这种情况下,你可以使用new构造,但是你必须检查指针并当其值为NULL时异常抛出。

第二点很重要,因为它并不总是显而易见的,特别当使用Qt和Symbian C++进行混合编程时,需要考虑是否能恰当地删除对象。请看下列代码:

BluetoothDiscovery::BluetoothDiscovery(QObject *parent)
: QObject(parent)
{
d_ptr = q_check_ptr(new BluetoothDiscoveryPrivate(this)); //from Qt 4.6
#ifdef Q_OS_SYMBIAN
QT_TRAP_THROWING(d_ptr->ConstructL());
#endif
}

如果ConstructL()出现异常,QT_TRAP_THROWING这个宏就会抛出异常。按照一般C++的规则,BluetoothDiscovery分配的内存被释放,但是不会调用其析构函数。因而,BluetoothDiscoveryPrivate 中任何被部分分配资源的对象都会造成内存泄漏。为此,应将d_ptr 设置成一个智能指针( QScopedPointer)。

该公共类必须在其析构函数中删除掉这个私有实现对象。

方法

我们在私有类中复制了公共API接口PI。

一些公共方法通过这个指针到实现调用私有类的对等方法,即:

// Called to start searching for new devices
void BluetoothDiscovery::startSearch()
{
d_ptr->startSearch();
}

请注意,我们并未隔离Qt和Symbian C++异常处理机制。这是因为,我们从私有类实现中了解到,startSearch()并不会出现Leave异常。这个方法会抛出Qt异常,这是可接受的,因为从来不会从私有类的Symbian代码中调用它。

私有平台实现

类声明

下面是Symbian平台的BluetoothDiscoveryPrivate实现。这是一个Symbian活动对象。由于这是一个Symbian C++类,遵循#Symbian 编码标准就特别重要:

  • 使用Symbian机制构造
  • 如果使用多重继承,首先从继承自CBase 的类继承,唯一例外则是继承Mixin(接口)类。

Warning.pngWarning: 切勿同时继承CBaseQObject ,因为用于构造的new并不确定。

class BluetoothDiscoveryPrivate : public CActive
{
public:
//static factory "constructor" function
static BluetoothDiscoveryPrivate* NewL(BluetoothDiscovery *aPublicAPI = 0);
 
~BluetoothDiscoveryPrivate(); //Destructor
 
public:
void startSearch(); //Called to start searching for new devices
void stopSearch(); //Called to stop searching
 
public: //From CActive
virtual void DoCancel(); //Implements cancellation of an outstanding request.
void RunL(); //Handles an active object's request completion event.
 
private:
BluetoothDiscoveryPrivate(BluetoothDiscovery *aPublicAPI = 0); //constructor
void ConstructL(); //Second phase constructor - connects to socket server and finds protocol
 
//Error translator - converts global errors into local format then emits to parent object
void ErrorConvertToLocalL(int err);
 
private: // Data
 
RSocketServ iSocketServ; //Socket server connection
RHostResolver iHostResolver; //Host resolver
TNameEntry iCurrentDeviceEntry; //The entry of the device just returned.
TInquirySockAddr iInqSockAddr;
TProtocolDesc iProtocolInfo;
 
BluetoothDiscovery *iPublicBluetoothDiscovery; //pointer to parent object (from constructor).
};

这个类拥有指向其父类BluetoothDiscovery 的指针,当活动对象完成或出错时,该指针会向Qt客户端发送信号。

这个私有类复制了公共类的API,减少了公共类实现中的状态编码。另外,它还重新实现了CActive类的虚拟方法RunL()DoCancel() ,用于处理该活动对象操作的完成和取消。

这个类具有一些私有数据成员,用来从Symbian的Bluetooth.dll获取远程蓝牙设备的名称和id。iHostResolver 就是包含异步请求函数的对象。

构造

因为这个私有实现是一个Symbian类,如前所述,我们使用两步构造的方法。

公共静态NewL()工厂函数使用一个会leave的构造函数来创建这个对象,将其推入清除堆栈,并使用会抛出异常的第二阶段构造函数对其作初始化,最后将其从清除堆栈中弹出并将返回给用户。

BluetoothDiscoveryPrivate* BluetoothDiscoveryPrivate::NewL(BluetoothDiscovery *wrapper)
{
BluetoothDiscoveryPrivate* self = new (ELeave) BluetoothDiscoveryPrivate(wrapper);
// push onto cleanup stack in case self->ConstructL leaves
CleanupStack::PushL(self);
// complete construction with second phase constructor
self->ConstructL();
CleanupStack::Pop(self);
return self;
}

作为构造的一部分,我们也向这个私有对象提供一个指向公共类对象的句柄 – 此处即一个指针,但也可以是一个引用。该指针可用于直接调用公共类的一些方法,或发出一些公共类信号。

方法

私有类方法的实现依赖于所需实现的功能。总体上,最需关注的重要事情是:平台异常处理系统间的正确交互,如异常处理节所述。

也请注意void ErrorConvertToLocalL(int err);的使用,它将Symiban的全局出错信息转换为针对Qt的本地出错信息。

#将活动对象转换为信号和槽一节以实例讨论了RunL()DoCancel()方法。

平台特定方法

在一切可能场合,公共APIs都应避免平台特定成员和函数。这一规则极少会有例外!

偶尔,平台特定的帮助器(helper)函数会被设为公共的,不然的话开发伙伴就需要有其自己的方法。比如,QPixmap就提供了能对CFbsBitmap做双向转换的帮助器函数:

与之相似的是,有时候必须公开一些平台特定类型,并公开地包含一些平台头文件,以便将它们映射到通用的typedefs(类型定义)中。你可以参阅QProcess中的相关内容,在此,Q_PIDtypedef为Symbian平台上的TProcessId

在所有情况下,平台特定的相关声明都必须被定义为仅在特定平台内可见。比如,在QPixmap声明中:

#if defined(Q_OS_SYMBIAN)
CFbsBitmap *toSymbianCFbsBitmap() const;
static QPixmap fromSymbianCFbsBitmap(CFbsBitmap *bitmap);
#endif

文件和类的命名惯例

通常,如果公共类名为QMyClass ,那么:

  • 私有类就被定义为QMyClassPrivate
  • 公共类的源文件和头文件共享公共类名:qmyclass.h,qmyclass.cpp
  • 私有类的头文件和源文件名以_p 结尾(比如qmyclass_p.h),除非该文件是一个平台特定实现。
  • 平台特定实现的头文件和源文件名中包括平台名 – 如qmyclass_symbian.cpp (不必在结尾处添加_p ,因为已经暗示)。

多任务处理

多任务处理是同时执行一项以上的任务。对于GUI应用来说这十分重要,因为它允许执行费时或耗费计算的操作,却仍能响应用户输入。

有两种类型的多任务处理:抢占式和协同式。在抢占式多任务处理(或称作“多线程”)机制中,操作系统向每个执行线程提供一段运行时间 – 线程本身不能控制其运行的时刻和时长。在协同式多任务处理机制中,某个调度程序控制下一个运行任务,但当前任务自主决定何时完成。

抢占式多任务处理机制在内存占用和执行速度上显得比较重量级,而且其编程更为困难,因为需要对共享资源的访问进行调度(并确保各线程不会死锁)。协同式多任务处理机制则较为简单 – 因为对资源的访问是按序排列的。但是,各个任务的运行时间都必须够短,以便让用户界面能保持响应。

在多任务操作系统中我们使用术语进程来表示一组共享同一个全局内存空间的线程,因而它们可以直接地相互访问进程变量;应用中的所有线程通常运行于同一个进程中!请注意,这些是线程,而非进程,它们被调度执行。

一个多任务操作系统也可以是多重运算的,多重运算是指:线程可以运行在一个以上的处理器/CPU上。

Symbian平台上的多任务处理

Symbian平台是一种现代型抢占式多任务操作系统。

应用被创建在自己的进程中,且运行于某个单一主线程中。内核程序以抢占方式根据线程的优先级在系统中调度所有线程。虽然Symbian也可以创建二级线程,我们却强烈推荐应用程序用活动对象协同地实现多任务处理。

几乎所有的Symbian服务都由其它进程中所运行的服务器(或“后台程序”)提供(包括文件服务器、窗体服务器、字体和位图服务器、位置服务器等)。这些服务器通常会输出一个包含TRequestStatus对象引用的异步API,服务器使用该对象告知请求完成。活动对象提供了统一而轻量级的方式来编写代码以发送那些异步请求并处理其完成信息。

活动对象都继承自CActive,这个类对象拥有或具有指向服务器提供程序的一个句柄。这个活动对象必须在其构造函数中将该对象加入到活动调度器中,并提供将自己设置为活动状态的方法 - 首先调用异步方法(传入TRequestStatus iStatus,然后调用CActive::SetActive(). 。当异步服务结束后,会向该活动对象线程发出信号并更改该对象的iStatus状态,表示其已不再处于等待状态。活动调度器稍后会调用该对象的RunL() 方法(你必须实现该方法)以完成服务。注意这并非立即实施– 这些活动对象都以协同模式进行多任务处理,所以调度器一次只能运行一个对象,并在最后一个对象完成后才结束。最后,开发伙伴必须实现虚拟DoCancel()方法,该方法取消异步请求,并确保在活动对象的析构函数中调用了Cancel() 方法。

以上的概要性介绍仅涉及到活动对象的一些皮毛。开发伙伴们可阅读文章《活动对象(Symbian C++基础)》《协同式多任务处理(Symbian C++设计中的最佳实践)》。如果你对使用或实现各种服务感兴趣,你也许需要阅读《客户端服务器》一文。

喜欢用线程和进程的开发伙伴们当然可以使用线程和进程 – 某些情况下也必须这么做。可以分别使用 RProcessRThread 来创建并操作Symbian C++的进程和线程。Symbian C++有一些常规的同步元类,包括互斥量(RMutex)、信号灯(RSemaphore)、临界区Sections (RCriticalSection) 等。文档 《 Symbian C++基础》中的线程、进程,和IPC一节讨论了所有这些类。有兴趣了解这些类的实现方法的开发伙伴们可参阅《深入Symbian OS》 一书的第三章(同样内容也可以在Symbian Foundation维基百科:深入Symbian OS/3,线程、进程和库)中看到)。如果你正在用标准的C++语言编程(通过Open C和Open C++),那么你也可以如常创建pthreads

同一进程中的线程能方便直接地共享数据(请注意对共享数据的按序访问)。其它进程中的线程则需使用Symbian的进程间通信机制进行通信。这些机制包括客户端-服务器、发布和订阅、消息队列,及RPipe. 等。下列文档提供关于这些机制方面的有益讨论:

Symbian^3起,Symbian有望增加对对称多重运算(symmetric multiprocessing – SMP)的支持。

Qt中的多任务处理

Qt应用既使用协同式也使用抢占式的多任务处理。

Qt的主应用线程运行其自己的事件循环程序,处理响应用户交互(按键事件、鼠标事件等)时所生成及来自计时器和窗体系统的事件。这种事件循环程序就是协同式多任务处理的例子 – 各种事件按序排列,并得到同步处理。如果在某个事件上耗时过长,用户界面将不复响应。

如果计算量繁重的操作能被分解为一定数量的多个步骤 – 比如写文件,你可以在操作过程中定时调用QApplication::ProcessEvents(),以便让事件循环程序有时间处理来自其它用户界面的事件。Qt4 C++ GUI编程,第二版,Jasmin Blanchette和Mark Summerfield编撰,Prentice Hall出版 (2006)此处可以免费下载该书第一版)一书第七章讨论了这种方法。

多线程(抢占式多任务)是更为常见的方法。开发伙伴们创建QThread 的子类,并重新实现其run() 函数,以便在新线程中执行代码。一些同步类包括:提供对某项资源进行独占式互访的QMutex ,提供无限制读访问但却阻止写访问的QReadWriteLock,概要化QMutex以实现对规定数量的资源进行访问的QSemaphore,及在条件未满足前阻止访问的QWaitCondition。还有一些帮助器类,如QMutexLocker,它简化了互斥量mutexes编程,也简化了标准C++的异常处理。

Thread Support in Qt|Qt中的线程支持概要讲解了一些线程类,也提供了对同步线程可重入性和线程安全线程和QObjects并发编程,和Qt模块中的线程支持等主题的各种链接。以下是一些优秀范例:Mandelbrot范例Semaphores范例,和等待条件范例Qt4 C++ GUI编程,第二版,Jasmin Blanchette和Mark Summerfield编撰,Prentice Hall出版 (2006) 第14章中有关于多线程方面的精彩讨论(与其它链接中的内容有某些重复)。

线程使用共享内存和上述同步类开展相互间的通信。线程可以使用信号和槽与主线程通信。然而请注意,这些信号在默认情况下并不考虑同步,因为它们都在某个单一线程内通讯。

应用也可以使用其它一些进程实现多任务。比如,可以创建一个QProcess用于启动另一个进程,设置其命令行参数,并侦测其启动、出错,和完成状态。

你也可以使用标准的C++线程、进程及进程间通信机制。

将活动对象转换成信号和槽

一起使用Qt和Symbian C++时,大家可能需要异步API来调用某项Symbian服务。最好的办法就是将请求封装到一个活动对象中,然后使用一个Qt信号通知Qt代码:该项请求已经完成。

将活动对象完成事件转换成信号和槽非常直截了当:

  • 使用PIMPL方法创建一个公共的Qt API
  • 创建一个活动对象,其或是Symbian私有实现类本身,或是作为由私有类拥有的对象。
  • 将该公共类中的函数/槽映射到私有类中启动该活动对象的函数。
  • 通过活动对象的指向公共实现对象的指针,在成功完成任务时发出信号。(显而易见)仅向Qt 对象发送信号。
  • 出错时发送信号。请注意Qt使用模块化本地出错信息,所以你要将来自底层接口的全局(或本地)出错信息翻译为模块公共接口中所定义的本地错误信息。

Note.pngNote: 在某些情况下,有可能进行从Symbian对象到Qt对象的零拷贝(zero-copy)传递:比如,初始化一个QString时,用到一个指针,并指向异步服务的缓存。如同C++语法,必需小心确保你所传递的对象的生命周期超过其所有使用者。Qt和Symbian C++对内存管理有各自不同的方案,通常最好是去创建一个“全新”的Qt样式对象。

前文所述的蓝牙发现范例是一个活动对象,所以你已经了解到如何构造对象、如何传递公共类的指针,及如何启动对象(通过公共类的startSearch() 调用私有类的startSearch() )。

关于活动对象,其实在活动对象(Symbian C++)中已有完整描述,余下需要讨论的只是:如何向Qt API发回你的完成信号。

从活动对象发送信号

通过调用公共类的信号发送函数,使用构造函数中传递过来的指向父类对象的私有指针,既可以发送出错信号,也可以发送事件信号。我们所发送的对象应该是Qt 对象。所发送的错误代码应该是当前Qt组件的本地错误代码(也就是在其公共头文件中所枚举的定义,而非全局的Symbian错误代码)。

所发送的信号可以被连接到任意数量的槽。这有两个含义:

  • 运行于非抢占式活动调度程序(RunL) 内部的槽其运行时间必须够短。如果所有槽所花费的总时间太长,用户界面可能会无响应(更糟的话则会死锁)。
  • 任何连接到该信号的槽都有可能抛出异常,然后被扩散回信号发送调用者。因此我们需要针对可能被扩散到的Symbian信号调用代码,使用QT_TRYCATCH_LEAVING 隔离函数。

下面是RunL()代码段,当异步服务完成时该方法得到调用。调用成功后它会创建一个新的QBluetoothRemoteDevice,并通过对等的Symbian C++类的信息构建,并在我们的信号中将其发送。如果出错,我们调用ErrorConvertToLocalL()将其转换成模块特定的Qt出错信息。

RunL()

/**
Handles an active object's request completion event.
*/

void BluetoothDiscoveryPrivate::RunL()
{
if (iStatus == KErrNone) {
...
//Create a QBluetoothRemoteDevice and populate it with
//information from the asynchronous service
QBluetoothRemoteDevice remoteDevice(qtBtDeviceAddress);
...
//emit the device as a signal from the public class
QT_TRYCATCH_LEAVING (emit iPublicBluetoothDiscovery->newDevice(remoteDevice) );
//Search for the next device
iHostResolver.Next(iCurrentDeviceEntry,iStatus);
SetActive();
}
else if (iStatus == KErrHostResNoMoreResults) {
//No more devices to detect
QT_TRYCATCH_LEAVING (emit iPublicBluetoothDiscovery->discoveryStopped() );
}
else {
//Error. Emit "discovery stopped" signal and then translate
//to local error (which is also emitted within the function)
QT_TRYCATCH_LEAVING (emit d_ptr->discoveryStopped() );
ErrorConvertToLocalL(iStatus.Int());
}
}

SetActive()

私有类的方法startSearch() 用于启动活动对象。它检查该对象是否处于活动状态,在一个异步服务提供程序上创建一个服务请求,然后调用CActive::SetActive()将该对象设置为活动状态。

另外,该方法会发送一个信号,提示客户端对象已经启动,即使出错信号也会发出。在此情况下,我们无需围绕信号发送的调用函数使用隔离函数,因为尽管信号发送函数会抛出异常,该函数却只能被Qt API调用。stopSearch()的情况与之类似。

void BluetoothDiscoveryPrivate::startSearch()
{
...
emit iPublicBluetoothDiscovery->discoveryStarted();
...
}

DoCancel()

可以调用CActive::Cancel() 来取消一个异步请求,处于活动状态下的活动对象会接着调用CActive::DoCancel()需要实现该函数)。必需从活动对象的析构函数调用Cancel()来取消任何未完成的请求- 如果某个已被删除的对象仍处于等待信号状态,活动调度程序将会崩溃。

在下面的例子中,DoCancel() 发送一个信号,表示终端搜寻已停止,这是一个潜在的可抛出异常的操作(信号可被连接到任意槽,任何被连接的代码都可能抛出异常)。正如#融合Symbian C++ 和Qt 的异常处理一节中所讨论的,因为该方法是在一个析构函数中被调用的,我们应尽可能地避免使用throwing或leaving代码。基于这种考虑,我们假定这个emit是无可避免的,因此(仅)须捕获所有标准的异常。

void BluetoothDiscoveryPrivate::DoCancel()
{
//Note that must trap any errors here as
// Cancel() is is called in destructor and destructor must not throw.
try {
emit iPublicBluetoothDiscovery->discoveryStopped();
}
catch (std::exception&) {}
 
iHostResolver.Cancel();
}

回调APIs

Symbian C++ 提供了一些APIs,它们使用回调方式发送异步服务的完成信号(其实这些APIs都是作为活动对象实现的;使用回调简化了API在客户端代码的用法)。这方面的一个优秀范例就是CMdaAudioPlayerUtility,该范例在其静态工厂构造函数中使用了一个MMdaAudioPlayerCallback 实例,并调用其中的一些方法来指出实例化及播放过程。

封装一个Symbian C++回调比直接使用一个活动对象还简单。你又可以使用PIMPL以一个私有实现类来创建一个Qt类。在这里,这个私有实现类将拥有Symbian类的一个实例,并实现其回调接口。

class MyAudioPlayerPrivate : public QObject, public MMdaAudioPlayerCallback
{
public:
QMyAudioPlayer (MyAudioPlayer *wrapper = 0);
~QMyAudioPlayer ();
 
public:
//methods, slots intiate play (duplicate API of the public class)
 
public: //From MMdaAudioPlayerCallback
virtual void MapcInitComplete(TInt aError, const TTimeIntervalMicroSeconds &aDuration);
virtual void MapcPlayComplete(TInt aError);
 
private:
void ErrorConvertToLocal(int err);
 
private: // Data
CMdaAudioPlayerUtility *iAudioPlayer; //The audio player object
MyAudioPlayer *d_ptr; // pointer to Qt public API
};
 
 
MyAudioPlayerPrivate::MyAudioPlayerPrivate (MyAudioPlayer *wrapper)
: d_ptr(wrapper)
{
QT_TRAP_THROWING(iAudioPlayer=CMdaAudioPlayerUtility::NewL(this));
//note, throws if can't construct object
}
 
MyAudioPlayerPrivate::~MyAudioPlayerPrivate()
{
delete iAudioPlayer;
}
我们可以在回调函数中发出任何信号:
MyAudioPlayerPrivate::initialisationComplete()
{
QT_TRY {
emit d_ptr->initialisationComplete();
}
QT_CATCH (std::exception&) {}
}

请注意,上面的代码会捕获异常,却并不会去处理它们,也不会重新抛出异常:通常认为这种方式并不好!但是,我们却受到了API的限制 -不能有Leave异常,因为其原型指出,该API仅用于无异常场合,不能再抛出异常,因为这样异常就又会发回Symbian代码。直接消化错误就是我们的唯一选择。

Warning.pngWarning: 请务必记住:任何回调都可以从某个RunL()的场景内运行。如果你的Qt代码可能在一个出现异常的回调中发生Leave异常,就应该将其封装到QT_TRYCATCH_LEAVING方法中。

编码标准和惯例

Symbian 编码标准

Symbian浩繁的编码标准和惯例在文档编码标准和惯例中有详细讲述。如果你用Symbian C++在该平台上进行开发(并不打算直接向该平台贡献源代码),你会发现编码标准快速入门是很好的起点。

这些编码标准涉及到一些规则,这些规则关乎代码格式化、安全性、易维护和效率等。开发伙伴们可以合理忽略那些与格式化代码相关的标准,然而其它那些惯例对以Symbian C++编写强壮高效的代码却不可或缺。

Qt 编码标准

Qt提供一组简洁的编码指引,被分为编码样式编码惯例 两个文档。前者定义了代码格式/排版方面的最佳实践,而后者则讨论了不应该使用的那些C++特性、头文件的导入方式、类型转换、保持二进制兼容,及编译器特定问题等。如你所期望,着重点在于编码,以保证跨平台兼容性。

Tip.pngTip: 如果你使用Carbide.c++,你可使用Qt_Code_Style.xml 模板让你的工程遵循默认的Qt编码样式。如欲添加该模板,请在Carbide.c++菜单中通过Window > Preferences... > C/C++ > Code Style下载并导入该模板文件。

融合Symbian C++和Qt代码的编码标准

Qt应用程序应遵循Qt编码样式规则,同时遵循既适用Symbian C++也适用Qt代码的排版要求。否则,Qt代码须遵循Qt惯例,而Symbian C++代码则应遵循Symbian 编码标准

本文所附File:Qtbluetoothdiscoveryexample.zip中的范例就遵循这种方法。该规则的例外是:BluetoothDiscoveryPrivate类是一个SymbianC类(继承自CActive),本该以前缀C命名。而在本例中,类名必需保持其私有实现的“非透明性”,而不使用类前缀的风险也很低,因为BluetoothDiscovery是其唯一的使用者。

异常及出错处理

Symbian 异常和出错

Symbian C++ 使用自己的异常处理机制,该机制由TRAPs(有些类似于try)、Leaves (有点类似于throw),和Cleanup Stack (允许在某个异常事件中安全地删除本地范围内的堆分配对象)。

Leaving代码是在TRAP宏内潜在运行的。某个Leave事件出现时,调用堆栈展开TRAP宏,自动变量被释放,而清除堆栈删除或清除处于当前TRAP层的任何对象。TRAP宏返回该leave出错码(一个整数),然后立即继续执行(没有分离的“catch”)。与catch代码段很相似,TRAP后的代码用于处理对应的leave代码,并在无所作为时再次Leave传出 。

TInt result;
TRAP(result, MayLeaveL());
 
if (KErrNone!=result) {
// Deal with errors that can be handled at this level
// Leave again with any errors you choose not to handle
}

Tip.pngTip: TRAPs的可执行文件相对较大,RAM消耗也相对较高。它们可被嵌套,并可在任何层被使用,因此通常最好在某个高层中集合一些Leaving函数进行trap。

Symbian针对函数和类类型使用一套命名惯例,帮助用户理解如何安全地分配对象以防止内存泄漏:

  • 可能Leave的函数按惯例以LLC ,表示该方法返回时对象被保留在清除堆栈中)结尾。
  • 在堆上分配的类通常首先继承自CBase,其名字以C开头。
    • CBase类在构造时提供零成员初始化,提供当分配失败时会Leave的的一个重载new(new (ELeave)) ,还提供一个虚析构函数供清除堆栈在Leave时,删除产生Leave的对象(清除堆栈还提供一些方法用于删除非继承自(CBase的其它堆对象,如数组和接口类)。
    • C类使用leave安全的二阶段构造方法构造。
  • T前缀用于栈分配的类,它们不拥有指向堆资源的指针,也没有析构函数。
  • • R前缀用于拥有其它资源的栈类 - 因而要求提供显式的清除支持。

Note.pngNote: 在Symbian的最初实现中,自动变量的析构函数并不会在某个Leave事件中得到调用。结果是,不能将智能指针用于清除本地对象。命名惯例能让开发伙伴们清楚了解这些对象是否要求通过清除堆栈(R、C类)明确支持清除,还是根本无需清除堆栈(T类)。

文档异常和清除堆栈(Symbian C++基础)详细讨论了异常处理机制的工作原理,无异常代码的编制,及命名惯例等。类的类型和声明(Symbian C++基础)讲解了各种不同的类类型,而对象构造(Symbian C++基础)则讲解了leave安全处理的对象构造。

Note.pngNote: Symbian C++问世当初,标准C++的try-catch-throw异常处理机制对于嵌入式操作系统来说被认为太费内存。而且,Psion要用到的编译器所获得的支持也很有限。

Symbian OS v9中新增了对标准C++异常处理机制的支持,这个版本就是Symbian平台的前身,用于诺基亚S60 3rd Edition。

Symbian的异常处理机制现在按trycatchthrow方式实现。开发伙伴们能使用标准C++代码的trycatchthrow,甚至还可以与Symbian C++混合使用(小心!)。这种新异常处理机制及如何与标准C++的异常处理机制协调等内容在文档Leaves和Exceptions的比较混合编码指南中有所探讨。

Symbian函数既可用Leaves也可用错误代码来发出异常信号。当错误状况为“预知”时- 如读到了文件尾,则通常用Leaving来返回错误信息。然而这里并无一成不变的规则(除无内存可供对象分配时类构造过程总是以KErrNoMemory报错),你将看到Symbian C++ API内特定的各种错误代码和Leaves。请注意,让一个函数既出现leave异常,又返回错误信息,或者将一个异常转换成一个错误(无理由地使用一个TRAP),这些都被认为是不好的编程习惯。

文件e32err.h中定义了一组全局错误代码(负整数值)。其它一些错误代码则被分别定义在各个组件中 - 有不少罗列清单,其中之一在www.newlc.com中。

开发伙伴们也应注意,Symbian还定义了被称为Panic的第二级别的异常。Panic用于发送编程错误信号,结果是立即中断应用程序 - 这是错误使用某个API的正确应对方法。

Qt异常和出错代码

Qt支持标准C++的异常处理机制,只要其在底层平台和编译器上获得支持。

虽然Qt从其发展初期就坚持对第三方代码支持这个机制,但Qt框架代码本身在历史上却并没有使用该机制(就是出于跨平台兼容性的考虑)。Qt代码转而使用本地定义的错误代码向客户端代码提示错误状况。当内存分配失败时,框架代码仅仅返回一个空指针,并无法解除对第一个指针的引用(除了某些较大的对象,此时会采取一些特别的处理方式)。

Qt 4.6承诺对Qt容器类和QFile类的基本的异常安全性的保证(组件不变式得到保留,无资源泄漏):

  • 新增了QScopedPointer智能指针,允许在某个异常事件(使用期间或构造中)中清除本地范围对象。
  • 如果无法分配内存,对象现在会抛出std::bad_alloc(使用q_check_ptr()检查所返回的指针)
  • 在合适的地方增加了try/catch代码段

Qt(编撰本文时)并未定义其自身的异常处理类的层次架构,框架代码仅仅抛出std::bad_alloc.。Qt本身仍使用错误代码而非异常来扩散除内存分配失败之外的出错信息。开发伙伴们应该认识到,任何的Qt类几乎都会抛出异常,在其与第三方代码交互的场合,实际上它能抛出任何东西(QScopedPointer的构造函数是一个例外,也就是说,它能安全地用于清除本地范围对象)。当Qt捕获异常时,如果这些异常并未得到处理,Qt一定会将其再抛出。

开发伙伴们应优先考虑使用(在qglobal.h中定义的)宏QT_TRYQT_CATCHQT_THROW,和QT_RETHROW而非直接地trycatchthrow。这将保证异常处理代码只在支持异常机制的平台上编译。请注意,如果你编制的应用对异常是安全的,那么不管是否支持异常处理,它都不会有问题。

Symbian C++和Qt异常处理的融合使用

Qt使用标准的C++异常处理机制,而Symbian C++使用Leaves。当用异常来实现Leaves时,必需小心处理这两种用法的交错。

Symbian异常的安全 一文简要解释了一些问题,并讲解了在这两种用法间进行转换的一些分隔函数。文档Leaves和Exceptions的比较融合编码指南也是有用的参考资料。

这些分隔函数让你能捕获一些标准的异常并将之转换为Symbian的出错码或leaves,以便将其传递给Symbian代码。类似地,也可将Symbian C++的leaves或出错码转换为一些标准的异常。为方便读者,我将这些方法列出供大家参考:qt_symbian_throwIfError()q_check_ptr()QT_TRAP_THROWING()http://doc.qt.nokia.com/4.7/qtglobal.html#qt_symbian_exception2Error qt_symbian_exception2Error()]、http://doc.qt.nokia.com/4.7/qtglobal.html#qt_symbian_exception2LeaveL qt_symbian_exception2LeaveL()]、http://doc.qt.nokia.com/4.7/qtglobal.html#QT_TRYCATCH_ERROR QT_TRYCATCH_ERROR()], QT_TRYCATCH_LEAVING()

这些分隔方法只能捕获和转换标准异常 -应用特定的那些异常则会扩散,并当遇到某个TRAP时导致应用终止。这是预期表现 - 按惯例,代码只能捕获并处理那些能被理解的异常- 所有其它异常都必须被重新抛出。

Note.pngNote: catch (...)QT_CATCH (...)来捕获所有的异常使其不致被扩散到某个TRAP并终止应用程序,这看上去很吸引人。然而,当你这样做的时候错误依然存在 - 你所做的一切只是让应用程序无机会去恰当地处理出错 - 代码也许还在造成内存泄漏,或以不可预知及难以纠正的方式出现故障。

有一些情况值得大家作更仔细的考虑:某个函数不能失败,及某个函数可以调用失败但既不允许抛出异常也不允许leave。在这两种情形中,“调用不能失败”也可理解为“不能以某个leave或标准异常方式调用失败” - 其它一些异常则可以被扩散。

析构函数就是函数不能调用失败(抛出异常或Leave)的一个例子。当实现一个析构函数时:

  • 尝试只使用不会调用失败的函数。
  • 如果必须调用会Leave的函数,请使用某个TRAP宏阻止其扩散。
  • 如果必须调用会抛出的函数,请用QT_CATCH (const std::exception&)来阻止那些标准异常扩散。其它一些异常则必须可扩散 - 如果没有重新抛出异常,就不要QT_CATCH (...)
  • 这样做的合理结果就是,连接到QObject::destroyed()信号的某个Qt槽必须根据前述的所有规则如析构函数代码般实现(它在QObject析构函数中发出信号)。

某些函数会失败,但既不可以抛出异常也不可以leave。比如,不以L结尾的Symbian C++ 回调方法不能leave,因为调用代码可能未按可安全leave的方式编写。它也不可以抛出,因为在更高层中很可能有一个TRAP宏。

  • 如果函数能返回一个错误代码,那么你就能用Qt分隔函数将这些异常或标准异常转换为错误代码。
  • 如果函数原型不允许返回错误代码,那么你可以吸收所理解的错误代码,但是必须抛出其它那些错误代码。
  • 在这两种情况中,非标准异常必须可扩散。

常用Qt和Symbian类型之间的转换

本节讲解如何在一些常用类型间进行转换。

字符串

字符串被用于几乎每一个应用,也很可能是Symbian C++和Qt代码间最为常见的转换类型。

Symbian 字符串

图 10:描述符类的层次结构
Symbian C++用描述符来处理文本和数据。描述符是自说明的:它们使用最少量的内存来储存字符串数据和本身长度及内存布局分配方面的信息。描述符不会自动调整其大小,如果某项操作超过了其缓存长度,它们就会panic- 这提升了很少重启或从不会重启的终端上的代码的强壮性。如前文所述,它们既可用于文本也可用于数据,因为其长度并非通过NULL ('\0')终止符确定。

Symbian描述符类的层次结构很复杂,它具体提供可修改和不可修改的描述符,它们将自己的数据保存于栈或堆中,也提供一些基类,它们用于函数返回类型和参数,但却并非用于实例化(TDes, TDesC等)。甚至还有一些指针描述符(TPtr, TPtrC),它们只是指向那些储存于其它位置的数据。

每个描述符类都有窄(8位)型和宽(16位)型(如,分别为RBuf8, RBuf16。8位窄型描述符主要用于数据,而16位宽型描述符则用于Unicode文本。对于字符串数据,大多数开发伙伴实际上会使用如图中所示的裸型描述符(如RBuf) :这是对所有当前Symbian平台版本上16位变体描述符的一个typedef(类型定义)。

维基百科上有一些关于描述符的优秀文档:如描述符(Symbian C++基础)描述符列表,以及开发伙伴库中的描述符的使用。开发伙伴们还应注意用于语句解析及将数字字符串转换成数字类型的TLex,用于字符的各种typdefs类:TTextTChar,和用于在字符集之间进行转换的Charconv(比Qt所转换的字符集多,并可扩展)。

Qt 字符串

Qt使用单一类,即QString,处理几乎全部的Unicode字符串。这个类提供了开发伙伴们所需要的几乎全部功能,包括串比较、与数字类型的相互转换,及在字符集之间的转换。这个类集成了Qt的正则表达式类,以提供强大的解析和串操作功能。它也能与Qt的多语言国际化APIs无缝协同。

QString在必要时会自动改变大小以容纳更大的字符串(如果该字符串无法重新分配内存,该操作会抛出异常)。QString使用隐式共享(写时复制)以减少内存占用,也避免不必要的数据复制。这意味着,变量被储存在栈中,但相关数据却被储存在堆上。

Note.pngNote: 就内存占用而言,Symbian C++开发伙伴们将看到,将QString(以及任何隐性共享对象)视为引用计数的R-类很有用。对象数据被分配在堆上,那些不会修改数据的复制操作只是增加了计数,而无需创建一个新对象。只有当所有用到该数据的对象都出局后数据才会被释放掉。

QStringAPI 比描述符层次结构要好理解得多,也在多方面更显其强大。QString比描述符类的内存效率要低些,但仍不失相对高效强健。

Qt 还提供QByteArray(一种字节数组),它用于各种字符串操作。QByteArray经常用于数据,将在Qt二进制数据一节中对其进行讨论。

将描述符转换为QString

使用QString::fromUtf16创建一个新的QString ,它深度复制了某个地址上具有规定长度的数据。地址和长度分别用TDesC16::Ptr()TDesC::Length()获得:

QString myString = QString::fromUtf16(theDescriptor.Ptr(), theDescriptor.Length());
这种方式被用在这个范例代码中,将描述符的设备名和地址复制为QStrings,然后将其赋予QBluetoothRemoteDevice(请见BluetoothDiscoveryPrivate::RunL()的实现)。

Note.pngNote: qcore_symbian_p.h 定义了QString qt_TDesC2QStringL(const TDesC& aDescriptor)实现这种转换。因为该头文件不属公共API,你可选择将qcore_symbian_p.cpp 中的源代码复制到自己的工程中。

在很多情况下你会使用上述方法来创建数据的一个拷贝。如果你需要对数据做零拷贝转换,而你又能保证描述符的生命期长于可能的Qt变量,那么你就能使用QString::fromRawData()获得一个指向该描述符数据的QString :

QString myString = 
QString::fromRawData(reinterpret_cast<const QChar*>(theDescriptor.Ptr()),theDescriptor.Length());

含有文本的8位描述符可以先在Symbian C++内部被转换为Unicode然后按上面的方法传递。这样做有好处,因为Symbian的字符转换类允许自动探测字符集,并支持多种字符集之间的“默认(out-of-the-box)”转换。

然而你也许觉得在Qt代码内转换更容易。你可以使用const TUint8* TDesC8::Ptr() const;获取指向该描述符中的数据的一个指针。如果你知道字符集,你就能使用QStringfromAscii()fromLatin1()fromUtf8()来实现转换;如果你仅知数据为本地默认字符集,那么你可以使用:QString::fromLocal8Bit()

将QString转换为描述符

使用QString::utf16()QString::constData()获取指向QString myString 中的数据的指针并将其转换为一个TPtrC16 (TPtrC),如下所示:

TPtrC myDescriptor (static_cast<const TUint16*>(myString.utf16()), myString.length());

TPtrC myDescriptor (reinterpret_cast<const TText*>(myString.constData()),myString.length());

当原来的QString(或任何浅复制)还在生命周期范围内,则指针描述符是有效的。除非你能保证这点,否则你应将该字符串复制到一个堆或缓存描述符中。请使用一个(RBuf)堆描述符:

    RBuf buffer;
qt_symbian_throwIfError(buffer.Create(myDescriptor));

对于缓存描述符你既可以这么做:

TBuf buffer(myDescriptor);

也可以那么做:

TBuf<KBufLength> buffer(text.utf16());

Note.pngNote: qcore_symbian_p.h定义了实现这些转换的HBufC* qt_QString2HBufC(const QString& aString)TPtrC qt_QString2TPtrC( const QString& string ) 。因为这个头文件并不属于公共API,你可选择从qcore_symbian_p.cpp中将源代码复制到自己的工程中。

输入/输出和二进制数据

在本节中我们将简要探讨串行化对象数据、将之保存到文件中、及在终端和线程间传输的机制。

Symbian 二进制数据

一些Symbian C++流式类被用来将对象的内部数据作序列化处理,将其变为一个字节串,以及反之从字节串初始化对象。

需要外部表述的一些对象定义了ExternaliseL()InternaliseL()方法,如下所示(请注意,定义这些方法的类可以使用全局流式操作符>>和<<来流出/流入数据)。

void ExternalizeL(RWriteStream& aStream) const;
void InternalizeL(RReadStream& aStream);

读写对象成员的方法通过ExternalizeL()/InternalizeL() 方法,最终是通过RWriteStream/RReadStream类中所定义的最终与平台无关的一些基本类型表述(包括描述符)对该对象的成员进行读/写。

RWriteStream/RReadStream 都是抽象类。当我们对对象作外向化处理(externalize)时,我们使用一个具体的流将数据发送到某个文件、文件存储区、内存,或某个固定或动态缓存。有些流用其它的流式对象进行初始化 - 如对数据进行压缩或加密。本地Symbian应用通常将其数据储存为基于文件的流“储存(Stores)”,后者用该应用的唯一标识符将自己关联到应用。

这里有一篇优秀文章概要介绍了储存和流:流和储存(Symbian C++基础),有关应用中的使用则请参阅 储存器。还有一个实际样例GUI/引擎代码段向你演示如何使用流机制来保存你的应用数据。

Symbian使用具体的8位变量描述符作为非字符串数据的缓存:TBufC8TBuf8TPtrC8TPtr8RBuf8HBufC8。比如,它们被用作RSocketAPIs的发送和接收缓存。我们也可以使用一个流接口的套接字来使我们的对象直接从某个套接字或向某个套接字进行序列化。

包缓存(8位描述符对齐)被用作线程和进程间的对象传递。这些缓存让开发伙伴们能将任意的值类型(某个T class)打包为一个描述符。请注意,这种方法是可取的,因为我们无需一个平台独立的表述来与另一个线程进行通信。

有三种包缓存变体:TPckgBuf取该对象数据的一份拷贝,而TPckgCTPckg则只是指向const(常量)和non-const(非常量)对象。

Qt二进制数据

Qt的QIODevice是针对“终端”的抽象类,能读写数据块。它有一些子类(包括QTcpSocket, QUdpSocketQBufferQFileQLocalSocketQNetworkReply,及QProcess),这些子类用于向文件、进程、sockets、缓存写入数据。

Qt还提供了较高级的流式类QDataStreamQTextStream,可用于向任何的QIODevice类(分别)流入二进制和文本数据。这两个流式类以某种平台独立(但特定于Qt版本)的方式对数据进行序列化处理。能对数据实施序列化的类需要重载>>和<<操作符, 并操作QDataStream参数变量重载 >>和<<操作(或拥有一个相应方法)。

内存中的8位文本和二进制数据通常被保存在一个QByteArray中。这是一个字节数组,它拥有非常相似于QString类的一个API。请注意,QBuffer类为QByteArray提供了一个QIODevice接口。

输入/输出和二进制类在类文档中有完善的文档资料。《Qt4 C++ GUI编程,第二版,Jasmin Blanchette和Mark Summerfield编撰,Prentice Hall出版 (2006)》 一书的第11章对QByteArray作了很好的概要性介绍,而其第12章则概述了输入/输出类。

Qt和Symbian二进制数据间的转换

Symbian和Qt处理数据序列化方面的类和方案基本一致:数据被以某种平台独立的形式外向化流出到一个流中。在Symbian,这种流可以一个文件或内存缓冲区,而在Qt,这种流与 QIODevice相关联,是一个文件、缓存等。这两种实现的主要区别是:Symbian C++具有很少的一组平台独立类型(在不同版本中保持一致),而Qt则具有较丰富的类型组,其实现因各版本而有所不同。

好消息是:几乎没有什么理由要在Qt和Symbian的序列化机制间进行转换。如果你确实需要传递在一个或其它开发环境中被序列化了的数据,那么首先导入然后再作适当转换(使用casts,或其它一些更为复杂的方法)。

如果你正在处理原始数据,那么QByteArray和描述符间的转换与QString和描述符间的转换却是十分相似 – 如:

TPtrC8 myDataDescriptor( reinterpret_cast<const TText8*> (myQtData.constData()),myQtData.size());
 
//Take a copy of the data
HBufC8* buffer = HBufC8::New(myDescriptor.Length());
 
Q_CHECK_PTR(buffer);
buffer->Des().Copy(myDataDescriptor );

请记住,QByteArray::constData()data() 所返回的数据都属于QByteArray,所以你可能需要如上文所述保留一份拷贝。

还有另外一种转换方法,即使用QByteArray创建描述符中数据的一个深拷贝,或者,如果你知道QByteArray的生命周期将超过其Symbian C++代码中用户的生命周期,使用QByteArray::fromRawData()

QByteArray myQtArray(reinterpret_cast<const char*>(theDescriptor.Ptr()),theDescriptor.Length());

几何类型:点、尺寸、矩形

Qt和Symbian C++定义了相似的几何类型。

TPointQPoint在事实上是一样的。这两者都使用x和y坐标值(类型为TInt (typedefd 为带符号的int)和int )在笛卡尔坐标系中储存一个二维点。

TSizeQSize在事实上也是一样的。这两者都储存宽度和高度值,还是分别使用一个TIntint 。转换过程直观:

QSize myQSize = QSize(myTSize.iWidth, myTSize.iHeight);  //To QSize
TSize myTSize = TSize(myQSize .width(), myQSize .height()); //to TSize

TRectQRect都以坐标系中的一个特定位置定义一个矩形区域。这两者都储存该矩形的左上角坐标。TRect为该矩形储存一个TSize值,而QRect 分别保存宽度和高度。转换过程很直观:

QRect myQRect = QRect(myTRect.iTl.iX, myTRect.iTl.iY, myTRect.Width(), myTRect.Height());  //to QRect
TRect myTRect = TRect(TPoint(myQRect.left(), myQRect.top()),
TSize(myQRect.width(), myQRect.height())); //to TRect

Note.pngNote: qcore_symbian_p.h 定义下列内联函数用于实现这些转换。

  • static inline QSize qt_TSize2QSize(const TSize& ts)
  • static inline TSize qt_QSize2TSize(const QSize& qs)
  • static inline QRect qt_TRect2QRect(const TRect& tr)
  • static inline TRect qt_QRect2TRect(const QRect& qr)
因为该文件并非公共API的一部分,你可以将这些代码复制到你自己的工程中。

请注意,QRect也提供取右下角坐标的机制。这不应被用于转换到TRect,因为一些历史上的原因,它们与矩形的“真正”右下角坐标有所偏离。请查阅QRect文档了解更多信息。

图像

Symbian 图像

Symbian展现位图的主要类是CFbsBitmap,字体和位图服务器通过它管理位图。其API提供了一些方法,以便从Symbian本地图像文件格式(“多位图(MBM)文件”)中加载(并压缩)位图、创建并访问硬件所拥有的位图、访问位图数据、缩放位图等。它还提供了一种机制,用于复制当前位图的句柄,以使该位图可被其它一些线程所共享。所继承派生的CWsBitmap类展示的位图在window server中拥有对应的句柄,其操作也稍稍快于依赖某个CFbsBitmap的方法。

大部分的图像处理功能都是在Image Converter Library(图像转换器库,ICL)中定义的。这个ICL提供了向CFbsBitmap加载图像文件的一些类。这个库能对一些常见的桌面和移动格式进行解码,也能对一些子集进行编码。下文介绍了Qt和Symiban平台所提供的各种格式的比较。

ICL还提供了一些类,用于完成一些常见的图像操作,包括旋转(900级)、翻转、缩放(保持纵横比和拉伸)。一些不同的类允许CFbsBitmap对象的缩放,也允许对保存在文件或描述符中的静态图像的缩放。

ICL APIs是异步的;客户端会在某个请求操作结束后收到信号。这样就让用户界面能于繁复计算操作的同时仍能保持响应。

Qt图像

Qt为图像数据的处理提供了四个类:QImageQPixmapQBitmapQPicture

QImage是一种硬件无关的图像表现,它允许直接的像素级访问和操作 - 这是最接近Symbian CFbsBitmap的等价类。除了访问像素信息,这个类还能直接从文件或缓存中加载一些常用图像格式 ,使用下列格式之一展现这些图像 - ARGB, RGB32, Mono等。QImage提供强大的图像操作功能,包括旋转、缩放、镜像复制、掩码创建,及二维坐标系中的一些复杂变形。

QPixmap是专为在屏幕上展现图像而设计并优化的。它并不提供对图像数据的直接访问,但却能如QImage一样加载一些常用的文件格式,也同样能执行很多图像操作。QPixmap::toImage()QImage::fromImage()可用于两种图像类间的转换。

QBitmap是一个继承自QPixmap的便捷类,它保证色深为1。最后,QPicture是一个绘图设备,它记录并回放QPainter命令。

Qt还另外提供了一些图像类,包括:QImageReader(当从文件中载入图像时,它提供比QImageQPixmap更为精细的细粒度控制),而QIcon则提供一些可缩放图标,供你在Qt widgets中用来表现某种特定动作。

Symbian和Qt图像格式的交互

如果你正在处理以通常的文件格式保存在文件系统中的图像,你无需直接使用Symbian的图像处理代码。QImageQPixmap可跨平台工作,而且具有一个能加载和操作文件图像数据的更为方便的API。

如果你所处理的文件格式不获Qt支持,你可以选用Symbian的APIs。下表列出了各种环境所默认支持的图像格式 - 请注意,这两种架构都允许使用插件来扩展获支持的图像格式,所以对某个特定平台来说,这里所列的也并非会一成不变。

表: Qt和Symbian的默认图像文件格式
格式 说明 Qt支持 Symbian支持
BMP Windows 位图 读/写 读/写
EXIF 可交换图像文件格式 - 读/写
GIF 图型交换格式(可选) 读(单帧或多帧,位图掩码支持)/写(单帧,不透明)
ICO 图标 - 读(单帧和多帧)
JPG/JPEG 联合图象专家组 读/写 读/写
MBM Symbian 多重位图 - 读(单帧和多帧)/写(单帧)
MNG 多帧网络图型 读/写
PNG 便携式网络图型 读/写 读(位图掩码支持)/写(不透明)
PBM 便携式位图 -
PGM 便携式灰度位图 -
PPM 便携式彩色位图 读/写 -
SMS OTA 空中下载SMS -
TIFF 标记图像文件格式 读/写 读 (支持逆序和正序子类)
WBMP 无线位图 -
WMF Windows图元文件 - 读(支持Std、apm、和clp子类型)
XBM X11 位图 读/写 -
XPM X11 像素 读/写 -

最后,如果图像格式仅适用于Symbian平台,你就需要使用那些Symbian图像类。比如,相机拍摄的图像就可以用CFbsBitmap以支持该平台。

Qt提供一些#平台特定方法用于将CFbsBitmap转换为QPixmap (必要时你可以将QPixmap 转换到 QImage )。

CFbsBitmap *QPixmap::toSymbianCFbsBitmap() const;
static QPixmap QPixmap::fromSymbianCFbsBitmap(CFbsBitmap *bitmap);

Note.pngNote: 如果你需要基于当前的Symbian显示模式(TDisplayMode)获取图像格式(QImage::Format),Qt 4.6.0版本的 q_s60_p.h 中定义了一个私有函数qt_TDisplayMode2Format,你可以将之拷贝到自己的工程中。请注意,因为这个方法是私有的,所以它可能随时被修改。

容器/列表

Symbian 容器

Symbian C++提供了大量的数组类,它们应该优先于标准C/C++数组而被使用,这是因为:它们提供保护机制避免对内存的过分占用。这些数组的表现很像标准C++数组(stl::vector<>);它们以一个整数为索引,元素可以为任意类型(或指向某个类型的指针),数组增删时会进行内部再分配。

文章数组(Symbian C++基础)概要介绍了各种数组类。共有四个大类:

  • CArrayX (CArrayFixFlat, CArrayVarFlat, CArrayPakFlat, CArrayPtrFlat, CArrayFixSeg, CArrayVarSeg, CArrayPtrSeg)
  • RArrays (RArray, RPointerArray)
  • 固定数组(TFixedArray)
  • 描述符数组(CDesC16ArrayFlat, CDesC8ArraySeg, CDesC8ArrayFlat, CDesC8ArraySeg, CPtrC8Array, CPtrC16Array)

数组命名惯例中的Fix是指数组元素全都具有同样的长度,并被直接复制到数组缓存(如,TPoint, TRect)中,而Var是指数组元素是一些指向可变长度对象的指针,这些对象被置于堆中的其它地方(诸如HBufC*TAny*)。Pak (指‘packed,被打包的’数组)被用在数组元素为可变长度但被复制到了具有其各自预测长度的数组缓存的场合(例如,具有可变长度的T类对象)。Ptr用于那些数组元素是指向了继承自 CBase对象的指针的场合。

一般说来,我们用描述符数组储存一列字符串,因为它们具有一些方便的方法可用于搜寻相匹配的字符串。否则就先使用RArray类而非CArrayX。这条规则的例外是数组经常改变大小,这时候你就要从segmented方法中选择一种。

Symbian提供一些模板化的相关数组:

  • RHashMap - 用一个探针序列哈希表将数组关联到键类型K和值类型V。键和值两个对象加入时都被复制到了这个表中。这里使用了逐位二进制复制方法,因而类型K和V都不会去实现一个并非无关紧要的复制构造器。
  • RPtrHashSet- 使用一个探针序列哈希表的无序外延T类型对象集。这些对象加入时并不会被复制到这个集中,而是把指向一些内含对象的指针储存到了这个集中。
  • RPtrHashMap - 具有键类型K和值类型V的一个关联数组,它使用探针序列哈希表。键和值加入时都不会被复制到表中- 只是储存一些指针。

除了Symbian C++ 数组类,你还能使用由Open C和Open C++所提供的STL容器。

Qt 容器

Qt允许你使用STL容器,或使用其自己的一般目的模板容器类。这些Qt类被设计成比STL类更为轻量级、更安全,也更容易使用。在被所有访问它们的线程用作只读容器的场合,它们是隐式共享的可重入的, 也是线程安全的 。最后,它们可用于所有Qt平台(STL则不能用于Qt Embedded平台)。

Qt既提供顺序容器(QList,QLinkedList, QVector, QStack,和QQueue)。也提供关联容器(QMap, QMultiMap, QHash, QMultiHash,和QSet)。也有如QCacheQContiguousCache等特殊类,提供有限缓存中对象的高效率哈希查找功能,及如QStringList(继承自QList<QString>)这样的非模板专门化类,它们方便了对字符串列表的操作。

既可以用java风格的,也可以用STL风格的遍历函数,或使用方便的foreach 关键词,对这些容器进行遍历。Qt提供了一套通用算法 供你用于对容器中的项进行分类、填充、计数、删除等操作(对拥有遍历函数的容器类型)。

这些模板类储存某个特定类型T的一些项。值类型T可以是一个基本类型、一个指针类型、一个具有某种默认构造函数的类、一个复制构造函数及一个赋值操作符,也可以是符合作为类的同样标准的一个容器。

Qt容器类有很好的文档讲解,请参阅Qt 容器类Qt 4 C++ GUI编程,第二版,Jasmin Blanchette和Mark Summerfield编撰,Prentice Hall出版 (2006)(其第一版可在此免费获得)则对其进行了很详尽的讨论。

在Qt和Symbian容器间转换

如果你正在使用标准的STL容器,那么就不必对容器/列表进行转换,因为它们既获Qt也获Symbian平台支持。

如果你确实需要在Qt和Symbian数组间进行转换,这通常是对一个容器的遍历,将内含的每个对象都转换到环境允许的正确类型(如,从QString 到描述符),然后将其加入到新容器中。

当转换到Qt 时,QList 是最“好用”的能重新定义大小的容器模板,如果你的项少于1000项的话(它被实现成一个数组列表但却提供非常快的预规划及添加操作)。如果你所应付的是字符串列,那么就请使用QStringList 。当转换到Symbian C++时,对字符串使用一个描述符数组,或对大于4字节的其他对象使用RArray(除非你不介意有太多的数组需被重新定义大小)。

下列语句段展示了如何将一组整数从一个RArray转换为一个QList:

RArray<TInt> intArrayToList;
QList<int> integerList;
...
for (int i = 0; i < count; i++) {
integerList.append(intArrayToList[i]);
}

而这段语句则展示了如何将一个整数QList导入到一个RArray,及如何使用foreach来对QList作遍历。注意,TInttypdef为带符号int

QList<int> integerList;
RArray<TInt> listToIntArray;
...
foreach (int integerItem, integerList) {
listToIntArray.Append(integerItem);
}

对字符串列进行转换使用完全相同的方法,只是你使用QStringList而非QList,使用一个描述符而非一个RArray,而且你需要将容器内的字符串转换为该平台所接受的正确格式,如前面#字符串小节中已讨论的。

下列代码说明了如何将CDesCArrayFlat转换为QStringList

CDesCArrayFlat* arrayToStringList;
...
QStringList qlistOfStrings;
for (int i = 0; i < count; i++) {
qlistOfStrings.append(QString::fromUtf16(
arrayToStringList->MdcaPoint(i).Ptr(),
arrayToStringList->MdcaPoint(i).Length()));
}

结束语

本文讲述了在Qt应用中以PIMPL模式使用Symbian C++的一些技术和最佳实践。

此外,文章讲述了如何编写代码来安全地融合两种环境的异常处理机制、编码样式和惯例、字符串、几何图形、容器、图像,及数据等。每小节都对某个特定任务在Symbian C++ 和Qt中如何完成,也对各种惯例是如何融合的,作了高度概括。文中也提供了对一些重要文档的链接,以对这些方法作更为详细的解释。

所附范例代码(File:Qtbluetoothdiscoveryexample.zip)给出了能发现远程蓝牙设备的Qt API(及对话框)。它使用PIMPL模式获取底层平台信息,并展示如何安全地融合异常处理机制、编码样式和字符串等。

CreativeCommons attribution sharealike 2.5 by-sa2.5 88x31.png© 2009 Nokia Corporation and/or its subsidiary(-ies). This document is licensed under the Creative Commons Attribution-Share Alike 2.5 license. See http://creativecommons.org/licenses/by-sa/2.5/legalcode for the full terms of the license.

This page was last modified on 30 May 2013, at 09:34.
655 page views in the last 30 days.
×