ASIHTTPRequest和libxml结合,实现边请求边解析
这涉及到3 方面的关键知识:
¥ASIHTTPRequest
这部分的内容可以参考作者另一篇博文《ASIHTTPRequest的使用》。
¥NSOperation 和 Libxml
这部分内容在作者的一篇博文《使用NSOperation实现异步下载》中也有介绍。
背景知识已经具备,下面让我们继续。
一、准备libxml环境
libxml2 是一个开放源码库,默认情况下iPhone SDK 中已经包括在内。 它是一个基于 C 的 API,所以在使用上比 cocoa 的NSXML 要麻烦许多(一种类似 c 函数的使用方式),但是该库同时支持 DOM 和 SAX 解析,其解析速度较快,而且占用内存小,是最适合使用在 iphone 上的解析器。 从性能上讲,所有知名的解析器中,TBXML 最快,但在内存占用上,libxml 使用的内存开销是最小的。因此,我们决定使用 libxml 的sax接口。
首先,我们需要在project 中导入 framework:libxml2.dylib。
虽然libxml 是 sdk 中自带的,但它的头文件却未放在默认的地方,因此还需要我们设置 project 的 build 选项:HEADER_SEARCH_PATHS = /usr/include/libxml2,否则 libxml 库不可用。
然后,我们就可以在源代码中#import<libxml/tree.h>了。
至于ASIHTTPRequest 的使用环境,请参考《ASIHTTPRequest的使用》进行。
二、线程管理
首先,我们肯定要使用线程来进行实现。多线程的操作使用NSOperation子类。
新建o-c class,命名为SyncRequestParseOperation,它必需继承NSOperation。
我们决定不使用继承而使用聚合来让它同时具有ASIHTTPRequest 和 Xml 解析的功能,因此我们导入了libxml/tree.h 和 ASIHTTPRequest.h 。
由于服务器使用了GBK 编码,所以我们也使用了NSStringEncoding。kRequestStatus定义了一个枚举,用来表示SyncRequestParseOperation的不同状态:请求完毕、请求失败、收到数据包。这3种可能状态会被成员变量 status 使用,实际上它是个int。头文件定义如下:
#import<libxml/tree.h>
#import"BaseXmlParser.h"
#import"ASIHTTPRequest.h"
enumkRequestStatus{
kRequestStatusFinished,
kRequestStatusFailed,
kRequestStatusDataReceived
};
@interfaceSyncRequestParseOperation : NSOperation
{
NSURL*_url;
NSDictionary*_data;
//构建gb2312的encoding
NSStringEncodingenc;
//Xml解析器指针
xmlParserCtxtPtr_parserContext;
BaseXmlParser*baseParser;
iddelegate,progressDelegate;
intstatus;
}
@property(nonatomic,retain) NSDictionary *data;
@property(nonatomic,retain) NSURL *url;
@property(assign)intstatus;
- (id)initWithURLString:(NSString*)urlxmlParser:(BaseXmlParser*) parser delegate:(id)obj;
-(void)setProgressDelegate:(id)progress;
-(void)statusChangedNotify;
@end
BaseXmlParser 是一个Xml解析器的基类,我们使用它的子类来进行Xml解析,在其中定义了一些使用 libxml 时特有的结构体和函数声明。有了它,我们就可以在其子类中覆盖某些方法来解析各种不同的XML 文件。
BaseXmlParser 及其子类我们后面会介绍。
delegate和 progressDelegate 保存两个对象的 id 引用。前者是负责响应SyncRequestParseOperation类的一些特殊的通知消息,比如某个状态的改变;后者负责根据收到的数据实时进行进度显示。
接下来我们看实现,首先是初始化init 方法:
initWithURLString:xmlParser: delegate:(id)obj方法是个便利的初始化方法,分别对3个成员进行初始化,而不必要对它们一一调用setter方法:http请求地址url、解析器、通知消息的委托对象。
- (id)initWithURLString:(NSString*)urlxmlParser:(BaseXmlParser*)parser delegate:(id)obj{
if(self= [superinit]) {
_url=[[NSURLalloc]initWithString:url];
delegate=obj;
baseParser=[parserretain];
//构建gb2312的encoding
enc=CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
}
returnself;
}
除了init方法外,我们也提供了 setProgressDelegate 方法:
-(void)setProgressDelegate:(id)progress{
progressDelegate=progress;
}
用于对progressDelegate 进行初始化。
接下来是最主要的部分,NSOperation的生命周期方法:
#pragma mark NSOperation的生命周期方法
//开始线程-本类的主方法
- (void)start {
NSLog(@"operation start!");
if(![selfisCancelled]) {
//创建XML解析器指针
_parserContext=xmlCreatePushParserCtxt(&_saxHandlerStruct,baseParser,NULL,0,NULL);
//以异步方式处理事件,并设置代理块
__blockASIHTTPRequest*request = [ASIHTTPRequestrequestWithURL:_url];
//设置进度代理
if(progressDelegate!=nil) {
[requestsetDownloadProgressDelegate:progressDelegate];
}
//使用complete块,在下载完时做一些事情
[requestsetCompletionBlock:^(void){
[selfsetStatus:kRequestStatusFinished];
NSLog(@"request completed!");
//添加解析数据(结束),注意最后一个参数terminate
xmlParseChunk(_parserContext,NULL,0,1);
//添加解析数据(结束),
if(baseParser!=nil){
[selfsetData:[[baseParsergetResult]copy]];
}else{
NSLog(@"baseparser is nil");
}
//释放XML解析器
if(_parserContext) {
xmlFreeParserCtxt(_parserContext),_parserContext=NULL;
}
[selfstatusChangedNotify];
}];
//使用failed块,在下载失败时做一些事情
[requestsetFailedBlock:^(void){
[selfsetStatus:kRequestStatusFailed];
NSLog(@"request failed !");
//释放XML解析器指针
if(_parserContext) {
xmlFreeParserCtxt(_parserContext),_parserContext=NULL;
}
[selfstatusChangedNotify];
}];
//使用received块,在接受到数据时做一些事情
[requestsetDataReceivedBlock:^(NSData*data){
[selfsetStatus:kRequestStatusDataReceived];
NSLog(@"received data:%d",data.length);
//添加解析数据(结束),注意最后一个参数terminate
if(baseParser!=nil&&baseParser!=NULL){
[selfsetData:[[baseParsergetResult]copy]];
}else{
NSLog(@"baseparser is nil");
}
//使用libxml解析器进行xml解析
xmlParseChunk(_parserContext, (constchar*)[databytes], [datalength],0);
[selfstatusChangedNotify];
}];
[requeststartAsynchronous];
}
}
//停止线程
- (void)cancel
{
[supercancel];
}
对于一个NSOperation 来说,最主要的是start 方法,因为线程在这里启动。由于使用了 ASIHTTPRequest 的异步方式,所以在start方法中我们没有使用NSRunLoop循环(这个问题参考http://www.cocoabuilder.com/archive/cocoa/279826-nsurlrequest-and-nsoperationqueue.html )。因为 ASIHTTPRequest 的startAsynchronous 方法提供了额外的线程。我们在 start 方法中使用了一个ASIHTTPRequest ,利用 BaseXmlParser 解析器来提供一系列符合 libxml 规范的回调函数,以响应 sax 解析事件。当然,由于我们要实现“边接收数据,边解析Xml”的目的,我们在 ASIHTTPRequest 的三个委托块中,就对数据进行了处理(使用 libxml 的函数)。
比较怪异的是对ASIHTTPRequest 的3个事件委托中使用了块语法,块语法介绍可以参考作者另一篇(翻译)博文《块编程指南》。
为了把3个委托事件通知给delegate,我们需要在3个事件委托块中调用delegate 的相应方法:
// status状态变化通知
-(void)statusChangedNotify{
if(delegate!=nil) {
SELsel=NSSelectorFromString(@"syncRequestParseStatusNofity:");
if([delegaterespondsToSelector:sel]){
[delegateperformSelector:selwithObject:self];//注意冒号说明带1个参数
}
}
}
为了简便,我没有定义新的协议,而只是使用方法名syncRequestParseStatusNofity:作为内部协议。如果delegate要想接收通知,就必需实现该方法。作为一种技巧,其中使用了反射机制,避免运行时错误。
三、Sax 异步解析
libxml 是C函数库,其中很多函数需要使用令人生畏的结构体定义。为了便于扩展,这些定义被放到了 BaseXmlParser 类中:
#import<Foundation/Foundation.h>
#import<libxml/tree.h>
@interfaceBaseXmlParser : NSObject {
NSStringEncodingenc;
NSMutableDictionary*_root;
}
// Property
- (void)startElementLocalName:(constxmlChar*)localname
prefix:(constxmlChar*)prefix
URI:(constxmlChar*)URI
nb_namespaces:(int)nb_namespaces
namespaces:(constxmlChar**)namespaces
nb_attributes:(int)nb_attributes
nb_defaulted:(int)nb_defaultedslo
attributes:(constxmlChar**)attributes;
- (void)endElementLocalName:(constxmlChar*)localname
prefix:(constxmlChar*)prefix URI:(constxmlChar*)URI;
- (void)charactersFound:(constxmlChar*)ch
len:(int)len;
-(NSDictionary*)getAtributes:(constxmlChar**)attributes withSize:(int)nb_attributes;
-(NSDictionary*)getResult;
@end
//3个静态方法的实现,其实是调用了ctx的成员方法,其中ctx在_parserContext初始化时传入
staticvoidstartElementHandler(
void* ctx,
constxmlChar* localname,
constxmlChar* prefix,
constxmlChar* URI,
intnb_namespaces,
constxmlChar** namespaces,
intnb_attributes,
intnb_defaulted,
constxmlChar** attributes)
{
[(BaseXmlParser*)ctx
startElementLocalName:localname
prefix:prefixURI:URI
nb_namespaces:nb_namespaces
namespaces:namespaces
nb_attributes:nb_attributes
nb_defaulted:nb_defaulted
attributes:attributes];
}
staticvoidendElementHandler(
void* ctx,
constxmlChar* localname,
constxmlChar* prefix,
constxmlChar* URI)
{
[(BaseXmlParser*)ctx
endElementLocalName:localname
prefix:prefix
URI:URI];
}
staticvoidcharactersFoundHandler(
void* ctx,
constxmlChar* ch,
intlen)
{
[(BaseXmlParser*)ctx
charactersFound:chlen:len];
}
//libxml的xmlSAXHandler结构体定义,凡是要实现的handler函数都写在这里,不准备实现的用null代替。一般而言,我们只实现其中3个就够了
staticxmlSAXHandler_saxHandlerStruct= {
NULL,/* internalSubset */
NULL,/* isStandalone*/
NULL,/* hasInternalSubset */
NULL,/* hasExternalSubset */
NULL,/* resolveEntity */
NULL,/* getEntity */
NULL,/* entityDecl */
NULL,/* notationDecl */
NULL,/* attributeDecl */
NULL,/* elementDecl */
NULL,/* unparsedEntityDecl */
NULL,/* setDocumentLocator */
NULL,/* startDocument */
NULL,/* endDocument */
NULL,/* startElement*/
NULL,/* endElement */
NULL,/* reference */
charactersFoundHandler,/* characters */
NULL,/* ignorableWhitespace */
NULL,/* processingInstruction */
NULL,/* comment */
NULL,/* warning */
NULL,/* error */
NULL,/* fatalError //: unused error() get all the errors */
NULL,/* getParameterEntity */
NULL,/* cdataBlock */
NULL,/* externalSubset */
XML_SAX2_MAGIC,/* initialized特殊常量,照写*/
NULL,/* private */
startElementHandler,/* startElementNs */
endElementHandler,/* endElementNs */
NULL,/* serror */
};
在BaseXmlParser 类的头文件中,可以分为两部分。
1.第一部分是interface 定义,定义了BaseXmlParser类的成员,包括:
¥成员变量
enc:基于和前面同样的原因,用于定义GBK编码。
_root:一个Dictionary,用于保存解析后Xml对象,一个xml文档只有一个root 元素,因此用一个Dictionary对象即可。
¥成员方法
libxml 回调方法:前3个很像是C语言函数的方法其实都是被libxml回调的,它们会在3个静态函数(在第二部分)中调用。
getAttributes方法:这个是一个方便的获取 xml 元素属性的方法。由于本例中的 XML 文档大量使用了属性,所以这个方法很实用。
getResult方法:用于获得 XML 文档解析结果,即 _root 对象。
2.第二部分是libxml 回调函数和结构体定义,包括:
¥回调函数
本例我们决定实现3个回调函数,分别用于响应 Sax 解析中的3个事件:
处理XML 元素开始标记、处理 XML 元素结束标记、处理 XML 元素体。
为了更OO 一些,我们没有直接在这 3 个函数中写对应的 XML 解析代码,而是调用了类的成员方法进行处理。这样,我们可以在 implement 部分写入具体的代码。
¥结构体
只需要填充一个结构体xmlSAXHandler即可。这个结构成员数量众多(31个),但我们只需填充你要实现的几个。例如,我们要实现3个回调函数,那么只消在对应的地方填充这3个函数名即可(此外有一个特殊的成员叫XML_SAX2_MAGIC,你照填就是了)。为了便于大家理解这些成员所代表的意义,我们也在旁边做了注释,你可以对照着看。
接下来是implement (实现)。
#import"BaseXmlParser.h"
@implementationBaseXmlParser
// Property
-(id)init{
if(self=[superinit]){
//构建gb2312的encoding
enc=CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
_root=[[NSMutableDictionaryalloc]init];
}
returnself;
}
-(void)dealloc{
[_rootrelease],_root=nil;
[superdealloc];
}
//一个便利方法,用于获取元素的属性值
-(NSDictionary*)getAtributes:(constxmlChar**)attributes withSize:(int)nb_attributes{
NSMutableDictionary* atts=[[NSMutableDictionaryalloc]init];
NSString*key,*val;
for(inti=0; i<nb_attributes; i++){
key = [NSStringstringWithCString:(constchar*)attributes[0]encoding:NSUTF8StringEncoding];
val = [[NSStringalloc]initWithBytes:(constvoid*)attributes[3]length:(attributes[4] - attributes[3])encoding:NSUTF8StringEncoding];
[attssetObject:valforKey:key];
[keyrelease],[valrelease];
attributes +=5;//指针移动5个字符串,到下一个属性
}
returnatts;
}
//--------------------------------------------------------------//
#pragma mark -- libxml handler,主要是3个回调方法,空方法,等待子类实现--
//--------------------------------------------------------------//
//解析元素开始标记时触发,在这里取元素的属性值
- (void)startElementLocalName:(constxmlChar*)localname
prefix:(constxmlChar*)prefix
URI:(constxmlChar*)URI
nb_namespaces:(int)nb_namespaces
namespaces:(constxmlChar**)namespaces
nb_attributes:(int)nb_attributes
nb_defaulted:(int)nb_defaultedslo
attributes:(constxmlChar**)attributes
{
}
//解析元素结束标记时触发
- (void)endElementLocalName:(constxmlChar*)localname
prefix:(constxmlChar*)prefix URI:(constxmlChar*)URI
{
}
//解析元素体时触发
- (void)charactersFound:(constxmlChar*)ch
len:(int)len
{
}
//返回解析结果
-(NSDictionary*)getResult{
return_root;
}
@end
可以看到,除了getAttributes 和getResult 方法外,我们都没有进行其它方法的实现。这是因为 Sax 解析跟 Dom 解析不同,针对不同的 XML 文档很难使用相同的逻辑解析,因此我们准备把剩下的内容留给子类来实现,这样不同的XML 文档可以通过不同的子类来进行解析,而不用在每个子类中都写一遍那些怪异的 C 回调函数和结构体声明。
我们要解析的XML 文档可能是这样的:
<root>
<List Name="同事">
<user name="t2" phone="13884831140"/>
<user name="t3" phone="15877103548"/>
<user name="t1" phone="13399459990"/>
</List>
<List Name="好友">
<user name="f2" phone="13828831140"/>
<user name="f3" phone="15886103548"/>
<user name="f1" phone="13019459990"/>
</List>
</root>
也就是说,这是一个通讯录类似的东西。通讯录把电话号码按性质分成不同的组,就像Windows mobile智能手机上的的通讯录,把电话号码按“家庭”、“好友”、“同事”等进行划分。
我们新建一个BaseXmlParser的子类 TelNoXmlParser ,让这个 TelNoXmlParser 去实现 3 个回调方法:
#import<Foundation/Foundation.h>
#import<libxml/tree.h>
#import"BaseXmlParser.h"
@interfaceTelNoXmlParser : BaseXmlParser {
BOOLloginSuccess;
NSMutableArray*groups,*members;
NSMutableDictionary*_group;
NSDictionary*_user;
}
@end
#import"TelNoXmlParser.h"
@implementationTelNoXmlParser
-(id)init{
if(self=[superinit]) {
//一个groups数组,代表了所有List
groups=[[NSMutableArrayalloc]init];
[_rootsetObject:groupsforKey:@"items"];
loginSuccess=YES;
}
returnself;
}
-(void)dealloc{
[_grouprelease],_group=nil;
[superdealloc];
}
//--------------------------------------------------------------//
#pragma mark -- libxml handler,主要是3个回调方法--
//--------------------------------------------------------------//
//解析元素开始标记时触发,在这里取元素的属性值以及设置标志变量
- (void)startElementLocalName:(constxmlChar*)localname
prefix:(constxmlChar*)prefix
URI:(constxmlChar*)URI
nb_namespaces:(int)nb_namespaces
namespaces:(constxmlChar**)namespaces
nb_attributes:(int)nb_attributes
nb_defaulted:(int)nb_defaultedslo
attributes:(constxmlChar**)attributes
{//我们关心8个元素标签,所以设置了8个标志位
// login_status
if(strncmp((char*)localname,"login_status",sizeof("login_status")) ==0) {
loginSuccess=NO;
return;
}
if(loginSuccess) {
// List
if(strncmp((char*)localname,"List",sizeof("List")) ==0) {
NSDictionary* atts=[selfgetAtributes:attributeswithSize:nb_attributes];//获取List的所有属性
_group=[[NSMutableDictionaryalloc]init];
members=[[NSMutableArrayalloc]init];
[_groupsetObject:membersforKey:@"members"];
[_groupsetObject:[[NSStringalloc]initWithString:(NSString*)[attsobjectForKey:@"Name"]]forKey:@"groupname"];
[groupsaddObject:_group];//把group加入数组
return;
}
// user
if(strncmp((char*)localname,"user",sizeof("user")) ==0) {
NSDictionary* atts=[selfgetAtributes:attributeswithSize:nb_attributes];//获取List的所有属性
_user=[[NSDictionaryalloc]initWithDictionary:atts];
[membersaddObject:_user];
return;
}
}
}
//解析元素结束标记时触发
- (void)endElementLocalName:(constxmlChar*)localname
prefix:(constxmlChar*)prefix URI:(constxmlChar*)URI
{
if(strncmp((char*)localname,"root",sizeof("root")) ==0){//root结束时置login_status标志
if(loginSuccess) {
[_rootsetObject:@"true"forKey:@"login_status"];
}else{
[_rootsetObject:@"false"forKey:@"login_status"];
}
}
if(loginSuccess) {
//我们还关心<List>的结束标记
if(strncmp((char*)localname,"List",sizeof("List")) ==0) {
[_grouprelease],_group=nil;//回收_group对象,以便重复利用
}elseif(strncmp((char*)localname,"user",sizeof("user")) ==0){
[_userrelease],_user=nil;//回收_user对象,以便重复利用
}
}
}
//解析元素体时触发
- (void)charactersFound:(constxmlChar*)ch
len:(int)len
{
//没有元素体需要关心
}
@end
接下来我们看如何在ViewController 中使用。
四、在UI 中测试
在ViewController 中放入一个按钮和一个 WebView,当点击按钮时,请求http服务器,获取通讯录XML 数据,并解析为 Dictionary 对象。把解析结果显示在 WebView 中。
这是按钮的touch up inside 事件代码:
-(IBAction)go{
if(_queue==nil){
_queue= [[NSOperationQueuealloc]init];
}
[buttonsetEnabled:NO];
[progresssetProgress:0];
[webViewloadHTMLString:@""baseURL:[NSURLURLWithString:URL]];
//构造xmlparser
TelNoXmlParser* parser=[[TelNoXmlParseralloc]init];
//把self注册为delegate,这样self必需实现syncRequestParseStatusNofity:方法,以接收statusChanged方法
SyncRequestParseOperation* operation=[[SyncRequestParseOperationalloc ]
initWithURLString:URL
xmlParser:parser
delegate:self];
//把progress设置为progressDelegate,这样会显示进度
[operationsetProgressDelegate:progress];
[parserrelease];// opertaion已retain,可以release
[_queueaddOperation:operation];//开始处理
[operationrelease];//队列已retain,可以release;
}
这是异步消息到达时的处理代码,当数据接收完时,我们把解析结果在WebView 中显示:
//实现statusChanged通知方法
-(void)syncRequestParseStatusNofity:(id)sender{
SyncRequestParseOperation* operation=(SyncRequestParseOperation*)sender;
intstatus=[operationstatus];
NSLog(@"status:%d",status);
if(status==kRequestStatusFinished){//如数据接收完成
[buttonsetEnabled:YES];
NSDictionary* d=[operationdata];
[webViewloadHTMLString:[ddescription]baseURL:[NSURLURLWithString:URL]];
}
}
这是程序运行时WebView 显示效果:
注意,当xml 文档比较大时,WebView 的内容是从上到下逐渐刷新的。
这是控制台输出,可以看到服务器响应的数据是被分成很多次下载的: