一个iOS项目总结(一):网络接口的封装 - Go语言中文社区

一个iOS项目总结(一):网络接口的封装


作者:代培
地址:http://blog.csdn.net/dp948080952/article/details/52650092
转载请注明出处

写在前面

今年暑假,自己独立完成了一个简单的iOS的APP,是一个bbs的客户端,叫做喻信星空。现在正在测试,准备将其上架app store。但是光做项目不做总结肯定不行,所以这里写篇博客,把项目里遇到的坑都记录下来,不过这篇博客里肯定是有干货的,所以如果你看到了这里,希望你能把它看完,并顶我一下(^-^)
这是一系列博客,此系列共四篇文章,此篇博客是该系列博客的第一篇:网络接口的封装。

正文

从抓包开始

这个bbs已经有了安卓版的客户端,所以我就直接用安卓应用来抓包,不得不说用Charles真的是抓包的神器,抓包的具体细节这里就不详细说了。

概述

在动手写UI层的东西之前,我首先想到的是将网络请求封装起来,在这里我很不要脸的用SDK进行命名,想想自己能写SDK也是很不错啊。
那么现在的问题是这个SDK该如何组织呢?具体来说有哪些类,分别负责什么呢。对于像我这样接触编程不久的人来说在架构上的体会是很少的,虽然自己的目标一直是成为一名架构师,但是这是需要一个很长时间的积累才能再回头看书进行总结,才能有所小成。所以我这里几乎没有什么设计,就是从实用的角度弄出了一个可以正常使用的东西,大家如果有更好的设计,欢迎在下面留言讨论。
我的SDK分为3个部分:YuXinSDK,YuXinModel,YuXinXmlParser,其中YuXinSDK作为一个主要的部分,网络请求都封装在这个类中,使用这个SDK时也只要导入这个类的头文件就可以了。YuXinModel从名字就能看出来是做什么的,是数据模型。最后YuXinXmlParser是一个Xml的解析器,对从网络返回的Xml数据进行解析生成YuXinModel,这个Model是一个基类,有很多继承这个类不同的Model,当然这些Model不仅仅就是一个数据的承载器,在其内部也进行了数据的进一步加工(至于具体的加工什么,后面会具体讨论)。

字符编码问题

由于这个bbs的服务器是一个非常老的服务器,它的编码方式是gb2312,这里也有一个小小的坑,在苹果封装的NSStringEncoding里没有gb2312的编码方式

NSASCIIStringEncoding = 1,      
    NSNEXTSTEPStringEncoding = 2,
    NSJapaneseEUCStringEncoding = 3,
    NSUTF8StringEncoding = 4,
    NSISOLatin1StringEncoding = 5,
    NSSymbolStringEncoding = 6,
    NSNonLossyASCIIStringEncoding = 7,
    NSShiftJISStringEncoding = 8,          
    NSISOLatin2StringEncoding = 9,
    NSUnicodeStringEncoding = 10,
    NSWindowsCP1251StringEncoding = 11,    
    NSWindowsCP1252StringEncoding = 12,   
    NSWindowsCP1253StringEncoding = 13,    /* Greek */
    NSWindowsCP1254StringEncoding = 14,    /* Turkish */
    NSWindowsCP1250StringEncoding = 15,    /* WinLatin2 */
    NSISO2022JPStringEncoding = 21,        
    NSMacOSRomanStringEncoding = 30,
    NSUTF16StringEncoding = NSUnicodeStringEncoding,      
    NSUTF16BigEndianStringEncoding = 0x90000100,          
    NSUTF16LittleEndianStringEncoding = 0x94000100,       
    NSUTF32StringEncoding = 0x8c000100,                   
    NSUTF32BigEndianStringEncoding = 0x98000100,          
    NSUTF32LittleEndianStringEncoding = 0x9c000100        

我试了上面大部分的编码方式,很多都是英文没有问题,但是中文在服务器那边都会变成乱码
后来在网上看到有人说gb2312的编码方式在更底层的CFStringEncodings中才有,使用时要将CFStringEncodings转换为NSStringEncoding。然后在CFStringEnodings的枚举里看到了kCFStringEncodingGB_2312_80 = 0x0630,当时欣喜若狂,觉得马上就要成功了,但现实是残酷的,中文字符还是会出问题,最后终于在网上发现了有人说实际上gb2312是kCFStringEncodingGB_18030_2000而不是kCFStringEncodingGB_2312_80,真的觉得苹果这个命名太坑了,把我害苦了,写着gb2312却不是gb2312,真是服了。

xml解析(坑最多的地方)

下面说说解析从服务器返回的xml数据,说真的现在服务器返回的数据大多都是json了,返回xml也能看出来这个服务器有多老了,正是因为老,所以有很多坑在里面。
xml解析有两种方式:SAX(Simple API for XML)和DOM (Document Object Model),SAX是从根元素开始,按顺序一个元素一个元素往下解析,比较适合解析大文件,而DOM一次性将整个XML文档加载进内存,比较适合解析小文件,而我使用的Foundation中的NSXMLParser用的是SAX方式。

  • 控制字符

在使用NSXMLParser解析的过程中,会遇到某些返回的xml数据解析失败的情况,错误代码也查不出有用的信息,貌似网上没有人遇到过这种错误,或许有只是他没有将自己的解决方法放到网上,或许网上有解决方式,只是我没有能力找到,没有办法,只有自己解决了。
首先肯定是看看NSXMLParser在解析的时候发生了什么,于是就在解析的回调里打印解析的信息,然后我发现一个有趣的现象,对于那部分解析出问题的数据,每次都可以解析出部分数据,解析到中间的时候会出错并停止解析。
根据这个现象我推测是因为xml中的部分数据导致解析出错,那到底是什么数据导致的呢,从服务器返回的数据是NSData类型,而NSXMLParser需要传入的数据也是NSData,但是NSData没法查看,我们将其转换为NSString,我发现将其转换为字符串是没有问题的,为了查看这写字符串是否有问题,我将其复制到浏览器中,结果Chrome也无法解析,说是里面有非法字符,并且告诉了我在哪一行哪一列,这下问题就定位出来了,我找到那一行那一列,有趣的是那里有一个不可见的字符,这里肯定有人觉得我在搞笑了,不可见字符我是怎么看出来的呢?听我慢慢道来。
当我们编辑文字时,肯定是有光标这个东西的,而我就是用光标发现了那里的一个不可见字符的,我将光标定位到出问题的那一行,然后从左往右移动光标,我发现当光标移到浏览器报错的那一列时,光标会有一个停顿,怎么形容呢,就是那里本来有一个字符,而你却看不见,所以当你在那个字符前往后移动光标时,你会发现光标没有动,而实际上光标已经跨过了那个不可见字符,当你再次移动光标时你看到的现象就完全恢复正常了。
那么我这时候就很想知道那个作死的不可见的字符到底是什么,我将那段文字复制到我的代码里,然后将前后的字符删去,将其按照%d输出(其实这里输出的方式我进行了很多种尝试,最后%d才真正有用),出人意料的我得到了27这个整型数字,通过查阅ASCII表

我发现27代表的是一个控制字符退出键,在维基百科里我发现一个以前不知道的概念叫做脱出字符表示法,而退出键的脱出字符表示法是^[,而我从服务器拿回的xml数据里经常会出现这个^[字符,而且每个^[前都有一个不可见的ESC在里面,这也印证了这个不可见字符就是一个退出建的控制符。

脱出字符表示法:通常用于终端机连接(例如Telnet通信协议),以脱出字符^开头,再接一个符号,用来让这些控制字符得以在画面上显现。虽然看起来是两个字符,但在终端机上实际只有一个字符。在绝大部分的终端机系统中,包括Windows的命令提示字符(cmd.exe)、Linux和FreeBSD,都可用Ctrl代表脱出字符,输入想要的ASCII控制字符。例如想输入空字符,就要输入Ctrl+2,而非^@,后者会显示成两字符,前者只会显示成一字符。

到了这里,一切都没有那么难解释了,这个bbs确实是在那种类似命令行的软件里使用的,而为了在里面发送彩色的文字,就需要在彩色文字前按下ESC键,再根据加入的参数,控制后面文字的显示颜色。
问题发现了,那就很好解决了,将服务器返回的数据中的ESC控制符去除掉即可,在我以为这样就可以的时候,又出了一些其他问题,我发现这里面的控制符不止ESC这一种,为了保证能得到完全有效的数据,我将可能出现的控制符全部检查了一遍

NSStringEncoding gb2312 = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
    NSString *rawStr = [[NSString alloc] initWithData:data encoding:gb2312];
    NSString *refinedStr = rawStr;
    NSString *targetStr = @"123101113273031323334353637";
    NSString *tmpStr;
    for (int i = 0; i < targetStr.length; i++) {
        tmpStr = [targetStr substringWithRange:NSMakeRange(i, 1)];
        refinedStr = [refinedStr stringByReplacingOccurrencesOfString:tmpStr withString:@""];
    }

这里data就是从服务器返回的数据

  • 非法字符

当我以为字符的处理可以告一段落进行后面的步骤的时候,又有问题出现了,NSXMLParser又有无法解析的数据了,这次要比上次更严重,因为将NSData转换为NSString竟然失败了,返回了null,于是我就从NSData转NSString入手,我在Google里输入NSData to NSString null这几个关键字,很快我便在StackOverflow里找到了解决方案,这里我将解决的代码贴出来:

- (NSData *)cleanGB2312:(NSData *)data {
    iconv_t cd = iconv_open("gb2312", "gb2312");
    int one = 1;
    iconvctl(cd, ICONV_SET_DISCARD_ILSEQ, &one);
    size_t inbytesleft, outbytesleft;
    inbytesleft = outbytesleft = data.length;
    char *inbuf  = (char *)data.bytes;
    char *outbuf = malloc(sizeof(char) * data.length);
    char *outptr = outbuf;
    if (iconv(cd, &inbuf, &inbytesleft, &outptr, &outbytesleft)
        == (size_t)-1) {
        NSLog(@"this should not happen, seriously");
        return nil;
    }
    NSData *result = [NSData dataWithBytes:outbuf length:data.length - outbytesleft];
    iconv_close(cd);
    free(outbuf);
    return result;
}

使用这段代码要导入一个头文件#import "iconv.h",而这个iconv是GNU的一个库libiconv。

/* Allocates descriptor for code conversion from encoding `fromcode' to
   encoding `tocode'. */
iconv_t iconv_open (const char* /*tocode*/, const char* /*fromcode*/);

使用的时候要注意,当tocode和fromcode不一样时会出问题,所以最好不要用这个方法来转换编码,只是用它来去除非法字符。fromcode、tocode我只填过两种:gb2312、utf-8,都没有问题,其他的编码方式应该都是类似的。

  • 编码方式

当解决了前面两个问题,我想这次应该不会再有问题了吧,然而却事与愿违,NSXMLParser又出现了解析失败的情况,这次我真的搞不懂了,还能有什么原因呢,难道是NSXMLParser不行,突然灵感一现换一个编码方式让它解析如何,或许是因为对gb2312的支持不够好,于是将gb2312的NSData解码成NSString然后再用utf-8编码传入NSXMLParser,结果所有的数据都不能解析了,这下我慌了,肯定是我使用的姿势不对。赶紧去研究一下,发现NSXMLParser解析时会先读取xml数据头部的编码信息,encoding=”gb2312”我转换为utf-8时没有将这个改掉,所以解析会失败,当我将gb2312改为utf-8后在用utf-8进行编码,OK,所有问题都没有了,至此字符上的问题全部解决了。

当你成功登录bbs后,你想浏览一些需要权限的板块或者你想在某个板块发帖,服务器该如何识别你是否已经登录了呢?这里就需要有一个东西证明你已经登录,而这个东西就是cookie。

Cookie(复数形态Cookies),中文名称为“小型文本文件”或“小甜饼”[1],指某些网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)

  • NSHTTPCookieStorage

不得不说苹果的API写的真的很好,对于cookie处理,苹果有一个叫做NSHTTPCookieStorage,当你用NSURLRequest进行请求时,cookie会被自动储存在NSHTTPCookieStorage中,而当你下一次访问同一个网站时NSURLRequest会自动使用上次的保存的cookie继续去请求,你可以用下面的方法查看所有保存的cookie。

NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
   NSLog(@"%@", cookie);
}

但是这么方便的东西,对于一个老的bbs却不能使用。首先对于这个老的bbs服务器来说他的cookie是直接放在response中的,并且是以xml格式呈现的,从狭义的角度上来说这不能算是cookie,NSURLRequest无法自动保存这中cookie。
没办法那只有拿到那个xml数据然后自己解析手动设置cookie,然而悲催的是添加在NSHTTPCookieStorage中的cookie对于喻信的服务器来说没有效果。

  • HTTP Header

通过万能的谷歌,我发现了另一种设置cookie的方式就是将cookie放在HTTP的请求头中,像下面的代码一样,而其中的cookie就是我拼接的一段字符串。

[request setValue:cookie forHTTPHeaderField:@"Cookie"];

至此身份验证的工作就搞定了。

而这cookie信息由谁来持有呢?我想既然进行了封装,那cookie也应该由这SDK来保存,同时cookie信息也在登录的成功后返回给调用者,防止调用者有其他用处。而在后面需要cookie的网络请求便可直接使用,如此封装性更好。

URL中的特殊字符

在刚开始开发的时候并没有考虑到这个问题,当应用测试时有人跟我说他的密码中含有&符,登录时提示密码错误,我当时真是醉了,居然有人在密码中使用&(其实除了&,很多字符都不能直接放在URL里),没办法不能不让别人使用&,于是赶紧去网上搜索解决方案。
解决方法就是进行百分转义,对于iOS开发总共有两种方式:

  • 苹果的接口

Core Foundation

CFURLCreateStringByAddingPercentEscapes

Foundation

- (nullable NSString *)stringByAddingPercentEncodingWithAllowedCharacters:(NSCharacterSet *)allowedCharacters

虽然他们属于不同框架,但实质上他们做的是相同的事情就是对传入字符串中的指定字符进行转换为百分转义序列
对于第一个接口可以进行一定的封装,然后可以方便的使用:

- (NSString *)percentEscapesString:(NSString *)string encoding:(CFStringEncoding)encoding {
    return (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)string, NULL, (CFStringRef) @"!*'();:@&=+$,/?%#[]", encoding);
}

而第二个接口是在iOS7及以上才可以使用,使用起来也很方便:

 - (NSString *)percentEscapesString:(NSString *)string {
    NSCharacterSet *charSet = [NSCharacterSet characterSetWithCharactersInString:@""];
    return [string stringByAddingPercentEncodingWithAllowedCharacters:charSet];
}

这样会把整个string都转换为百分转义序列,后面传入的参数是告诉他哪些字符可以不用转义。

  • 手动实现

自己写代码实现就是将字符串中的非法字符用转义后的字符去替换

+ URL 中+号表示空格:%2B
空格 URL中的空格可以用+号或者编码:%20
/ 分隔目录和子目录:%2F
? 分隔实际的URL和参数:%3F
% 指定特殊字符:%25
# 表示书签:%23
& URL 中指定的参数间的分隔符:%26
= URL 中指定参数的值:%3D

实质就是%加上符号对应的ASCII码的十六进制

正则表达式

对于这样一个老的服务器,它返回的数据都是直接用于显示在网页上的,并没有对移动端进行优化,所以优化的工作都需要客户端来做,为了匹配一些特定的内容,正则表达式当然是最好的选择了。

正则表达式,又称正规表示式、正规表示法、规则表达式、常规表示法(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些匹配某个模式的文本。

正则表达式(regular expression)描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等。

正则表达式由四个部分组成:普通字符、非打印字符、特殊字符和限定符。

. 匹配除换行符外的任意字符
w 匹配字母或者数字的字符
W 匹配任意不是字母或数字的字符
s 匹配任意的空白符(空格、制表符、换行符)
S 匹配任意不是空白符的字符
f 匹配一个换页符。等价于 x0c 和 cL。
n 匹配一个换行符。等价于 x0a 和 cJ。
r 匹配一个回车符。等价于 x0d 和 cM。
d 匹配任意数字
D 匹配任意非数字的字符
b 匹配单词的结尾或者开头的字符
B 匹配任意不是单词结尾或开头的字符
[^x] 匹配任意非x的字符。如[^[a-z]]匹配非小写字母的任意字符
^ 匹配字符串的开头
$ 匹配字符串的结尾
() 标记一个子表达式的开始和结束位置。
[] 标记一个中括号表达式的开始和结束
| 两项中选择一个

* 匹配前面的子表达式重复任意次数
+ 匹配前面的子表达式重复一次以上的次数
? 匹配前面的子表达式一次或零次
{n} 匹配前面的子表达式重复n次
{n,} 匹配前面的子表达式重复n次或n次以上
{n,m} 匹配前面的子表达式重复最少n次最多m次

需要注意的是在iOS开发中,为了匹配正则表达式的特殊字符如:[ 则需要使用:\[,因为要先将转义为真正的,再用此转义后面的[字符,再比如如果要匹配字母或者数字的字符,不能直接用w,应该用\w。

友好的时间显示

服务器返回的时间都是字符串,而且显示的方式很不友好,并不适合用于展示,可以使用NSDateFormatter将字符串转换为NSDate,而使用NSDateFormatter需要设置date format,拿这个bbs返回的时间Sun Oct 2 16:07:49 2016为例其date format为@"EEE MMM d HH:mm:ss yyyy"

EEE表示简写的星期英文,MMM表示简写的月份英文,d表示的一位或二位的日期,dd表示二位的日期(即当日期小于10号时在前面补零),HH:mm:ss分别表示小时:分钟:秒钟,yyyy表示年份。

需要注意的是有时NSDateFormatter在将字符串转换为NSDate时可能会返回nil,有时可能是因为date format不正确,但有时即使正确也不行,对于这种情况有一种解决办法就是设置locale identifier:

[_dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];

而且需要有趣的是只有设置为@"en_US_POSIX"才不会出问题。

下面就是将转换的date以合适的方式展示出来:

- (NSString *)compareCurrentTime:(NSString *)str withDateFormatter:(NSDateFormatter *)formatter{
    NSDate *timeDate = [formatter dateFromString:str];

    NSTimeInterval  timeInterval = [timeDate timeIntervalSinceNow];
    timeInterval = -timeInterval;

    long temp = 0;
    NSString *result;
    if (timeInterval < 60) {
        result = [NSString stringWithFormat:@"刚刚"];
    }
    else if((temp = timeInterval/60) <60){
        result = [NSString stringWithFormat:@"%ld分钟前",temp];
    }
    else if((temp = temp/60) <24){
        result = [NSString stringWithFormat:@"%ld小时前",temp];
    }
    else if((temp = temp/24) <30){
        result = [NSString stringWithFormat:@"%ld天前",temp];
    }
    else if((temp = temp/30) <12){
        result = [NSString stringWithFormat:@"%ld个月前",temp];
    }
    else{
        temp = temp/12;
        result = [NSString stringWithFormat:@"%ld年前",temp];
    }
    return  result;
}

这里我直接将代码贴出,首先用NSDate提供的方法:timeIntervalSinceNow 获取该时间与目前的时间的差值,该差值的单位是秒,利用该值在下面的一连串判断中,转换为符合常人阅读习惯的时间,比如在一分钟内就显示刚刚,大于一分钟小于一小时就显示几分钟前,以此类推几小时前,几天前,几个月前,几年前,如此显示更加友好。

结语

关于网络接口的封装,大致就是这些,后面我还会带来另外三篇博客。这里给出此应用的github地址:喻信星空

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/dp948080952/article/details/52650092
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-05-22 15:48:34
  • 阅读 ( 1031 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢