我的观点是,人与人的讨论是需要有某些双方一致认同的基础的,否则,最后也难以得出一个一致认同的结果,自说自话毫无意义。所以我觉得需要把很多概念弄清楚,不要混用。平时在网络上和工作中,与各个工种的开发人员进行交流的时候,我发现过一个问题,很多人对面对对象的一些基础概念含糊不清,甚至一无所知。具体到iOS开发领域,常见的开发人员也只是在盲目的堆砌业务功能,而不去考虑这些代码在基础层面的含义。当然基础具体到哪一层次,我只能尽力。
Runtime 前置知识
- Runtime的版本有很多
- 早期的runtime版本不能原生地支持对某个类新增变量
- Mac OS X 10.6 之后的Runtime被称为
modern runtime
modern runtime
实现了原生支持动态增加属性,使用到了称之为Associated References
的技术,本文讲述的是modern runtime
的相关知识
Objective-C 是一门动态语言
动态语言是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。C、C++等语言则不属于动态语言。Objective-C 的动态特性是由其Runtime环境来实现的
动态性的表现
C++里类别与方法的关系严格清楚,一个方法必定属于一个类别,而且在编译时(compile time)就已经紧密绑定,不可能调用一个不存在类别里的方法。
在Objective-C,类别与消息的关系比较松散,调用方法视为对对象发送消息,所有方法都被视为对消息的回应。
所有消息处理直到运行时(runtime)才会动态决定,并交由类别自行决定如何处理收到的消息。
也就是说,一个类别不保证一定会回应收到的消息,如果类别收到了一个无法处理的消息,程序只会抛出异常,不会出错或崩溃。
前面的两篇文章讲到了Objective-C
类的结构,现在将对结构展开讲述其内部,会首先讲到平时开发中常用的实例、类的常用方法,然后再深入到runtime层面去理解这些方法。
所以,接下来会讲 Objective-C
基本代码的常见表达,然后再去深入到runtime层面去理解你平时常写的这些代码。
当然常用的代码的讨论需要建立一些基础的概念,后面会一一提到。
实例变量和属性的关系
实例变量
instance variables
,也就是常见的ivars
,是对类创建的时候用以描述其实例的组成中的数据的封装。是面对对象的基础概念之一。
每一个类某一个实例变量是唯一的,不能重复
实例变量只有类和其子类的实例可以访问
属性和实力变量并没有唯一确定的关系,属性算是一种访问类的内部信息的一个接口 属性可以关联到实例变量,用来访问类内部的实例变量
默认情况下,编译器会为你定义的每一个属性自动生成一个实例变量和配套的setter、getter方法。
实例变量的细节
前面的两篇文章已经讲述过,Objective-C的对象,类,类对象,元类都是由结构体实现的
拿C语言进行举例,前面提到过C语言结构体成员的两种访问方式,其实还有另外一种方式,就是得到结构体变量在内存中的首地址,根据每个成员的长度根据偏移量来访问其任何一个成员变量。
事实上这种方式对C语言系的编程语言都是通用的。
但是Objective-C 融和 runtime 使用一些巧妙的办法,使得程序再运行期间去确定偏移量,这就是动态性的表现之一。
再看一看 OC2.0以前 的实例变量的写法
1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>
@interface MTTStudent : NSObject {
@public
NSString *_name;
NSUInteger _age;
CGFloat _height;
}
@end
采用这种方式使得MTTStudent
的某个实例的内存布局在编译时期就已经固定了,碰到要访问_name
变量的代码,编译器就会把访问代码转换会偏移量,表示该变量距离MTTStudent
的实例的存储地址有多远,通过操作指针中地址来访问这块存储了_name
的内存。
这个偏移量是一个硬编码,编译时候用某种长度的整数记录下来。
可是,如果你要插入了一个新的实例变量
1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>
@interface MTTStudent : NSObject {
@public
NSDate *_birthDay;//新增一个实例变量
NSString *_name;
NSUInteger _age;
CGFloat _height;
}
@end
新增并插入的的实例变量_birthDay
,如以上代码所示,这个如果继续是访问_name
的时候使用原来的偏移量将得到错误的地址,重新编译才能获得新的内存布局,使用心得偏移量
Objective-C的做法是,把实例变量当作 一种存储偏移量的“特殊变量”,由类对象来保存,可以调用runtime的借口获取到具体的偏移量。简单来讲就是在运行时分配内存确定偏移量,并且这个偏移量和实例变量绑定起来,后面讲runtime的接口会看到。
偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也会变,能保证无论何时访问实例变量,都能使用正确的偏移量。
属性的细节
你需要时刻记住面对对象的特性之一:封装,实例变量是对数据的封装,属性是对实例变量的封装,每一层的封装都隐藏了一些融合的细节在其下一层。
属性最终是采用实例变量来实现的,可以把属性理解为一种简称:编译器会自动写出一套存取方法用以访问类中的实例变量。
在类的实现代码中使用
@synthesize
关键字来指定实例变量的名字在类的实现代码中使用@dynamic关键字来告诉编译器不要自动生成setter和getter方法,有时候你想使用懒加载 改写属性的setter和getter方法是IDE会提示你有错误,只能重写两者之一,可以同过@dynamic关键字来解决这一问题
属性的特质
所谓属性的特质,是指对属性设置某些修饰,这样编译器在自动实例变量和其存取方法时会考虑这些修饰来生成
属性的特质分为四类:原子性、读写控制、内存管理、方法名
以下的例子是一个具备四类修饰的属性的声明
1
@property (nonatomic,readwrite,strong,setter=setName:)NSString * name;
原子性,缺省为
atomic
,iOS平台上的代码一般都要指定nonatomic
来提升性能- 读写权限,缺省为
readwrite
,编译器会自动生成读写方法,指定readonly
时要配合@synthesize
编译器才会生成getter方法。 - 内存管理语义,有五种
assign
、strong
、weak
、unsafe_unretained
、copy
- 方法名字,getter=
<name>
,setter=<name>
关于内存管理语义的细节后面会专门开文章写一写,这些修饰关系到指针的操作,使用不当会导致很多莫名其妙的bug
参考:
- 《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》
- 维基百科