天天看点

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

我们前面的章节中就一直新建cocoa class,那么cocoa到底是什么,它和我们前面以及后面要讲的内容到底有什么关系呢?objective-c开发中经常用到nsobject,那么这个对象到底是谁?它为什么又出现在objective-c中间呢?今天我们将揭开这层面纱,重点分析在ios开发中一个重要的框架foundation,今天的主要内容有:

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#foundation">foundation概述</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#struct">常用结构体</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#date">日期</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#string">字符串</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#array">数组</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#dictionary">字典</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#boxing">装箱和拆箱</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#reflector">反射</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#copy">拷贝</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#io">文件操作</a>

<a href="http://www.cnblogs.com/kenshincui/p/3885689.html#archiver">归档</a>

为什么前面说的内容中新建一个类的时候我们都是选择cocoa class呢?cocoa是什么呢?

cocoa不是一种编程语言(它可以运行多种编程语言),它也不是一个开发工具(通过命令行我们仍然可以开发cocoa程序),它是创建mac os x和ios程序的原生面向对象api,为这两者应用提供了编程环境。

我们通常称为“cocoa框架”,事实上cocoa本身是一个框架的集合,它包含了众多子框架,其中最重要的要数“foundation”和“uikit”。前者是框架的基础,和界面无关,其中包含了大量常用的api;后者是基础的ui类库,以后我们在ios开发中会经常用到。这两个框架在系统中的位置如下图:

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

其实所有的mac os x和ios程序都是由大量的对象构成,而这些对象的根对象都是nsobject,nsobject就处在foundation框架之中,具体的类结构如下:

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档
iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档
iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

通常我们会将他们分为几类:

值对象

集合

操作系统服务:文件系统、url、进程通讯

通知

归档和序列化

表达式和条件判断

objective-c语言服务

uikit主要用于界面构架,这里我们不妨也看一下它的类结构:

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

在foundation中定义了很多常用结构体类型来简化我们的日常开发,这些结构体完全采用objective-c定义,和我们自己定义的结构体没有任何区别,之所以由框架为我们提供完全是为了简化我们的开发。常用的结构体有nsrange、nspoint、nssize、nsrect等

可以看到对于常用结构体在foundation框架中都有一个对应的make方法进行创建,这也是我们日后比较常用的操作;而且与之对应的还都有一个nsstringfromxx方法来进行字符串转换,方便我们调试。上面也提到nssize其实就是cgsize,nsrect其实就是cgrect,我们可以通过查看代码进行确认,例如nssize定义:

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

继续查看cgsize的代码:

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

接下来熟悉一下foundation框架中日期的操作

在objc中字符串操作要比在c语言中简单的多,在下面的例子中你将看到字符串的初始化、大小写转化、后缀前缀判断、字符串比较、字符串截取、字符串转换等,通过下面的例子我们基本可以掌握常用的字符串操作(注意这些内容虽然基本,但却是十分常用的操作,需要牢记):

注意:上面代码注释中提到的需要释放内存指的是在mrc下的情况,当然本质上在arc下也需要释放,只是这部分代码编译器会自动创建。

在objc中路径、文件读写等操作是利用字符串来完成的,这里通过几个简单的例子来演示(首先在桌面上新建一个test.txt文件,里面存储的内容是”hello world,世界你好!”)

注意:在上面的例子中我们用到了可变数组,下面会专门介绍。

我们知道在字符串操作过程中我们经常希望改变原来的字符串,当然这在c语言中实现比较复杂,但是objc为我们提供了新的可变字符串类nsmutablestring,它是nsstring的子类。

下面将演示常用的数组操作:初始化、数组对象的方法执行、数组元素的遍历、在原有数组基础上产生新数组、数组排序等

需要注意几点:

nsarray中只能存放对象,不能存放基本数据类型,通常我们可以通过在基本数据类型前加@进行转换;

数组中的元素后面必须加nil以表示数据结束;

makeobjectsperformselector执行数组中对象的方法,其参数最多只能有一个;

上面数组操作中无论是数组的追加、删除、截取都没有改变原来的数组,只是产生了新的数组而已;

对象的比较除了使用系统自带的方法,我们可以通过自定义比较器的方法来实现;

下面看一下可变数组的内容:

可变数组中的元素后面必须加nil以表示数据结束;

往一个可变数组中添加一个对象,此时这个对象的引用计数器会加1,当这个对象从可变数组中移除其引用计数器减1。同时当整个数组销毁之后会依次调用每个对象的releaes方法。

在不可变数组中无论对数组怎么排序,原来的数组顺序都不会改变,但是在可变数组中如果使用sortusingselector:排序原来的数组顺序就发生了变化。

字典在我们日常开发中也是比较常用的,通过下面的代码我们看一下在objc中的字典的常用操作:初始化、遍历、排序

注意:同数组一样,不管是可变字典还是不可变字典初始化元素后面必须加上nil以表示结束。

其实从上面的例子中我们也可以看到,数组和字典中只能存储对象类型,其他基本类型和结构体是没有办法放到数组和字典中的,当然你也是无法给它们发送消息的(也就是说有些nsobject的方法是无法调用的),这个时候通常会用到装箱(boxing)和拆箱(unboxing)。其实各种高级语言基本上都有装箱和拆箱的过程,例如c#中我们将基本数据类型转化为object就是一个装箱的过程,将这个object对象转换为基本数据类型的过程就是拆箱,而且在c#中装箱的过程可以自动完成,基本数据类型可以直接赋值给object对象。但是在objc中装箱的过程必须手动实现,objc不支持自动装箱。

在objc中我们一般将基本数据类型装箱成nsnumber类型(当然它也是nsobject的子类,但是nsnumber不能对结构体装箱),调用其对应的方法进行转换:

+(nsnumber *)numberwithchar:(char)value;

+(nsnumber *)numberwithint:(int)value;

+(nsnumber *)numberwithfloat:(float)value;

+(nsnumber *)numberwithdouble:(double)value;

+(nsnumber *)numberwithbool:(bool)value;

+(nsnumber *)numberwithinteger:(nsinteger)value;

拆箱的过程就更加简单了,可以调用如下方法:

-(char)charvalue;

-(int)intvalue;

-(float)floatvalue;

-(double)doublevalue;

-(bool)boolvalue;

简单看一个例子

上面我们看到了基本数据类型的装箱和拆箱过程,那么结构体呢?这个时候我们需要引入另外一个类型nsvalue,其实上面的nsnumber就是nsvalue的子类,它包装了一些基本数据类型的常用装箱、拆箱方法,当要对结构体进行装箱、拆箱操作我们需要使用nsvalue,nsvalue可以对任何数据类型进行装箱、拆箱操作。

事实上对于常用的结构体foundation已经为我们提供好了具体的装箱方法:

+(nsvalue *)valuewithpoint:(nspoint)point;

+(nsvalue *)valuewithsize:(nssize)size;

+(nsvalue *)valuewithrect:(nsrect)rect;

对应的拆箱方法:

-(nspoint)pointvalue;

-(nssize)sizevalue;

-(nsrect)rectvalue;

那么如果是我们自定义的结构体类型呢,这个时候我们需要使用nsvalue如下方法进行装箱:

+(nsvalue *)valuewithbytes:(const void *)value objctype:(const char *)type;

调用下面的方法进行拆箱:

-(void)getvalue:(void *)value;

通过前面的介绍大家都知道无论在数组还是在字典中都必须以nil结尾,否则数组或字典无法判断是否这个数组或字典已经结束(与c语言中的字符串比较类似,c语言中定义字符串后面必须加一个”\0”)。但是我们有时候确实想在数据或字典中存储nil值而不是作为结束标记怎么办呢?这个时候需要使用nsnull,这个类是一个单例,只有一个null方法。简单看一下:

我们知道在objc中很多关键字前都必须加上@符号,例如@protocol、@property等,当然objc中的字符串必须使用@符号,还有就是%@可以表示输出一个对象。其实@符号在新版的objc中还有一个作用:装箱。

相信聪明的童鞋在前面的例子中已经看到了,这里简单的介绍一下(在下面的演示中你也将看到很多objc新特性)。

由于objc动态性,在objc中实现反射可以说是相当简单,下面代码中演示了常用的反射操作,具体作用也都在代码中进行了注释说明:

account.h

account.m

person.h

person.m

main.m

对象拷贝操作也比较常见,在objc中有两种方式的拷贝:copy和mutablecopy,这两种方式都将产生一个新的对象,只是后者产生的是一个可变对象。在objc中如果要想实现copy或者mutablecopy操作需要实现nscopy或者nsmutablecopy协议,拷贝操作产生的新的对象默认引用计数器是1,在非arc模式下我们应该对这个对象进行内存管理。在熟悉这两种操作之前我们首先需要弄清两个概念:深复制(或深拷贝)和浅复制(或浅拷贝)。

浅复制:在执行复制操作时,对于对象中每一层(对象中包含的对象,例如说属性是某个对象类型)复制都是指针复制(如果从引用计数器角度出发,那么每层对象的引用计数器都会加1)。

深复制:在执行复制操作时,至少有一个对象的复制是对象内容复制(如果从引用计数器角度出发,那么除了对象内容复制的那个对象的引用计数器不变,其他指针复制的对象其引用计数器都会加1)。

注: 指针拷贝:拷贝的是指针本身(也就是具体对象的地址)而不是指向的对象内容本身。 对象复制:对象复制指的是复制内容是对象本身而不是对象的地址。 完全复制:上面说了深复制和浅复制,既然深复制是至少一个对象复制是对象内容复制,那么如果所有复制都是对象内容复制那么这个复制就叫完全复制。

对比copy和mutablecopy其实前面我们一直还用到一个操作是retain,它们之间的关系如下:

retain:始终采取浅复制,引用计数器会加1,返回的对象和被复制对象是同一个对象1(也就是说这个对象的引用多了一个,或者说是指向这个对象的指针多了一个);

copy:对于不可变对象copy采用的是浅复制,引用计数器加1(其实这是编译器进行了优化,既然原来的对象不可变,复制之后的对象也不可变那么就没有必要再重新创建一个对象了);对于可变对象copy采用的是深复制,引用计数器不变(原来的对象是可变,现在要产生一个不可变的当然得重新产生一个对象);

mutablecopy:无论是可变对象还是不可变对象采取的都是深复制,引用计数器不变(如果从一个不可变对象产生一个可变对象自然不用说两个对象绝对不一样肯定是深复制;如果从一个可变对象产生出另一个可变对象,那么当其中一个对象改变自然不希望另一个对象改变,当然也是深复制)。

可变对象:当值发生了改变,那么地址也随之发生改变; 不可变对象:当值发生了改变,内容首地址不发生变化; 引用计数器:用于计算一个对象有几个指针在引用(有几个指针变量指向同一个内存地址);

为了方便大家理解上面的代码,这里以图形画出str1、str2、str3、str4在内存中的存储情况:

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

从上面可以清楚的看到str1和str3同时指向同一个对象,因此这个对象的引用计数器是2(可以看到两箭头指向那个对象),str2和str4都是两个新的对象;另外objc引入对象拷贝是为了改变一个对象不影响另一个对象,但是我们知道nsstring本身就不能改变那么即使我重新复制一个对象也没有任何意义,因此为了性能着想如果通过copy方法产生一个nsstring时objc不会再复制一个对象而是将新变量指向同一个对象。

注意网上很多人支招在arc模式下可以利用_objc_rootretaincount()或者cfgetretaincount()取得retaincount都是不准确的,特别是在对象拷贝操作之后你会发现二者取值也是不同的,因此如果大家要查看retaincount最好还是暂时关闭arc。

要想支持copy或者mutablecopy操作那么对象必须实现nscoping协议并实现-(id)copywithzone:(nszone*)zone方法,在foundation中常用的可复制对象有:nsnumber、nsstring、nsmutablestring、nsarray、nsmutablearray、nsdictionary、nsmutabledictionary。下面看一下如何让自定义的类支持copy操作:

在上面的代码中重点说一下test2这个方法,在test2方法中我们发现当修改了person2.name属性之后person1.name也改变了,这是为什么呢?我们可以看到在person.m中自定义实现了copy方法,同时实现了一个浅拷贝。之所以说是浅拷贝主要是因为我们的name属性参数是直接赋值完成的,同时由于name属性定义时采用的是assign参数(默认为assign),所以当通过copy创建了person2之后其实person2对象的name属性和person1指向同一个nsmutablestring对象。通过图形表示如下:

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

上面test2的写法纯属为了让大家了解复制的原理和本质,实际开发中我们很少会遇到这种情况,首先我们一般定义name的话可能用的是nsstring类型,根本也不能修改;其次我们定义字符串类型的话一般使用(copy)参数,同样可以避免这个问题(因为nsmutablestring的copy是深复制)。那么如果我们非要使用nsmutabestring同时不使用属性的copy参数如何解决这个问题呢?答案就是使用深复制,将-(id)copywithzone:(nszone *)zone方法中person1.name=_name改为,person1.name=[_name copy];或person1.name=[_name mutablecopy]即可,这样做也正好满足我们上面对于深复制的定义。

在好多语言中字符串都是一个特殊的对象,在objc中也不例外。nsstring作为一个对象类型存储在堆中,多数情况下它跟一般的对象类型没有区别,但是这里我们需求强调一点那就是字符串的引用计数器。

看完上面的例子如果不了解nsstring的处理你也许会有点奇怪(注意上面的代码请在xcode5下运行)?请看下面的解释

str1是一个字符串常量,它存储在常量区,系统不会对它进行引用计数,因此无论是初始化还是做retain操作其引用计数器均为-1;

str3、str4、str5创建的对象同一般对象类似,存储在堆中,系统会对其进行引用计数;

采用stringwithstring定义的变量有些特殊,当后面的字符串是字符串常量,则它本身就作为字符串常用量存储(str2),类似于str1;如果后面的参数是通过类似于str3、str4、str5的定义,那么它本身就是一个普通对象,只是后面的对象引用计数器默认为1,当给它赋值时会做一次拷贝操作(浅拷贝),引用计数器加1,所有str2_1引用计数器为2;

str6其实和str1类似,虽然定义的是可变数组,但是它的本质还是字符串常量,事实上对于可变字符串只有为字符串常量时引用计数器才为-1,其他情况它的引用计数器跟一般对象完全一致;

后记:注意上面这段代码的运行结果是在xcode5中运行的结果,事实上针对最新的xcode6由于llvm的优化,只有str2_1和str7的引用计数器为1(str7 retain一次后第二次为2),其他均为-1。

在今天的最后一节内容中让我们看一下foundation中文件操作,下面将以一个例子进行说明:

归档,在其他语言中又叫“序列化”,就是将对象保存到硬盘;解档,在其他语言又叫“反序列化”就是将硬盘文件还原成对象。其实归档就是数据存储的过程,在ios中数据的存储有五种方式:

xml属性列表(plist归档)

nsuserdefaults(偏好设置)

nskeyedarchiver归档(加密形式)

sqlite3(嵌入式数据库)

core data(面向对象方式的嵌入式数据库)

当然关于2、4、5点不是我们今天介绍的重点,这个在ios开发过程中我们会重点说到。

首先我们先来看一下xml属性列表,xml属性列表进行归档的方式是将对象存储在一个plist文件中,这个操作起来比较简单,其实相当于xml序列化。但是同时它也有缺点:一是这种方式是明文保存的;二是这种方式操作的对象有限,只有nsarray、nsmutablearray、nsdictionary、nsmutabledictionary支持(归档时只要调用对应的writetofile方法即可,解档调用arraywithcontentsoffile或dictionarywithcontentsoffile,注意像nsstring、nsnumber、nsdata即使有这个方法它存储的也不是xml格式)。

生成的文件如下

iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档
iOS开发系列—Objective-C之Foundation框架概述Foundation概述常用结构体日期字符串数组字典装箱和拆箱反射拷贝文件操作归档

如果要针对更多对象归档或者需要归档时能够加密的话就需要使用nskeyedarchiver进行归档和解档,使用这种方式归档的范围更广而且归档内容是密文存储。从归档范围来讲nskeyedarchiver适合所有objc对象,但是对于自定义对象我们需要实现nscoding协议;从归档方式来讲nskeyedarchiver分为简单归档和复杂对象归档,简单归档就是针对单个对象可以直接将对象作为根对象(不用设置key),复杂对象就是针对多个对象,存储时不同对象需要设置不同的key。

首先看一下系统对象两种归档方式(注意由于本章主要介绍foundation内容,下面的程序是os x命令行程序并没有创建成ios应用,如果移植到到ios应用下运行将nsarchiver和nsunarchiver换成nskeyedarchiver和nskeyedunarchiver。虽然在foundation部分ios和os x在设计上尽可能通用但是还存在着细微差别。)

接下来看一下自定义的对象如何归档,上面说了如果要对自定义对象进行归档那么这个对象必须实现nscoding协议,在这个协议中有两个方法都必须实现:

-(void)encodewithcoder:(nscoder *)acoder;通过给定的archiver对消息接收者进行编码;

-(id)initwithcoder:(nscoder *)adecoder;从一个给定的unarchiver的数据返回一个初始化对象;

这两个方法分别在归档和解档时调用。下面通过一个例子进行演示(注意对于自定义类的多对象归档与系统类多对象归档完全一样,代码中不再演示):

今天的文章就到这里了,内容确实不少,但是要用一篇文章把foundation的所有内容说完也是完全不现实的。这篇文章有一部分内容并没有详细的解释代码,这部分内容主要是个人认为比较简单,通过注释大家就可以理解。不过并不是说这部分内容不重要,而是这些内容多数是记忆性的东西,不需要过多解释。

继续阅读