欢迎光临
专注android技术,聚焦行业精粹,传扬中国传统文化,我们一直在努力

Objective-C学习笔记

1、前言

Objective-C(以下简称OC)实质上就是在C语言的基础上添加了一些面向对象的特性,同时添加了Smalltalk 语言的优势,所以熟悉C语言的朋友对于ObjectIve-C会感觉不陌生,且会对smalltalk 这种消息类型的语言的巧妙构思所折服。

学习OC主要是由于目前移动平台中的两大平台Android和IOS不分伯仲,平分秋色。如果说你是一个独立程序开发者,你怎么可能放弃两个平台的任何一个呢?通过这篇文档相信已经了解OOP、C语言的你能很快感受到OC给你带来的无穷魅力~

这篇文章我们将了解:

  1. cocoa和objective-c的历史
  2. 第一个HelloWorld程序认识OC程序结构
  3. OC的OOP
  4. Object-C的语言特性
  5. 深入学习:内存管理、异常捕获、并发

本篇文章是基于《Objective-C基础教程(第2版)》,想要详细学习OC的朋友们,我推荐这本书做为入门书籍。它知识点比较系统和全面,相对于市面上的其它的书籍,它的知识点会描述的更详细,新旧特性会反复强调并贯串全书。

 

2、cocoa和Objective-C的历史

Cocoa和OC是苹果公司OS X和iOS操作系统的核心。OS X和iOS出现的时间比OC和Cocoa晚。

  1. OC早在20世纪80年代初,由Brad Cox设计,它融合了流行的、可移植的C语言和优雅的Smalltalk语言的优势
  2. 1985年史蒂夫.乔布斯创立了NeXT公司,公司使用Unix做为操作系统并创建了NextSTEP(用OC语言开发的一款功能强大的用户界面工具包)
  3. 苹果公司在1996年收购了NeXT(当时苹果公司已濒临绝境,在收购NeXT后,乔布斯成为了苹果的CEO,并开始改革)。NextSTEP更名为Cocoa

那OC呢,当时苹果公司的开发工具(包括Cocoa)都是免费提供的,使用这套工具需要基本的OC知识。OC和Cocoa经过多年的打磨,已经演化成了一个美观精致且功能强大的工具集。近些年,iOS已经成为了非常热门的开发平台,因此OC依然很受欢迎。

 

3、HelloWorld

3.1、环境准备

首先你要在App Store上下载Xcode。打开Xcode创建一个Xcode的Command Line Tool(命令行工具)(考虑到这是一篇关于OC的文章,所以不涉及其它的UI界面的编码,所以命令行工具的工程会很简洁)。此时我们能获得一个包含基本代码的工程界面如下:

我们点击左上角的运行,可以在命令行输出我们想要的“Hello World!!”。结果如下:

我们看到这里包含了一段最简单的HelloWorld程序,且代码风格和C很像,但又有一些不同。这里讲解一下:

 

3.2、工程结构介绍

我们看到main.m文件是.m后缀,m代表message,顾名思义是以OC的消息类型的特征命名。和C语言很像,OC的源代码也分为两部分:类的申明类的实现类的申明对应.h头文件,类的实现对应.m实现文件。

在OC里面类的申明类的实现分别对应@interface@implementation编译器指令,这里涉及到OC的OOP相关的知识,下面OOP会详细介绍。

我们简单的将HelloWorld程序按照入口和日志打印的实现来拆分为3个文件:主入口(main.m)、打印方法申明的头文件(print.h)和打印方法实现的文件(print.m),拆分后如下:

接下来我们在下面的OOP中继续介绍main.m中的日志打印逻辑是如何拆分到print.hprint.m两个文件中的。

 

4、OC的OOP

4.1、‘HelloWorld’代码结构改造

既然OC的全名是Objective-C,那么顾名思义它就是一门面向对象的编程语言(OOP)。我们就可以按照OOP的思维重新改造我们的“Hello World”程序目录了。

上面已经介绍,在OC里面类的申明实现分别对应编译指令@interface@implementation。于是我们可以将打印日志输出的代码实现拆分到print.h和print.m 2个文件中。而main.m只需要调用对应的class的方法即可:

main.m

main.m里先import print.h,然后调用该头文件里申明的PrintClass类的类方法print,并传入参数“Hello World!

print.h

print.h首先引入了基础头文件Foundation/Foundation.h,因为OC里面都是面向对象的,所以接着需要定义了一个类,OC里面申明一个类使用关键字@interface (在android里面interface代表接口,这里interface是表示类。OC里面接口有其它的概念描述,例如protocol,另外定义一个该类的实现用@implementation

由于我们基于Cocoa框架,所以@interface新申明的类需要至少继承NSObject这一点和Java非常像),继承的语法是使用“”表明之后是需要继承的类。

print.m

print.m里面首先import print.h,然后通过@implementation实现了PrintClass类申明的print方法。

运行结果如下:

4.2、一个OOP中多态的例子

我们写一个这样的示例:有一个shape类,它包含设置颜色和设置边界、以及绘制的draw方法。另一个类Circle,它实现自己的draw方法,同时根据自己的需要重写了setFillColor方法。UML图如下:

这个简单的示例包含了OOP中的多态和重写,源码如下:

shape.h

shape.h 中申明了Shape类,并继承自NSObject。它包含2个实例变量:fillColor,bounds。同时申明了3个方法:setFillColor,setBounds,draw

shape.m

shape.m实现了setFillColor和setBounds,用于设置自己的两个实例变量,同时对draw方法提供了一个空实现。

circle.h

在circle.h中申明了Circle类,它继承自Shape类。

circle.m

在circle.m 我们重写了setFillColor和draw方法,并提供了circle自己特有的实现。例如setFillColor方法对入参的颜色如果是蓝色,会修改为红色。

接下来我们实现程序入口main.m

main.m:

main方法里面我们构建了Circle对象,并通过setBounds和setFillColor对该circle对象设置了它的两个属性,我们此时调用circle对象的draw方法会发生什么?我们来剖析一下,其实用OOP的概念来套,可以很快的得出结论:

  1. Circle对象通过new方法构建出来,它也是一个Shape对象(继承是is-a的关系),所以它继承了Shape类的两个属性和3个方法
  2. Circle对象重写了Shape类的setFillColor和draw两个方法,而另一个setBounds会保留父类Shape的实现
  3. 我们调用circle对象的setFillColor,会优先找到circle类的实现,这里检查到设置的是蓝色,做了一个特殊处理,转成了红色
  4. 当我们调用circle对象的draw方法时,会找到circle类中定义的draw方法的实现并调用,此时会通过NSLog输出自己的属性

所以结果应该是在console打印了bounds和红色值,实际输出结果如下:

 

4.3、对象初始化

这里单独介绍对象的初始化是因为OC中的对象初始化和其它平台有所不同,OC里面有两种初始化,第一种是[类名 new],第二种是[[类名 alloc] init],这两种方法是等价的,不过cocoa推荐的是使用alloc和init。

4.3.1、alloc

alloc的职责是为该类分配一块足够大的内存(用于存放该类的全部实例变量),并将这块内存区域全部初始化为0。

该默认初始化为0的操作会将BOOL类型的变量初始化为NO,int类型的变量被初始化为0,float类型变量被初始化为0.0,所有的指针被初始化为nil。

4.3.2、初始化

OC中没有固定的初始化方法名,你可以随意自定义,但是一般会遵守OC的命名约定——方法名以init开头。初始化的写法主要注意两个点:推荐初始化写法,init方法命名约定

4.3.2.1、推荐初始化写法

  • 嵌套的写法实现内存分配 + 初始化,例如:

这样写法的主要原因是初始化方法返回的对象可能与分配的对象不同

 

  • 调用父类的初始化方法时需要更新self

这行代码也意味着self可能发生了改变,因为init方法可能会返回完全不同的对象。如果从init方法返回了一个新对象,则需要更新self, 以便其后的实例变量的引用可以被映射到正确的内存位置。

另外注意这个赋值操作只影响该init方法中的self值,而不影响该方法范围以外的任何内容。

 

  • init方法名约定

有些对象拥有多个以init开头的方法名,需要记住的是,这些init方法其实没有什么特别的,只是遵循命名约定的普通方法。

我们以NSString类中的一些初始化方法举例:

> 初始化一个新的空字符串

> 初始化一个新的格式化的字符串

使用示例如下:

输出如下:

> 读取一个指定url的文本文件

代码示例:

输出结果:

 

4.4、属性

属性是为了提升编码效率而引入的。在我们为一个类的实例变量编写访问方法时需要编写很多冗长的代码(例如-setBlah, -blah),而庆幸的是苹果公司在Objective-C 2.0中引入了属性(property),它组合了新的预编译指令和新的属性访问器方法。

本节将围绕以下几点介绍属性:

  • 自动getter setter
  • 自动创建实例变量
  • 访问属性的点表达式,左值和右值
  • 属性拓展
  • 修改属性名称
  • @dynamic
  • 属性的范围

 

4.4.1、自动getter setter

回顾一下上面Shape的代码,它包含了两个实例变量:fillColor和bounds。当我们需要访问这两个实例变量时,我们编写了setFillColor、setBounds。将这段代码改为属性风格的写法如下:

我们将set方法移除,替换成了 @property 关键字描述的属性,此时我们就可以通过-setFillColor: 和 -setBounds: 来设置属性,也可以通过-fillColor和-bounds来访问属性了。于是我们不需要改动main.m 来运行一下程序,看下运行结果同之前的结果,如下:

由此看到属性为我们大大减少了编写setter, getter方法的枯燥无味的工作量了。

 

4.4.2、有了属性,实例变量也可以不存在

在OC中所有的属性都是基于变量的,所以我们在使用@property定义属性时,编译器会自动创建与属性名称相同的实例变量。我们修改代码删除类头文件中申明的两个实例变量,保留两个属性,看运行结果如何:

shape.h

shape.m

circle.m

覆写的draw方法里面对bounds变量的访问使用点表达式,这个稍后介绍。

运行结果:

和前面的写法输出一致

 

4.4.3、点表达式的妙用

属性的引入也带来了一些新的语法特性,使我们可以更加容易的访问对象的属性。例如4.4.1节circle.m的draw方法中访问自己实例变量时使用的方式:

这个点表达式(.)看起来和C语言的结构体访问,Java语言中的对象访问有些相似,这个的确如此。在OC中点表达式和这些语言的使用是一致的,体现在以下两点:

  • 点表达式出现在等号(=)的左边,该实例变量的setter方法(例如:-setBounds: )将被调用
  • 点表达式出现在等号(=)的右边,则该实例变量的getter方法(例如:-bounds)将被调用

 

4.4.4、属性的拓展

属性通过自动的setter和getter方法帮助我们提升了编码效率,不过实际编码过程中我们在setter方法里面可能希望做一些额外的常用操作,例如保留对象的引用,或者想要复制一个字符串的值,但是在自动生成的方法里面我们可以怎么做呢?属性的拓展帮助我们解决了这个问题。

我们基于上面的示例,给shape类加一个NSString 类型的实例变量name,此时该name先不设置为属性,因为我们想自己先实现它的setter方法,并在该setter方法里面将外部传入的参数复制到该name实例变量,示例代码如下:

shape.h

shape.m

shape.m里面我们实现了setter和getter方法,setter方法里面我们通过copy方法复制了一份传入的值。

main.m

draw方法前面我们调用了setName方法,该方法内部我们复制了传入的字符串值,输出如下:

接下来我们仍然用回property属性来实现上面的功能,shape.h 代码如下:

注意看name的property描述变为:@property (copy) NSString *name; 同时我们删除shape.m 里面的setter和getter方法,同时circle.m里面draw方法的name应用改为self.name,输出结果如下,和上面的结果是相同的:

除了copy特性,我们还可以使用retainatomicreadwritereadonly等。如果你没有对property指定任何特性,它会使用默认atomicassign来修饰。关于assign是啥,会在下面的内存管理章节介绍。默认值见:链接

顾名思义如果我们给属性添加了readonly特性,此时如果你尝试去调用写方法,编译器会报错。

我们修改之前的代码,将name property的特性中添加一个readonly,修改如下:@property (copy, readonly) NSString *name; 此时编译结出错果如下:

 

4.4.5、修改属性名称

如果我们想隐藏实例变量名,而让公开的属性使用另一个名称怎么做呢?我们在头文件申明了属性之后,我们可以在实现文件中添加以下内容修改属性名称

@synthesize 属性名 = 实例变量名;

我们使用这种方法修改我们的代码,首先shape.h 里面我们把实例名变量 name 换一个不想对外的名字 private_name,不过我们的属性名称不变,仍然叫name,改动后代码如下:

同时我们修改shape.m ,改动如下:

其它代码不做任何改变,编译后运行输出如下,和之前是一致的:

 

4.4.6、@dynamic

前面讲的属性都是和实例变量一一对应的,如果我们没有定义某个名字的实例变量,而我们又声明了这个名字的一个属性,编译器会为我们创建相应名字的实例变量,如果我们创建一个属性但又不想要创建对应的实例变量,需要怎么做呢?我们可以使用@dynamic指令来指定这个属性并告诉编译器不需要去创建实例变量getter方法

我们在shape.h 中添加一个新的属性——role_name,这个属性我们自己实现一个getter方法,并且不希望编译器帮助我们去创建对应的实例变量,我们使用@dynamic 来修饰它,代码如下:

shape.h

在shape.h里面我们定义了一个属性名为roleName: @property (readonly) NSString *roleName; 这个roleName 是动态根据属性name计算出来的值,所以我们需要自定义getter方法,同时我们不需要生成对应的实例变量,于是我们在shape.m 里面申明了@dynamic roleName; 和定义了roleName getter方法(之所以这么绕,是因为我们还是希望使用property提供的一些语法糖的便利,但又能自定义一些行为的改动),代码如下:

我们也修改了circle.m,打印roleName getter方法,代码如下:

输出如下:

日志中打印出了roleName getter方法的输出内容

 

4.4.7、属性的限制

了解了属性如此强大的功能,我们也需要了解属性无法做到的事情,例如我们现在setter方法都是一个参数用于赋予一个值,如果setter方法是下面这样的多个参数就做不到了:

所以属性只支持替代 -setBlah-blah 方法,但是不支持那些需要接收额外参数的方法。

 

4.5、类别

类别是一种为现有的类添加新方法的方式,有了类别就可以为任何类添加新的方法,包括那些没有源代码的类。

类别可以以单独的文件存在,通常以“类名称+类别名称”的风格命名。我们来开始一个例子,为NSString添加一个获取一个NSNumber类型的字符串长度的方法,类别文件的名称为NSString+NumberConvenience。我们可以使用Xcode为我们创建这样一个类别文件:

4.5.1、创建类别文件

1、新建文件,选择Objective-C File

2、给自己的类别取个名字,并在File Type 选择Category

3、选择位置保存后,查看文件和类别的代码

创建完成后我们看到工程的目录里面多了2个文件:NSString+NumberConvenience.h, NSString+NumberConvenience.m

类别文件有以下两个特征:

  • 文件名:“基于的类名称+类别名称”的风格命名
  • 类别的申明和实现文件中的类名称:基于的类名称(类别名称)

我们可以看到无论是文件名称还是类名称,都明显的分为两个部分:基于的类名称,类别名称。这意味着类别叫做NumberConvenience,而且它是添加给NSString类的。换句话说就是:“我们为NSString类添加了一个名为NumberConvenience”的类别。另外只要保证类别的名称唯一,你可以向一个类中添加任意数量的类别。

4.5.2、类别的局限性

  • 不能向类别中添加实例变量(类别没有空间容纳实例变量
  • 可以向类别中添加 @dynamic 类型的属性
  • 名称冲突:类别的方法如果与现有的方法重名,类别具有更高的优先级

前面我们了解到@dynamic 修饰的属性有两个特点:

  • 不会创建该属性的getter方法
  • 不会自动创建该属性的实例变量

 

4.5.3、为类别添加新的方法

接下来我们为上面新加的类别添加一个新的方法:lengthAsNumber,实现如下:

NSString+NumberConvenience.h

NSString+NumberConvenience.m

 

接下来我们修改circle.m 的draw打印函数,添加打印name 的字符串长度:

注意:我们首先在circle.m的头文件import了 “NSString+NumberConvenience.h” ,接下来我们就可以对所有的NSString类型调用lenghtAsNumber了。我们对self.name 返回的NSString类型字符串调用了lengthAsNumber方法,最终输出如下:

 

4.5.4、类的拓展

接下来介绍一个可以添加实例变量的特殊类别,它不需要类别名称——类拓展,它具有以下的特点:

  • 这种类别不需要类别名称
  • 可以添加实例变量
  • 你可以将只读权限改成可读写权限
  • 创建数量不限
  • 可以在你自己的类中使用它

举个例子,我们在之前的示例中的shape.h中为已有的Shape类添加一个拓展:

该拓展类没有名字,它添加了一个新的实例变量privateName3,并设置属性为可读写。同时我们在原Shape类中加了privateName实例变量,并设置属性为readonly,接下来在类拓展中修改该属性的特性为可读写。

接着我们在main.m中通过setter方法给新加的这两个属性设置值,在Circle.m打印这两个属性:

main.m

circle.m

输出结果如下:

我们回顾一下这个Shape类的拓展,看起来就像在定一个类,我们所做的基本上就是获取Shape类,并通过添加属性来拓展它:

4.5.4.1、使用类别来组织代码

使用类别可以将一个庞大的类的代码拆分成若干个类别来存放,这样拆分的一个最大的好处是小规模管理。例如NSWindow类,我们可以看到官方接口是这样的

它对应大量的类别声明,包括以下这些:

 

4.5.5、非正式协议和委托类别

Cocoa中的类经常会使用一种名为委托(delegate)的技术,委托是一种对象,由另一个类请求执行某些工作。举个例子,Cocoa中的滚动列表是由AppKit中的NSTableView类处理的。当tableview对象准备好执行某些操作(比如选择用户刚刚点击的行)时,它询问其委托对象是否能选择此行。tableView对象会向其委托对象发送一条消息:

该委托方法可以查看tableView对象和第几行并确定是否能够选择该行。如果表中包含了不该选择的行,则委托对象会告诉我们这些行是无法选择的。

知道了委托对象的概念,那么委托对象与类别有什么关系呢?它和面向对象的OOP的is-a 不同,当我们定义一个类时我们通过类名告诉用户这个类可能包含的方法,然后所有同类型的类都可以继承自它,表明它们也is-a这个类型的类。然而类别并不会强制要求它的对象实例一定is-a 某个类型,也就是它一定有某些方法的实现,而是它可能有某些方法的实现,所以在使用“委托类别”时需要通过“响应选择器”来检查对象是否能响应我们发送的消息。

例如我们定义了一个这样的NSObject的类别:

它包含两个方法,通过这种方法创建的NSObject的类别,我们定义的任何类的对象都可以做为委托对象来使用了,这种方式非常方便,它既不需要从特定类型的类继承,也不需要符合某个特定的接口(也就是没必要表明它的is-a关系)。

创建这样一个NSObject的类别,我们称之为“创建了一个非正式协议”,它和“正式协议”相对应,正式协议相当于Java的接口,它明确的表明了它的“is-a”关系。而“非正式协议”看起来更轻量,但是使用时需要结合“响应选择器”,我们继续看一个示例:

shape.h添加NSObject的类别:

circle.m里面实现其中的一个方法changeColor:

main.m里面尝试调用这两个方法:

我们期望的结果是changeColor方法能调用成功,而recoverColor没有实现,我们期望它检查失败且打印失败的日志,实际打印结果符合预期:

 

4.6、正式协议

在前面一节中提到了“非正式协议”,我们提到了它与“正式协议”不同,正式协议需要显示的采用,也就是需要明确的在类的@interface声明中列出协议的名称,同时需要实现该协议的所有方法,否则编译器会生成警告来提醒你。

你可能意识到了正式协议相对于非正式协议的一个优势是:既然正式协议要求声明者承诺实现该协议定义的所有方法,那么你也就不用在访问这个对象实例的方法时使用selector响应选择器来询问对应的方法是否已经实现。也就是“正式协议”它是is-a关系,我们可以放心的调用它对应的方法了。

例如我们看一个NSCopying的正式协议定义:

声明协议和声明类很像,不过这里不是@interface,而是@protocol,@protocol 后面跟的是协议名称,协议名称必须要唯一。同java的接口一样,oc的protocol也可以继承自其它的协议,格式如下:

父协议的名称位于协议名称后面的尖括号内,和java的接口一样,父协议可以是多个的,多个父协议名称之间通过逗号“,”分割,使用如下:

4.6.1、正式协议示例

接下来我们在示例中实现上面的协议,我们将此前的Circle类实现这个协议,我们修改circle.h 让它遵循NSCopying协议,代码如下:

NSCopying协议中只定义了一个方法:- (id)copyWithZone:(NSZone *)zone; 我们在示例中需要实现这个方法,先看看不实现会怎么样?编辑器给了我们一个警告:

 

我们接下来实现这个方法:

我们描述一下这个代码,首先我们看下allowWithZone 的定义:

这个方法申明是以”+”开头,因此它是一个类方法,所以我们代码中是给[self class]发送的allocWithZone消息,通过这个allocWithZone方法我们可以得到一个这个类的实例,接下来我们我们用当前调用的实例的实例变量来初始化这个复制出来的实例对象。

最后我们在main.m 调用一个已经初始化的对象实例的copy,并调用copy出来的对象实例的draw方法,代码如下:

最红运行输出如下:

我们看到打印中有两个draw的打印,其中第二个打印的是复制后的对象,这个对象基本所有值都是从第一个对象复制过来,除了name我们设置了另一个值”copy item name”,用于区分复制的对象和被复制对象之间的差异。

 

4.6.2、正式协议与委托

举个例子:

这个方法是用来设置委托的,参数的类型告诉我们,这个委托对象需要遵守NSNetServiceBrowserDelegate正式协议。如果我们调用setDelegate时传了不遵守该协议的对象时会出现什么,我们试验一下,可以看到编辑器给出了一个警告:

 

5、内存管理

前面第4节我们讲了对象的概念,但是怎么知道一个对象的生命周期结束了呢?Cocoa采用了一种叫做引用计数的技术,也叫保留计数(retain counting)。每个对象都有一个与之关联的整数,被称作它的引用计数器或者保留计数器。

在OC编码中我们使用alloc, new或者copy方法创建一个对象时,对象的保留计数器的值被设置为1。如果要增加对象的保留计数器的值,可以给对象发送一条retain消息,相反的,要减少的话,可以给对象发送一条release消息。当一个对象保留计数器归0时即将被销毁,OC会自动向对象发送一条dealloc消息。(注意一定不要直接调用dealloc方法)

我们之前的代码没有考虑内存的管理,我们这里按照OC中的内存管理约定,对main.m new的对象做一些调整,看下会发生什么?

我们对前面章节示例中的copy出来的对象发送一条release消息,并在此操作前打印retainCount, 同时我们在circle.m中覆写dealloc方法,实现如下:

通过这节的知识我们知道,当我们使用copy得到一个对象实例shape2时,该对象的保留计数器的值为1,此时我们调用该对象的release,它的引用计数为0,同时会触发该对象的dealloc方法,最终打印和我们理解的一致:

 

5.1、自动释放

截止目前内存的释放操作和C/C++很像,都需要自己手动管理,我们知道内存管理在C/C++中并非一件容易的事情。Cocoa提供了一个自动释放池(autorelease pool)的概念——@autoreleasepool或者NSAutoreleasePool

我们来理解一下自动释放池的工作原理,包含以下四点:

  • 当使用@autoreleasepool{}时,所有花括号里定义的变量在括号外都无法使用了。这就像是典型的C语言中的有效范围,比如for循环代码
  • 当使用NSAutoreleasePool时,在创建(new)和释放(release)之间的代码会使用这个新的池子
  • 通过向对象发送autorelease消息,将该对象添加到自动释放池中,此时该对象的引用计数器的值不会改变(例如copy出来的对象,调用autorelease消息,它的引用计数器的值仍然是1)
  • 当自动释放池范围之外或者NSAutoreleasePool对象收到release消息时,会向该池子中的所有对象发送一条release消息

通过以上四点,我们知道autoreleasepool帮助我们对一个代码块范围内的主动通过autorelease消息添加到自动释放池中的对象,在自动释放池生命周期结束后会自动收到release消息,完成对象的释放。也就是帮助我们创建对象后省去了释放对象操作的工具,避免我们忘记release释放,或者释放的时机不对等问题。但同时也告诉我们使用自动释放池需要注意的点,例如我们构建了对象调用了autorelease消息,但是我们又主动给该对象retain增加保留计数器的值,那么这个属于自己干涉了内存管理,需要自己额外去释放因这次retain增加的保留计数器的值

我们可以继续写一个示例来验证自动释放和手动在自动释放池干涉后的行为。我们继续在main.m中修改,通过copy得到两个对象,两个都添加到自动释放池,并对其中一个对象额外调用retain方法,最终打印两个的retainCount,看引用计数器的情况是否符合我们的预期:

我们修改main.m的代码如下,通过NSAutoreleasePool new和release的方式来创建和销毁一个自动释放池。在这个new和release之间我们通过copy创建了两个对象的实例,并通过autorelease将这些实例添加到自动释放池内,此时两个对象的retainCount都应该为1。我们又额外对shape3 调用了retain,那么自动释放池内shape3的保留计数器的值应该增加为2。当自动释放池release后,自动释放池里面的对象不会全部释放,虽然我们显示的通过autorelease的消息将对象添加到池子里,但是我们也显示的给shape3 retain增加了保留计数了。所以自动释放池只释放了自己持有的那一份,那么自动释放池外再次打印两个对象的保留计数器的值时,shape2被释放了无法打印,shape3打印的值为1(已经减少了1)

输出结果如下,和我们期望的是一致的:

Cocoa提供的自动释放池非常有用,我们需要理解它的原理,这样用起来就得心应手了。不过Cocoa不仅仅是提供了这样的自动释放池语法糖就结束了,它还为我们提供了编译器级别的工具来辅助我们编写代码,真可谓做事做全套,这样才够完美。

我们接下来看下ARC是啥?

 

5.2、ARC

ARC其实并没有引入新的概念,它只是在编译时帮助我们在代码中的合适位置自动插入了retain和release的代码的工作是不是很容易理解,我们来看下如何使用这个功能。

XCode提供了一个简单的步骤帮助我们把已有的项目转成支持ARC,操作步骤如下:

1、选定工程找到菜单中的转换选项

2、选中要转换的文件点击”check”检查

我们全选所有文件

3、点击check后,系统检测出需要修改的代码

4、逐个修改代码,满足ARC的要求

我们把自动释放池相关的代码都删除,上面示例中两个shape copy的代码变为如下:

我们再次点击convert

5、检查成功,并提示即将进行转换步骤

我们点击下一步

6、系统自动帮助我们做了以下3个改动

  • 移除父类的dealloc调用
  • 移除了[shape3 retain]
  • 将shape类的两个属性增加了weak特性

具体如下:

7、点击”save”保存

提示这个改动是不可逆的,我们点击继续

8、工程ARC已经开启

此时我们再去看工程的 ARC开启状态已经是Yes

 

至此工程已经被转换成支持ARC,你代码中关于自动释放池和retain相关的代码都不见了,就仿佛和java一样不需要手动调用了。我们此时再在代码里面添加release消息调用,xcode就会给我们错误警告了:

此时我们再运行我们的程序,我们发现输出中很多我们之前没有管理好的内存都正常触发释放了,其中包含我们两个copy的对象,这太令人振奋了:

 

5.3、弱引用、归零弱引用

前面的内容提到过,当用指针指向某个对象时,你可以管理它的内存(通过retain和release),也可以不用管理(是用ARC)。如果你管理了,就拥有了对这个对象的强引用,如果你没有管理,那么你拥有的就是弱引用,弱引用在OC里面就是对属性使用了assign特性。

当你是用assign持有一个对象的引用,保留计数器的值不会增加,如果这个对象的另一个强引用的拥有者释放它时,它的保留计数器就会变为0。这个很好的解决了我们经常提到的循环引用的问题,避免两个对象彼此强引用,保留计数器的值都不会为0,而产生的内存泄露。

这个是默认的弱引用的概念,然后实际使用中弱引用的对象被释放了,但是代码仍然指向了这个被释放对象的弱引用,这个引用已经失效了,此时使用它会导致问题,于是OC有另一种——”归零弱引用”,在其它指向它的强引用被释放时,这个归零弱引用就会被设置为0(即为nil)。我们可以通过下面两种方式申明它:

或者property属性中添加weak:

 

6、异常

异常就是如同Java的Exception,当出现意外时间,例如数组溢出等,程序可以创建一个异常对象(Cocoa中使用NSException类来表示异常),你的代码逻辑可以捕获这个异常并处理。

Cocoa要求所有的异常必须是NSException类型的异常,你也可以创建NSException的子类来作为你自己的异常。

当然生成这些异常不是目的,目的是希望程序能处理这个异常,因为Cocoa框架处理异常的方式通常是退出程序,而我们希望捕获这个异常并做出对应的处理。

如果想要支持这个特性,我们需要确保Xcode需要启用 Enable Objective-C Exceptions项,参考下面截图中的配置项为 Yes

 

6.1、异常关键字

异常所有的关键字都是以@开头,以下列举了这几个关键字:

  • @try: 定义用来测试的代码块以决定是否需要抛出异常
  • @catch(): 定义用例处理已抛出异常的代码块。接收一个参数,通常是NSException类型,但也可能是其它类型
  • @finally: 同java的finally,定义无论是否有抛出异常都会执行的代码块,这段代码总是会执行的
  • @throw: 抛出异常

我们参考Java的写法使用这些关键字写一个捕获和处理异常的例子:

上面代码中我们模拟了一个数组越界,并添加了@catch和@finally,我们期望是这两个关键字对应的代码块都会被执行,最后输出如下:

 

我们接下来再模拟一个抛出异常的例子,也同样修改main.m方法:

此时输出如下:

 

7、代码块

7.1、代码块的概念

代码块是对C语言中函数的扩展,它包含函数中的代码和代码块外的变量绑定(类似Lua 里面的闭包和它的上值),代码块有时也被称为闭包(closure)。

代码块在Xcode的GCC和Clang工具中是有效的,但它不属于ANSI的C语言标准。

7.2、代码块的使用

知道C语言的都知道如何申明一个函数指针:

在OC中只要把*符号替换为^幂符号就可以把它转换成一个代码块的定义了。在OC中声明代码块变量和代码块实现的开头位置都要使用这个^幂操作符

我们来写一个简单的示例:

输出如下:

OC中代码块的定义格式如下:

<returntype> (^blockname) (list of arguments) = ^(arguments) { body; };

7.3 代码块中的上值

上值是Lua里的概念,在代码块中也有基本相同的特性。代码块诶声明后会捕捉创建点时的转态,代码块可以访问函数用到的标准类型的变量:

  • 全局变量,包括在封闭范围内声明的本地静态变量
  • 全局函数
  • 封闭范围内的参数
  • 函数级别(即与代码块声明时相同的级别)的__block变量。它们是可以修改的变量
  • 封闭范围内的非静态变量会被获取为常量
  • Objective-C的实例变量
  • 代码块内部的本地变量

我继续写一个示例:

输出为:

我们看到封闭范围内的非静态变量my_local_value初始值为123,定义代码块my_block时捕获的是这个初始值,在代码块定义之后我们修改了这个my_local_value值为456,但是此时不影响代码块内部捕获的123的值,所以我们接下来的2个打印,代码块内的打印为123,代码块外的打印为456。

我们修改一下my_local_value为static,此时这个变量就变为全局的了,此时代码块定义之后修改了这个值,代码块内部的打印时的捕获的值会是什么呢?

代码块修改:

输出:

我们看到修改为静态全局变量后,block捕获的值在定义之后如果被修改也会跟着改变

 

7.3.1、__block变量

上面讲到本地变量会被代码块作为常量获取到,如果你想修改它们的值,那么必须将它们声明为可修改的,我们看一个示例:

我们尝试修改代码块外的本地变量,Xcode会提示一个错误,告诉我们这个本地变量不能被赋值,需要添加__block 修饰符

我们给这个本地变量my_local_value添加__block修饰符,改动如下:

输出结果如下,这个本地变量值在代码块内被修改成了789了:

 

8、并发性

利用并发性最基础的方法是使用POSIX线程,POSIX线程是级别较低的API,必须得手动管理,根据硬件和其它软件运行环境需要创建的线程数量会发生变化。使用这个底层API需要很多技巧,一旦遇到问题会比较麻烦。

比较幸运的是苹果公司为了减轻在多核变成的负担,引入了Grand Central Dispatch,也就是CCD。它减少了线程管理的麻烦,它是一个系统级别的技术。

8.1、@synchronized和属性关键字nonatomic

OC提供了一个语言级别关键字@synchronized,这个关键字拥有一个参数,通常这个对象是可以修改的。这个和Java的synchronized的关键字很像。

示例如下:

它能确保不同的线程连续的访问临界区的代码。

在OC的属性中默认自动处理了@synchronized 访问临界区。我们定义属性时使用了atomic关键字时,编译器会设置使用@synchronized来确保getter和setter彼此互斥,这样会对代码和变量产生一些损耗,我们可以指定nonatomic来做为属性的特性,去除这个互斥,但前提是我们知道这些属性不会被多个线程访问。

 

8.2、一些简单的后台执行

NSObject提供了一些简单的方法帮助我们将代码放在后台执行,这些方法都有performSelector:的类似前缀,例如performSelectorInBackground:withObject:,它能在后台执行一个方法,它通过创建一个线程来运行方法。是不是很方便,不过也要记住定义这些方法需要遵从以下两个限制:

  • 由于这些方法运行在各自的线程里,因此你必须为这些方法里面的对象创建一个独立的自动释放池,因为主自动释放池是与主线程相关的

例如:

调用代码:

输出:

  • 这些方法不能有返回值,并且要么没有参数,要么只有固定的一个参数对象,也就是格式是固定的

我们可以看到NSObject里面提供了3个这样的格式定义:

这些简单的代码在执行结束后,OC运行时会特地清理并弃掉这些线程,也不会通知你,不过已经很轻量好用了。我们再来做一些复杂的事情。

 

8.3、调度队列

GCD可以使用调度队列,你只需写下你的代码,把它指派为一个队列,系统就会执行它了。你可以同步或异步执行任意代码,一共有以下3种类型的队列:

  • 连续队列:每个连续队列都会根据指派的顺序执行任务。你可以按自己的想法创建任意数量的队列,队列之间会并行操作任务
  • 并发队列:每个并发队列都能并发执行一个或多个任务。任务会根据指派到队列的顺序开始执行。
  • 主队列:应用程序的主线程任务队列

8.3.1、连续队列

有时我们有一连串的任务需要按照一定的顺序执行,这个时候可以使用连续队列。任务执行顺序为先入先出(FIFO)。任务可以是按异步提交的,但是顺序队列会确保任务按照预订顺序执行。这种队列是不会发生死锁的。

代码示例:

输出:

8.3.2、并发队列

并发调度队列适用于那些可以并行运行的任务。并发队列也遵循先入先出(FIFO)的规范,不过任务可以前前一个任务结束前就开始执行,这个是它和顺序队列的差异。我们需要知道的是一次所运行的任务数量是无法预测的。它会根据其它运行的任务在不同时间变化。所以你每次运行同一个程序,并发任务数量可能会是不一样的。

说明:如果需要确保每次运行的任务数量都是一样的,可以通过线程API来手动管理线程。

每个应用程序都有3种并发队列可以使用:

  • 高优先级(high)
  • 默认优先级(default)
  • 低优先级(low)

使用它们的方式是调用dispatch_get_global_queue方法,示例如下:

输出:

我们可以看到它们输出的顺序不是固定的。

 

8.3.3、主队列

可以通过dispatch_get_main_queue();来找出当前主线程队列

 

9、其它特性介绍

9.1、预处理器指令”#import”和”#include”的区别

#import是由Xcode使用的编译器提供的,Xcode在你编译Objective-C, C和C++程序时都会使用它。 #import可保证头文件只被包含一次,无论此命令在该文件中出现多少次。

在C语言中,程序员通常使用基于ifdef命令的方案来避免一个文件包含另一个文件而后者又包含前者的情况。而在Objective-C 中,程序员使用#import命令来实现这个功能

9.2、@class

@class 关键字可以解除循环依赖的问题,例如你有一个class A 和 class B,如下:

以上代码是无法通过编译的,因为A依赖了B,且B也依赖了A。

如果我们用@class来调整这个代码,可以改为如下:

@class 告诉编译器这个class在其它地方已经定义,因此这里用指针来指向它是合法的。但是此时你不能用该class类型的指针来访问里面的方法,因为编译器无法获得更多关于该class的信息。

9.3、super & self

self

当我们实现class的时候,我们往往会定义很多实例方法,每个实例方法都获得了一个名为self的隐藏参数,它是一个指向接收消息的对象的指针。这些方法通过self参数来寻找它们需要用到的实例变量和其它的实例方法。示例如下:

super

Objective-C 提供了一种方法,让你即可以重写方法的实现,又能调用超类中的实现方式。当需要超类实现自身的功能,同时在之前或之后执行某些额外的工作时,这种机制非常有用。为了调用继承的方法在父类的实现,需要使用super作为方法调用的目标。示例如下:

super来自哪里呢?它既不是参数也不是实例变量,而是有Objective-C 向该类的超类发送消息。如果超类中没有定义该消息,Objective-C 会和平常一样继续在继承链上一级中查找。

 

赞(1) 打赏
未经允许不得转载:花花鞋 » Objective-C学习笔记

评论 抢沙发

国内精品Android技术社区

专注android技术,聚焦行业精粹,传扬中国传统文化,我们一直在努力

联系我们

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏

登录

找回密码

注册