你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

ASIHTTPRequest和libxml结合,实现边请求边解析

2022/6/23 4:47:11

原文网址:http://blog.csdn.net/kmyhy/article/details/6525075

ASIHTTPRequests是非常强大的 http 异步请求开源框架,libxml 是非常老牌的 C 语言xml函数库。在 http + xml 文件的 javaEE-iPhone 应用中,如何把二者结合起来,实现在异步请求数据的同时,进行xml的同步解析呢?

这涉及到 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"

enum kRequestStatus {

kRequestStatusFinished,

kRequestStatusFailed,

kRequestStatusDataReceived

};

@interface SyncRequestParseOperation : NSOperation

{

NSURL *_url;

NSDictionary* _data;

//构建gb2312的encoding

NSStringEncoding enc;

//Xml解析器指针

xmlParserCtxtPtr _parserContext;

BaseXmlParser* baseParser;

id delegate,progressDelegate;

int status;

}

@property (nonatomic,retain)NSDictionary *data;

@property (nonatomic,retain) NSURL*url;

@property (assign)int status;

- (id)initWithURLString:(NSString *)url xmlParser:(BaseXmlParser*) parserdelegate:(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*)url xmlParser:(BaseXmlParser*)parser delegate:(id)obj{

if(self = [super init]) {

_url=[[NSURL alloc]initWithString:url];

delegate=obj;

baseParser=[parser retain];

//构建gb2312的encoding

enc =CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);

}

return self;

}

除了init方法外,我们也提供了 setProgressDelegate方法:

-(void)setProgressDelegate:(id)progress{

progressDelegate =progress;

}

用于对 progressDelegate进行初始化。

接下来是最主要的部分,NSOperation的生命周期方法:

#pragmamark NSOperation的生命周期方法

// 开始线程-本类的主方法

- (void)start {

NSLog(@"operationstart!");

if (![self isCancelled]) {

// 创建XML解析器指针

        _parserContext = xmlCreatePushParserCtxt(&_saxHandlerStruct, baseParser, NULL, 0, NULL);

// 以异步方式处理事件,并设置代理块

__block ASIHTTPRequest *request =[ASIHTTPRequest requestWithURL:_url];

// 设置进度代理

if (progressDelegate!=nil) {

[request setDownloadProgressDelegate:progressDelegate];

}

 

// 使用complete 块,在下载完时做一些事情

[request setCompletionBlock:^(void){

[self setStatus:kRequestStatusFinished];

NSLog(@"requestcompleted!");

// 添加解析数据(结束),注意最后一个参数terminate

xmlParseChunk(_parserContext, NULL, 0, 1);

//添加解析数据(结束),

if(baseParser!=nil){

[self setData:[[baseParser getResult]copy]];

}else {

NSLog(@"baseparseris nil");

}

 

// 释放XML解析器

if (_parserContext) {

xmlFreeParserCtxt(_parserContext), _parserContext = NULL;

}

[self statusChangedNotify];

}];

// 使用 failed 块,在下载失败时做一些事情

[request setFailedBlock:^(void){

[self setStatus:kRequestStatusFailed];

NSLog(@"requestfailed !");

// 释放XML解析器指针

if (_parserContext) {

xmlFreeParserCtxt(_parserContext), _parserContext = NULL;

}

[self statusChangedNotify];

}];

// 使用 received 块,在接受到数据时做一些事情

[request setDataReceivedBlock:^(NSData* data){

[self setStatus:kRequestStatusDataReceived];

NSLog(@"receiveddata:%d",data.length);

// 添加解析数据(结束),注意最后一个参数terminate

if(baseParser!=nil && baseParser!=NULL){

[self setData:[[baseParser getResult]copy]];

}else {

NSLog(@"baseparseris nil");

}

// 使用libxml解析器进行xml解析

xmlParseChunk(_parserContext, (const char*)[data bytes], [data length], 0);

[self statusChangedNotify];

}];

[request startAsynchronous];

}

}

// 停止线程

- (void)cancel

{

[super cancel];

}

对于一个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) {

SEL sel=NSSelectorFromString(@"syncRequestParseStatusNofity:");

if([delegate respondsToSelector:sel]){

[delegate performSelector:sel withObject:self]; //注意冒号说明带1个参数

}

}

为了简便,我没有定义新的协议,而只是使用方法名 syncRequestParseStatusNofity: 作为内部协议。如果delegate要想接收通知,就必需实现该方法。作为一种技巧,其中使用了反射机制,避免运行时错误。

 

三、Sax 异步解析

libxml 是C函数库,其中很多函数需要使用令人生畏的结构体定义。为了便于扩展,这些定义被放到了BaseXmlParser 类中:

#import <Foundation/Foundation.h>

#import <libxml/tree.h>

@interface BaseXmlParser : NSObject {

NSStringEncoding enc;

NSMutableDictionary*    _root;

}

// Property

- (void)startElementLocalName:(const xmlChar*)localname

   prefix:(const xmlChar*)prefix

 URI:(const xmlChar*)URI

nb_namespaces:(int)nb_namespaces

  namespaces:(const xmlChar**)namespaces

nb_attributes:(int)nb_attributes

 nb_defaulted:(int)nb_defaultedslo

  attributes:(const xmlChar**)attributes;

- (void)endElementLocalName:(const xmlChar*)localname

 prefix:(const xmlChar*)prefix URI:(const xmlChar*)URI;

- (void)charactersFound:(const xmlChar*)ch

len:(int)len;

-(NSDictionary*)getAtributes:(const xmlChar**)attributes withSize:(int)nb_attributes;

-(NSDictionary*)getResult;

@end

//3个静态方法的实现,其实是调用了 ctx 的成员方法,其中ctx在_parserContext初始化时传入

static void startElementHandler(

void* ctx,

const xmlChar* localname,

const xmlChar* prefix,

const xmlChar* URI,

intnb_namespaces,

const xmlChar**namespaces,

intnb_attributes,

intnb_defaulted,

const xmlChar**attributes)

{

    [(BaseXmlParser*)ctx

 startElementLocalName:localname

 prefix:prefix URI:URI

 nb_namespaces:nb_namespaces

 namespaces:namespaces

 nb_attributes:nb_attributes

 nb_defaulted:nb_defaulted

 attributes:attributes];

}

 

static void endElementHandler(

  void* ctx,

  const xmlChar* localname,

  const xmlChar* prefix,

  const xmlChar* URI)

{

    [(BaseXmlParser*)ctx

 endElementLocalName:localname

 prefix:prefix

 URI:URI];

}

 

static void charactersFoundHandler(

   void* ctx,

   const xmlChar* ch,

   int len)

{

    [(BaseXmlParser*)ctx

 charactersFound:ch len:len];

}

//libxml的xmlSAXHandler结构体定义,凡是要实现的handler函数都写在这里,不准备实现的用null代替。一般而言,我们只实现其中3个就够了

static xmlSAXHandler _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 //: unusederror() 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"

 

@implementation BaseXmlParser

// Property

-(id)init{

if(self=[super init]){

//构建gb2312的encoding

enc =CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);

_root=[[NSMutableDictionary alloc]init];

}

return self;

}

-(void)dealloc{

[_root release],_root=nil;

[super dealloc];

}

//一个便利方法,用于获取元素的属性值

-(NSDictionary*)getAtributes:(const xmlChar**)attributeswithSize:(int)nb_attributes{

NSMutableDictionary* atts=[[NSMutableDictionary alloc]init];

NSString *key,*val;

for (int i=0;i<nb_attributes; i++){

key = [NSString stringWithCString:(const char*)attributes[0] encoding:NSUTF8StringEncoding];

val = [[NSString alloc] initWithBytes:(const void*)attributes[3] length:(attributes[4] -attributes[3]) encoding:NSUTF8StringEncoding];

[atts setObject:val forKey:key];

[key release],[val release];

attributes+= 5;//指针移动5个字符串,到下一个属性

}

return atts;

}

 

//--------------------------------------------------------------//

#pragmamark -- libxml handler,主要是3个回调方法,空方法,等待子类实现--

//--------------------------------------------------------------//

//解析元素开始标记时触发,在这里取元素的属性值

- (void)startElementLocalName:(const xmlChar*)localname

  prefix:(const xmlChar*)prefix

  URI:(const xmlChar*)URI

nb_namespaces:(int)nb_namespaces

   namespaces:(const xmlChar**)namespaces

nb_attributes:(int)nb_attributes

 nb_defaulted:(int)nb_defaultedslo

   attributes:(const xmlChar**)attributes

{

}

//解析元素结束标记时触发

- (void)endElementLocalName:(const xmlChar*)localname

 prefix:(const xmlChar*)prefixURI:(const xmlChar*)URI

{

}

//解析元素体时触发

- (void)charactersFound:(const xmlChar*)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"

@interface TelNoXmlParser : BaseXmlParser {

BOOL loginSuccess;

NSMutableArray *groups,*members;

NSMutableDictionary *_group;

NSDictionary *_user;

}

@end

#import "TelNoXmlParser.h"

 

 

@implementation TelNoXmlParser

 

-(id)init{

if(self=[super init]) {

//一个groups数组,代表了所有List

groups=[[NSMutableArray alloc]init];

[_root setObject:groups forKey:@"items"];

loginSuccess=YES;

}

return self;

}

-(void)dealloc{

[_group release],_group=nil;

[super dealloc];

}

//--------------------------------------------------------------//

#pragmamark -- libxml handler,主要是3个回调方法--

//--------------------------------------------------------------//

//解析元素开始标记时触发,在这里取元素的属性值以及设置标志变量

- (void)startElementLocalName:(const xmlChar*)localname

  prefix:(const xmlChar*)prefix

  URI:(const xmlChar*)URI

nb_namespaces:(int)nb_namespaces

   namespaces:(const xmlChar**)namespaces

nb_attributes:(int)nb_attributes

 nb_defaulted:(int)nb_defaultedslo

   attributes:(const xmlChar**)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=[self getAtributes:attributes withSize:nb_attributes];//获取List的所有属性

_group=[[NSMutableDictionary alloc]init];

members=[[NSMutableArray alloc]init];

[_group setObject:members forKey:@"members"];

[_group setObject:[[NSString alloc]initWithString:(NSString *)[atts objectForKey:@"Name"]] forKey:@"groupname"];

[groups addObject:_group];//把group加入数组

return;

}

// user

if (strncmp((char*)localname,"user", sizeof("user")) == 0) {

NSDictionary* atts=[self getAtributes:attributes withSize:nb_attributes];//获取List的所有属性

_user=[[NSDictionary alloc]initWithDictionary:atts];

[members addObject:_user];

return;

}

}

}

//解析元素结束标记时触发

- (void)endElementLocalName:(const xmlChar*)localname

 prefix:(const xmlChar*)prefixURI:(const xmlChar*)URI

{

if(strncmp((char*)localname,"root", sizeof("root")) == 0){//root结束时置 login_status标志

if (loginSuccess) {

[_root setObject:@"true" forKey:@"login_status"];

}else {

[_root setObject:@"false" forKey:@"login_status"];

}

}

if (loginSuccess) {

//我们还关心<List>的结束标记

if (strncmp((char*)localname,"List", sizeof("List")) == 0) {

[_group release],_group=nil;//回收_group对象,以便重复利用

}else if (strncmp((char*)localname,"user", sizeof("user")) == 0){

[_user release],_user=nil;//回收_user对象,以便重复利用

}

}

}

//解析元素体时触发

- (void)charactersFound:(const xmlChar*)ch

len:(int)len

{      

//没有元素体需要关心

}

@end

接下来我们看如何在 ViewController 中使用。

 

四、在 UI 中测试

在 ViewController 中放入一个按钮和一个WebView,当点击按钮时,请求http服务器,获取通讯录XML 数据,并解析为 Dictionary 对象。把解析结果显示在 WebView 中。

这是按钮的 touch upinside 事件代码:

-(IBAction)go{

if(_queue==nil){

_queue = [[NSOperationQueue alloc] init];

}

[button setEnabled:NO];

[progress setProgress:0];

[webView loadHTMLString:@"" baseURL:[NSURL URLWithString:URL]];

//构造xmlparser

TelNoXmlParser* parser=[[TelNoXmlParser alloc]init];

// 把self注册为delegate,这样self必需实现syncRequestParseStatusNofity:方法,以接收statusChanged方法

SyncRequestParseOperation*operation=[[SyncRequestParseOperation alloc ]

  initWithURLString:URL

  xmlParser:parser

  delegate:self];

// 把progress设置为progressDelegate,这样会显示进度

[operation setProgressDelegate:progress];

[parser release];// opertaion已retain,可以release

[_queue addOperation:operation];// 开始处理

[operation release];//队列已retain,可以release;

}

这是异步消息到达时的处理代码,当数据接收完时,我们把解析结果在WebView 中显示:

// 实现statusChanged通知方法

-(void)syncRequestParseStatusNofity:(id)sender{

SyncRequestParseOperation*operation=(SyncRequestParseOperation*)sender;

intstatus=[operation status];

NSLog(@"status:%d",status);

if(status==kRequestStatusFinished){//如数据接收完成

[button setEnabled:YES];

NSDictionary*d=[operation data];

[webView loadHTMLString:[d description] baseURL:[NSURL URLWithString: URL]];

}

}

这是程序运行时 WebView 显示效果:

 

注意,当xml 文档比较大时,WebView的内容是从上到下逐渐刷新的。

这是控制台输出,可以看到服务器响应的数据是被分成很多次下载的: