前言
前段时间把Effective Objective-C 这本书慢慢的看了下,记录每章一些需要注意的知识点。说实话,这本书籍写的真的是经典,我感觉过段时间还得细读一遍。
目前对于每章的记录,有点类似流水账,就是粗略的记录知识点,方便快速浏览,希望过段时间可以慢慢思考每章每个知识点。
正文
这里记录下在读这本书一些有趣的东西,可以在平时编码的时候注意
利用@class 向前声明 来解决两个类相互引用的问题
A 类中有B 类的属性,B 类中也有A 类的属性;在这种情况下,我们用import、include 都是不能解决的,用@class 就可以完美解决这个问题,而且用@class 还可以减少对其他类的依赖,减少链接其他类所需要的时间,从而降低编译时间
读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做
通过属性来访问其实就调用set/get 方法,用实例变量来访问其实就是利用指针直接操作,这样子不用经过Objective-C 的 “方法派发” 步骤,这样子速度会比较快= =
Cocoa 中大部分collection 类都是类族
类族使用 “工厂模式”,调用抽象基类方法返回子类实例,这样子可以隐藏子类实现的细节,但是要注意,我们这样子用基类生成的实例,很有可能并不是基类的实例而是子类的实例,这个时候我们要弄清楚这两个方法的区别
-(BOOL) isKindOfClass: classObj; 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: classObj; 判断是否是这个类的实例
关于 “协议” 可选方法的判断
在实现委托模式和数据源模式的时,协议中的方法是可选的,我们就会写出大量这种判断代码:
if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){
[_delegate networkFetcher:self didRecevieData:data];
}
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。
struct data {
unsigned int filedA : 8;
unsigned int filedB : 4;
unsigned int filedC : 2;
unsigned int filedD : 1;
}
filedA 位段占用8个二进制位,filedB 位段占用4个二进制位,filedC 位段占用2个二进制位,filedD位段占用1个二进制位。filedA 就可以表示0至255之间的值,而filedD 则可以表示0或1这两个值。
我们可以像filedD 这样子,创建大小只有1的位段,这样子就可以把Boolean 值塞入这一小块数据里面,这里很适合这样子做。
@interface EOCNetworkingFetcher ()
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags
@end
//使用
//set flag
_delageteFlags.didReceiveData = 1;
//check flag
if(_delageteFlags.didReceiveData){
//YES
}
可以在delegate 属性的设置方法里面写实现缓存功能所用的代码。
这样子,每次调用delegate 的相关方法之前,就不用检测委托对象是否能响应给定的选择子了,而是直接查询结构体里面的标志。
在相关方法需要调用很多次时,就要思考是否有必要进行优化,分析代码性能,找出瓶颈,使用这个位段这个技术可以提供执行速度。
使用atomic 特质来修饰属性,来保证其原子性 但是不代表是线程安全的
属性是开发者经常需要同步的地方,可以使用atomic 特质来修饰属性,来保证其原子性,每次肯定可以从中获取到有效值,然而在同一个线程上多次调用获取方法(getter),每次获取到结果未必相同,在两次访问操作之间,其他线程可能会写入新的属性值。
使用 “串行同步队列”,将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。
对于NSTimer 设置tolerance 这个偏差时间 可以提高性能
最后
这次看完,感觉对很多知识点都了解的不深,很多原理性的东西完全不懂,还是得多加把劲呀。
最后,还是推荐大家去看原著 少数优秀的Objective-C 书籍之一 = =
努力,奋斗!
前言
最经买了本编写高质量代码 改善Objective-C程序的61个建议,拿到手看了下目录感觉内容比这本52个有效方法更深点,之前的这本也是浅浅的看过,具体讲什么也不是很记得了,所以打算先重新看下这本52个有效方法,然后再来拜读新入手的这本。
这里准备记录下Effective Objective-C 2.0 编写高质量iOS与OS X 代码的52个有效方法这本提到的知识点。
第 1 条:了解Objective-C 语言的起源
1. Objective-C 在C 语言基础上添加了面向对象特性。
关于面向过程、面向对象的区别大概是:面向对象是将事物高度抽象化, 面向过程是一种自顶向下的编程。
这个问题没有固定的答案,每个人回答的思路都是不一样的,这里可以看下逼乎上面的回答。(PS:是在下经验尚浅,不知如何回答)
“面向对象"和"面向过程"到底有什么区别?
2. 使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。(Objective-C 利用运行时系统(Runtime )来做到消息传递,也叫做动态绑定 )
3. Objective-C 的重要工作都由 “运行期组件”(runtime component)而非编译器来完成,运行期组件本质上就是一种与开发者所编代码相链接的 “动态库”(dynamic libary),其代码能把开发者编写的所有程序粘合起来。
关于静态库跟动态库的区别在于:静态库在编译的时候直接拷贝一份到应用程序的,会使得程序变大;动态库是在运行的时候加载到内存,程序会链接到动态库,不会使得程序变大,动态库相当于共享库,多个应用程序之间可以共享。
关于静态库、动态库的知识点以及制作:iOS 静态库和动态库的基本介绍和使用、iOS 静态库,动态库与 Framework 浅析、组件化-动态库实战
4. Objective-C 是C 的 “超集”,所以C 语言中的所有功能在编写Objective-C 代码时依然适用。
超集的意思大概就像爸爸跟儿子的区别:S1 就是 S2 的超集,S2 有的 S1 都有。
5. C 语言的内存模型(memory medel ),对象所占的内存总是分配在 “堆空间”(heap space)中,而绝不会分配在 “栈”(stack)上,不能在栈上面分配Objective-C 对象。
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理,Objective-C 将堆内存管理抽象出来了,不需要用malloc 及free 来分配或释放对象所占内存,Objective-C 运行期环境把这部分工作抽象成一套内存管理架构,叫 ”引用计数“ 。
C语言内存模型及运行时内存布局
6. 对于创建结构体相比,创建对象需要额外的开销,例如分配及释放堆内存等操作,所以Objective-C 对于 ”非对象类型“ 通常都是适用结构体来存储,储存在栈空间。
第 2 条:在类的头文件中尽量少引用其他头文件
1. Objective-C 标准编写类方式也是头文件、实现文件组成
2. 场景:A 类的头文件中有一个B 类型的属性
@property (nonatomic,strong) B *b;
要通过编译,处理方式有3种:使用#import #incudule @class关键字
3. 两个类互相引用的问题: A 类中有B 类的属性,B 类中也有A 类的属性
4. 所以应该将引入头文件的时机尽量延后,只有确有需要的时候才引用,这样子可以减少类的使用者所需引用的头文件数量。
除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样子可以尽量降低类之间的耦合(coupling)。
有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把 “该类遵循某协议” 的这样声明移至 “class-continuation 分类中” 中。如果不行的话,就把协议单独放在一个头文件,然后将其引入。
“class-continuation 分类”,其实就是一个特殊的分类,写在实现文件中的分类,只能被该实现文件所引用
第 3 条:多用字面量语法,少用与之等价的方法
1. 使用字面量语法可以缩减源代码长度,使其更加易读,减少代码出错机率。字面量语法实际是一种 “语法糖”,也称 “糖衣语法”,是指计算机语言中与另外一套语法等效但是开发者用起来却更加方便的语法。
2. 字面数值
NSNumber *someNumner = @1;
NSNumber *intNumner = @1;
NSNumber *floatNumner = @2.5f;
NSNumber *doubleNumner = @3.14159;
NSNumber *charNumner = @'s';
3. 字面量数组
NSArray *array = @[@"a",@"b"@"c"];
NSString *string = array[0];
4. 字面量字典
NSDictionary *dict = @{@"key":@"value"};
NSString *string = dict[@"key"];
5. 可变数组与字典
NSMutableArray *mutable = [@[@"a",@"b"] mutableCopy];
6. 局限性
字面量所创建的对象必须属于Foundation 框架,如果自定义这些类的子类,则无法用字面量语法创建其对象。
7. 字符串字面量创建的是常量,对象不在持有了也不会立马被释放
例子:
__strong NSObject *yourObject= [NSObject new];
__weak NSObject *myObject = yourObject;
yourObject = nil;
__unsafe_unretained NSObject *theirObject = myObject;
NSLog(@"%p %@", yourObject, yourObject);
NSLog(@"%p %@", myObject, myObject);
NSLog(@"%p %@", theirObject, theirObject);
2017-02-16 11:02:37.702543 TKApp[1767:599122] 0x0 (null)
2017-02-16 11:02:38.612380 TKApp[1767:599122] 0x0 (null)
2017-02-16 11:02:40.985613 TKApp[1767:599122] 0x0 (null)
__strong NSString *yourString = @"Your String";
__weak NSString *myString = yourString;
yourString = nil;
__unsafe_unretained NSString *theirString = myString;
NSLog(@"%p %@", yourString, yourString);
NSLog(@"%p %@", myString, myString);
NSLog(@"%p %@", theirString, theirString);
2017-02-16 11:00:42.407410 TKApp[1757:597837] 0x0 (null)
2017-02-16 11:00:44.340836 TKApp[1757:597837] 0x1013b9480 Your String
2017-02-16 11:00:45.392346 TKApp[1757:597837] 0x1013b9480 Your String
这里主要有2个知识点:
1.关于ARC中的引用计数问题
2.字符串常量和字符串字面量的区别是什么?
Line By Line
第一种情况:
__strong NSObject *yourObject = [NSObject new];
yourObject New了一个NSObject对象 并且持有 对象引用计数+1
__weak NSObject *myObject = yourObject;
myObject 指向 yourObject指向的的对象地址 没有持有 对象引用计数 不变
yourObject = nil;
yourObject 指向nil 不持有NSObject对象 对象不被持用 引用计数-1 这个时候这个对象自动释放
__unsafe_unretained NSObject *theirObject = myObject;
这个时候myObject已经被置为nil了 所以theirObject也为nil
第二种情况:
本来第二种情况也应该类似像第一种,这里就是关于字符串常量和字符串字面量的区别了。
What's the difference between a string constant and a string literal?
在这里为什么没有释放的情况跟字符串常量没什么联系,主要是这里是一个字符串字面量,字符串字面值创建了不会再修改了,一个对象持有这个字符串,当它不指向它了,也不会立马释放。
这里还有个点,Objective-C 会做字符串的编译单元,而且会合并相同字符串的编译单元,来减少额外的消耗去链接这些编译单元。
NSString *string1 = @“pengxuyuan”;
NSString *string2 = @“pengxuyuan”;
string1跟string2内存地址是一样的。
参考资料:
应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
应该通过取下标操作来访问数组下标或字典中的健所对应的元素。
用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
第 4 条:多用类型常量,少用#define 预处理指令
1. 在编码时候多次用到一个变量(数值,字符串等),我们会进行抽取以便修改一处所有用到的地方都会生效。
我们可能会使用#define 预处理指令#define ANIMATION_DURATION 0.3 编译的时候会将遇到的ANIMATION_DURATION 替换成0.3,这样子可以解决问题,但是会存在一些问题:
这个时候我们定义一个常量的话,就可以包含类型信息static const NSTimerInterval kAnimationDuration = 0.3; 这样子在编译的过程中就可以清楚的知道要替换的类型,如果不一致会报警告,这样子也方便排查问题;常用的命名法是:若常量局限于 “编译单元”(translation-unit,也就是 “实现文件” 中),则在前面加字母k;若常量在类之外可见,则通常已类名作为前缀。
2. 定义常量的位置很重要。
如果将#define ANIMATION_DURATION 0.3 static const NSTimerInterval kAnimationDuration = 0.3; 定义在头文件,引入了这个头文件都会有这个名字,而且 static const NSTimerInterval kAnimationDuration = 0.3; 定义在头文件的话,等于会声明一个全局变量,这样子所有类都可以使用了,这样子我们应该用类型作为前缀。
3. static 修饰符则意味该变量仅在此变量的编译单元可见。编译器每收到一个编译单元,就会输出一份 “目标文件”(object file)。在Objective-C 的语境下,”编译单元“ 通常指每个类的实现文件(.m 文件),如果声明此变量不加static,则编译器会为它创建一个 “外部符号”(external symbol),如果其他编译单元也声明同样的变量就会报错了。
4.如果用static 和 const 声明一个变量,不会创建符号,而是会像#define 预处理指令一样,将遇到的变量全部替换,但是区别在这样子有变量类型。
5. 如果要定义一个外界可见的常量变量(constant variable),可以放在 “全局符号表”(global symbol table)中,来全局使用。
objective-c
//In the header file
extern NSString *const EOCStringConstant;
//In the implementtation file
NSString *const EOCStringConstant = @"VALUE"
编译器会在 “数据段”(data section)为字符串分配存储空间,这里在上面C 语言的内存模型有讲,数据段通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
在实现文件中使用static const 来定义 “只在编译单元内可见的常量“(translation-unitspecific constant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
在头文件中使用extern 来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类型做前缀
第 5 条:用枚举表示状态、选项、状态码
C++ 11 标准扩充了枚举的特性,最新系统框架使用了 “强类型”(strong type)的枚举。
实现枚举所用的数据类型取决于编译器,不过其二进制位(bit)的个数必须能完全表示下枚举编号才行,一个字节含8个二进制位,所以至多能表示256中(2^8^个)枚举(编号为0~255)的枚举变量。
只要枚举定义得对,各选项之间就可通过 “按位或操作符”(bitwise OR operator)来组合。
用宏来定义枚举类型,这些宏具备向后兼容(backward compatibility)能力,如果目标平台编译器支持新标准,那就使用新式语法,否则改用旧式语法。
objective-c
typedef NS_ENUM(NSUInterger,EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef NS_OPTINS (NSUInterger,EOCPermittedDirection) {
EOCPermittedDirectionUp = 1 << 0,
EOCPermittedDirectionDown = 1 << 1,
EOCPermittedDirectionLeft = 1 << 2,
EOCPermittedDirectionRight = 1 << 3,
}
5. 在switch 语句中,最好不要有default 分支,这样子要做到处理所有样式,这样子在新家类型的时候,没有default 编译器会发出警告,让我们注意到。
应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
用NS_ENUM 与 NS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现的,而不会才用编译器所选的类型。
在处理枚举类型的switch 语句中不要实现defauly 分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch 语句并未处理所有枚举。
第 6 条:理解 “属性” 这一概念
1. “对象”(object)就是 “基本构造单元”(building block),开发者可以通过对象来存储并传递数据,在对象直接传递数据并执行任务的过程就叫做 “消息传递”(Messaging)。
2. 如果对象布局在编译器就固定了,访问变量时,编译器会使用 “偏移量”(offset)来计算,这个偏移量是 “硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。 存在一个问题:如果代码使用了编译期计算出来的偏移量,那么修改类定义之后必须重新编译,否则就会出错。
Objective-C 处理方式是:把实例变量当作一种存储偏移量所用的 “特殊变量”(speacial variable),交由 “类对象”(class object)保管。偏移量会在运行期查找,这样子总能找到正确的偏移量,这就是稳固的 “应用程序二进制接口”(Application Binary Interface,ABI)。
3. 使用 “点语法” 的效果与直接调用存取方法相同,没有丝毫差别。
4. 属性有很多优势:
可以使用 “点语法”
编译器会自动编写访问这些属性所需的方法,此过程就做 “自动合成”(autosynthesis)
编译器还会自动向类添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字
在实现代码中可以通过@synthesize 语法来指定实例变量的名字
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
属性特质
1. 属性可以拥有的特质分类四类:原子性、读/写权限、内存管理语义、方法名
原子性(atomicity)
2. 属性默认情况下编译器所合成的方法会通过锁定机制确保其原子性,用nonatomic 特质,就不使用同步锁。
iOS 使用同步锁的开销较大,这会带来性能问题,一般情况下并不要求属性必须是 “原子的”,因为 “原子性” 并不能保证 “线程安全”(thread safety),若要实现 “线程安全” 的操作,还需采用更加深层的锁定机制才行。
3. 读/写权限
4. 内存管理语义
assign
strong
weak
unsafe_unretained
copy
5. 方法名
可以指定存取的方法名。
@property (nonatomic,getter = isOn) BOOL on;
可以用@property 语法来定义对象中所封装的数据。
通过 “特质” 来指定存储数据所需的正确语义
在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
开发iOS 程序时应该使用nonatomic 属性,因为atomic 属性会严重影响性能。
第 7 条:在对象内部尽量直接访问实例变量
1. 强烈建议:读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。
2. 关于直接访问跟通过属性访问的区别:
由于不经过Objective-C 的 “方法派发” 步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
直接访问实例变量时,不会调用其 ”设置方法“,这就绕过了为相关属性所定义的 ”内存管理语义“。比方说,如果在ARC 下直接访问一个声明为copy 的属性,那么并不会拷贝改属性,只会保留新值并释放旧值。
如果直接访问实例变量,那么不会触发 ”键值观测“(Key-Value Observing,KVO)通知。
通过属性来访问有助于排查与之相关的错误,因为可以给 ”获取方法“ 或 ”设置方法“ 中新增断点,进行调试。
3. 在初始化方法中总是应该直接访问实例变量,避免子类重写了设置方法(处理异常情况抛出异常)但是:如果待初始化的实例声明在超类中,而我们又无法在子类直接访问此实例变量的话,那么就需要调用 “设置方法” 了。
4. 在 “惰性初始化”(lazy initialization),必须通过 “获取方法” 来访问属性,不然实例变量永远不会初始化。
在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
在初始化方法及dealloc 方法中,总是应该直接通过实例变量来读写数据。
有时会使用惰性初始化技术配置某份数据,在这种情况下,需要通过属性来读取数据。
第 8 条:理解 “对象等同性” 这一概念
1. 使用 == 操作符比较的两个指针的本身,而不是其所指的对象;所以这里有可能会出轨,得不到我们想要的结果。NSObject 提供 “isEqual” 方法,某些对象也提供了特殊的 “等同性判定方法”
NSString *foo =@"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i",123];
BOOL equalA = (foo == bar); //equalA = NO
BOOL equalB = [foo isEqual:bar]; //equalB = YES
BOOL equalC = [foo isEqualToString:bar]; //equalC = YES
2. NSObject 协议中有两个用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
如果 “isEqual” 方法判定两个对象相等,那么其hash 方法也必须返回同一个值。但是,如果两个对象的hash 方法返回同一个值,那么 “isEqual” 方法未必会认为两者相等。
对于实现hash 方法需要一些技巧:
- (NSUInteger)hash {
return 1337;
}
//这样子是可以的,但是会对collection 使用这个对象产生性能问题。因为在collection 在检索哈希表的时,会用对象的哈希码来做索引,在set 集合中,会根据哈希码把对象分装到不同的数组里面,在添加新对象的时候,要根据其哈希码找对与之对应的数组,依次检查其中各个元素,看数组已有的对象是否和将要添加的新对象相等,如果相等,就说明添加的对象已经在set 集合中了,是添加失败的。(如果所有对象的hash 值对一样,这样子set 集合只会有一个数组,所有数据都在一起了,每次插入数据都会遍历这个数组,这样子就会出现性能问题)
- (NSUInteger)hash {
NSString *stringToHash = [NSString stringWithFormat@"%@:%@",_firstName,_lastNmae];
return [stringToHash hash];
}
//这样子能保证返回不同的哈希码,但是这里会存在创建字符串的开销,会比返回单一值要慢
- (NSUInteger)hash {
return [self.firstName hash] ^ [self.lastNmae hash];
}
//这样子可以保存较高的效率,又不会过于频繁的重复
3. 特定类所具有的等同性判定方法
1) isEqualToString、isEqualToArray、isEqualToDictionary
2) 如果需要经常判断等同性,可以自己创建等同性判断方法,这样子可以避免检测参数的类型,提升检测效率。
- (BOOL)isEqualToPerson:(EOCPerson *)otherPerson {
if (self == object) return YES;
if (![_firstName isEqualToString:otherPerson.face]) return NO;
if (![_lastName isEqualToString:otherPerson.head]) return NO;
return YES;
}
-(BOOL)isEqual:(id)object {
if ([self class] == [object class]){
return [self isEqualToPerson:(EOCPerson *)object];
}else{
return [super isEqual:object];
}
}
3) 等同性判定的执行深度
在我们只需要通过判断一个标识符就可以判断对象相等的时候,我们重写方法可以很方便的达到目的,比如判断一个idectifier 就能确定这两个对象相等,就不用判断那么多属性了。
5) 容器中可变类的等同性
把某个对象放入colloection 之后,不应该再去改变其哈希码了,不然会出现问题,在set 集合会导致改变之后对象存在在一个在原则上 “错误” 的位置。
若想检测对象的等同性,请提供 “isEqual:” 与 hash 方法。
相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
不要盲目地逐个检测每条属性,而是应该依照具体需求来指定检测方案。
编写hash 方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
第 9 条:以 ”类族模式“ 隐藏实现细节
“类族” 是一种很有用的模式,可以隐藏 “抽象基类” (abstract base class)背后的实现细节。
用户无须自己创建子类实例,只需要调用基类方法来创建即可。
创建类族
每个 “实体子类” 都从基类继承而来,“工厂模式” 是创建类族的办法之一,调用基类方法返回子类实例。
如果对象所属的类位于某个类族中,那么查询其类型信息要注意,你可能觉得自己创建了某个类的实例,然后实际上创建的却是其子类的实例。
-(BOOL) isKindOfClass: classObj; 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: classObj; 判断是否是这个类的实例
Cocoa 里的类族
1. 系统框架中有许多类族,大部分collection 类都是类族。
2. 对于Cocoa 中NSArray 这样子的类族,新增子类需要遵循几条规则:
若要编写NSArray 类族的子类,则需要其继承自不可变数组的基类或可变数组的基类。
编写NSArray 子类时,必须用一个实例变量来存放数组中的对象;NSArray 本身只是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需要的一些接口。
在每个抽象基类中,都有一些子类必须覆写的方法,编码前需要看下文档。
第 10 条:在既有类中使用关联对象存放自定义数据
1. 可以给类关联许多其他的对象,这些对象通过 “键” 来区分。
2. 储存对象值的时候,可以指明 ”存储策略“(storage policy),用以维护相应的 ”内存管理语义“,objc_AssociationPolicy 的枚举定义存储策略。
| 关联类型 | 等效的@property 属性 |
| :-------------------------------- | :--------------- |
| OBJC_ASSOCIATION_ASSIGN | assign |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
| OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
| OBJC_ASSOCIATION_RETAIN | retain |
| OBJC_ASSOCIATION_COPY | copy |
下列方法可以管理关联对象:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
此方法以给定的键和策略为某对象关联对象值
id objc_getAssociatedObject(id object, const void *key)
此方法根据给定的键从某对象中获取相应的对象值
void objc_removeAssociatedObjects(id object)
此方法移除指定对象的全部关联对象
设置关联对象用的键是不透明指针(opaque pointer),其指向的数据结构不局限于某种特定类型的指针。
设置关联对象值时,若想令两个键匹配到同一个值,则两者必须是完全相同的指针,所以在设置关联对象值时,通常使用静态全局变量做键。
第 11 条:理解 objc_msgSend 的作用
调用对象方法,在Objective-C 中叫做 “传递消息”(pass a message),消息有 “名称”(name)或“选择子”(selector),可以接受参数,而且可能还有返回值。
objc_megSend 的原型: void objc_msgSend(id self,SEL cmd,...) ,是一个 “参数个数可变的函数”,能够接受两个或两个以上的参数,第一个参数代表接收者,第二个参数代表选择子,后续参数就是参数。
objc_megSend 函数会依据接收者和选择子来调用适当的方法:
在接收者所属的类搜寻其 “方法列表”
找不到的话,就沿着继承体系继续向上查找
最终还是找不到相符的方法就执行 “消息转发”
4. objc_msgSend 会将匹配结果缓存在 “快速映射表”(fast map)里面,每个类都有这样子的一块缓存,接下来还向该类发送一样的消息,那么执行起来就很快了。
5. 这里有些特殊情况,需要由Objective-C 运行环境的另外一些函数来处理:
objc_msgSend_stret :如果待发送的消息要返回结构体,那么可以交由此函数处理。只有当CPU 寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU 寄存器(比如说返回的结构体太大了),那么就由另外一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
objc_msgSend_fpret:如果消息返回的是浮点数,可以交由此函数处理。这个函数是为了处理x86 等架构CPU 中某些令人惊讶的奇怪状况。
objc_msgSendSuper:如果要给超类发消息,那么就交由此函数处理。
6. 每个类里都有一张函数表,选择子的名称则是表的 “键”,对应的值都是指向函数的指针。objc_msgSend 等函数就是通过这个函数表来寻找应该执行的方法并执行跳转的。
7. 如果某函数的最后一项操作是调用另外一个函数,那么就可以运用 “尾调用优化” 技术。编译器会生成跳转至另外一个函数所需的指令码,而且不会向调用栈推入新的 “栈帧”。
第 12 条:理解消息转发机制
当对象接收到无法解读的消息后,就会启动 “消息转发”(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。
消息转发分为两大阶段:
* 若没有 ”备援的接收者“(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation 对象中,再给接受者最后一次机会,令其设法解决当前还未处理的这条消息。
动态方法解析
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
//表示这个类是否能新增一个方法来处理此选择子
备援接收者
当前接收者还有第二次机会处理未知的选择子,运行期系统会它:能不能把这条消息转发给其他接收者来处理:
- (id)forwardingTargetForSelector:(SEL)aSelector
我们无法操作经由这一步所转发的消息,若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制。
完整的消息转发
将消息有关的信息全部丢到NSInvacation 对象中,把消息指派给目标对象
- (void)forwardInvocation:(NSInvocation *)anInvocation
若对象无法响应某个选择子,则进入消息转发流程。
通过运行期的动态方法解析功能,我们可以在需要用到的某个方法时再将其加入类中。
对象可以把其无法解读的某些选择子转交给其他对象来处理。
经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
第 13 条:用 “方法调配技术” 调试 “黑盒方法”
不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能,新功能在本类的所有实例都生效,此方案称为 “方法调配”(method swizzling)。
每个类有个方法列表(函数指针 IMP),各自映射到自己的方法实现,只要我们能操作这个函数指针的指向,我们就可以动态的增加替换原有的方法。
互换两个已经写好的方法实现:
void method_exchangeImplementations(Method m1, Method m2)
方法实现获取:
Method class_getInstanceMethod(Class cls, SEL name)
4. 为已有方法增加新功能:
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector, method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
在运行期,可以向类中新增或替换选择子所对应的方法实现。
使用另一份实现来替换原有的方法实现,这道工序叫做 “方法调配”,开发者常用此技术向原有实现中添加新功能。
一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
第 14 条:理解 “类对象” 的用意
每个Objective-C 对象实例都是指向某块内存数据的指针。
Objective-C 对象所用的数据结构
struct objc_object {
Class isa;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
每个对象结构体首个成员是Class 类的变量,定义了对象所属的类,通常称为 “is a” 指针。
3. Class 对象的数据结构定义:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
Class 首个变量也是isa 指针,说明Class 本身也是Objective-C 对象,指向 “元类”(meta class)。
在类继承体系中查询类型信息
“isMemberOfClass” 判断对象是否为某个特定类的实例
“isKindOfClass” 判断出对象是否为某类或其派生类的实例
每个实例都有一个指向Class 对象的指针,用以表明其类型,而这些Class 对象则构成了类的继承体系。
如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知。
尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
第 15 条:用前缀避免命名空间冲突
如果发生命名冲突(naming clash),那么应用程序的链接过程就会出错,因为出现了重复符号。
应该为所有名称都加上适当的前缀,最好是三个字母以上做前缀,因为苹果宣称其保留使用所有 “两字母前缀”。
在类的实现文件所有的纯C 函数及全局变量,也是容易命名冲突的,在编译好的目标文件中,这些要算做 “顶级符号”(top-level symbol)。
如果自己的代码准备再发布为程序供他人开发应用程序所用,自己的代码以及自己引用到的第三方库都是要加前缀的,避免在未来冲突。
第 16 条:提供 “全能初始化方法”
“全能初始化方法”(designated initializer):为对象提供必要信息以便其能完成工作的初始化方法。
每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。
在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
若全能初始化方法与超类不同,则需覆写超类中的对应方法。
如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
第 17 条:实现 description 方法
在调用NSLog(@"object = %@",onbject); 其实是调用了对象的description 方法。
在我们自定义类中,这样子打印输出信息有可能是这种object = ,这个我们需要重写description 方法,让它返回我们需要的一些信息。
description 定义在NSObject 协议里面,因为NSObject 不是唯一的 “根类”,用继承不能很好的让其他类有这个方法,例如:NSProxy 也是遵从了NSObject 协议的 “根类”。
小技巧:可以在description 中用NSDictionary 的description 方法来输出,就是将信息用字典的形式来展示,这样子更加直观,也更加容易扩展。
debugDescription 方法是开发者在调试器中以控制台命令打印对象时才调用的,默认是直接调用description 方法。po object;
第 18 条:尽量使用不可变对象
设计类的时候,用属性来封装数据,在用属性的时候,可将其声明为 “只读” ,避免外部不必要的修改(PS:如果把可变对象放到collection 之后又修改其内容,很容易会破坏set 的内部数据结构,使其失去固有的语义)。
尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。
当我们想外部暴露只读属性、内部需要修改属性,这样子通常是在内部将readonly 属性重新声明为readwrite。但是如果该属性是nonatomic 的,这样子做可能会产生 “竞争条件”(rece condition)。在对象内部写入某属性时,对象外的观察者也许正在读取该属性。若想避免此问题,我们可以在必要时通过 “派发队列”(dispatch queue)等手段,将所有的数据存取操作都设为同步操作。
虽然属性对外设置成readonly 了,但是外部仍能通过 “键值编码”(Key-Value Coding,KVC)技术设置这些属性值。[object setValue:@"abc" forKey:@"name"] ,这样子可以修改name 这个属性,KVC 会在类中查找 “setName:” 方法来修改属性值。
还可以通过类型信息查询功能,查出属性所对应的实例变量在内存中的偏移量,从此来人为设置这个实例变量的值。
第 19 条:使用清晰而协调的命名方式
方法和变量名使用 “驼峰式大小写命名法”:以小写字母开头,其后每个单词首字母大写。类名也采用驼峰式命名法,不过其首字母需要大写,通常还会加两三个前缀字母。
方法命名
- (id)initWithWidth:(float)width andHeight:(float)height;
把方法名起的稍微长一点,可以保证其能准确传达出方法所执行的任务,但是也不能累赘,尽量言简意赅。
清晰的方法名从左至右读起来好似一篇文章,易于维护,他人也更加易懂。
NSString 这个类就展示了一套良好的命名习惯,可以去查看下头文件。
给方法命名总结:
如果方法的返回值是新创建的,那么方法名的首个词应该是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。
应该把表示参数类型的名词放在参数前面。
如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名字。
不要使用str 这种简称,应该使用string 这样的全称。
Boolean 属性应加is 前缀。如果某方法返回非属性的Boolean 值,那么应该根据其功能,选用has 或is 当前缀。
将get 这个前缀留给那些借由 ”输出参数“ 来保存返回值的方法,比如说,把返回值填充到 ”C语言式数组“ 里的那种方法就可以使用这个词做前缀。
类与协议的命名
应该为类与协议的名称加上前缀,以避免命名空间冲突。
命名应该协调一致,从其他框架继承子类,务必遵循其命名惯例。UIView 子类末尾必须是View,委托协议末尾必须是Delegate。
起名时应遵从标准的Objective-C 命名规范,这样子创建出来的接口更容易为开发者所理解。
方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
方法名里不要使用缩略后的类型名称。
给方法起名时第一要务就是确保其风格与你自己的代码或所有集成的框架相符。
第 20 条:为私有方法名加前缀
便于区分公共方法跟私有方法。
前缀根据个人喜好定,目前发现很多第三方库也很少使用这个私有方法名加前缀,这个就看个人喜好吧。
第 21 条:理解Objective-C 错误模型
ARC 默认不是 “异常安全的”,如果抛出异常,那么应在作用域末尾释放的对象现在却不会自动释放了。想要生成 “异常安全的” 代码,可以设置编译器的标志来实现 “-fobjc-arc-exceptions”。
平常很难写出在抛出异常时不会导致内存泄漏的代码,Objective-C 语言现在采用的办法是:只在极其罕见的情况下抛出异常,抛出异常应用程序直接退出,不考虑修复问题,不用再写复杂的 “异常安全” 代码。
在 “不那么严重的错误”,令方法返回nil/0,或者是使用NSError,表明其中有错误发生。
NSError 可以经由此对象,把导致错误的原因回报给调用者。
- (void) connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error;
另外一种常见的方法是:经由方法的 “输出参数” 返回给调用者
//定义
- (BOOL) doSomething:(NSError **)error;
//用法
NSError *error = nil;
BOOL ret = [objecr doSomething:&error]
if(ret){
//to do
}
//具体实现
- (BOOL) doSomething:(NSError **)error {
if(/*there was an error*/){
if(error){
*error = [NSError errorWithDomain:domain
code:code
userInfo:userInfo];
return NO;
}
}else{
return YES;
}
}
//这个*error 语法会为error 参数“解引用”(dereference),也就是说,error 所指的那个指针现在要指向新的NSError 对象,所以这里要确保error 参数不是nil。
传递给方法的参数是个指针,而该指针的又指向另外一个指针,那个指针指向NSError 对象(指向NSError 对象的指针)。这样子,此方法不仅能有普通的返回值,还可以经由 “输出参数” 把NSError 对象回传给调用者。
使用ARC 时,编译器会吧NSError* 转换成NSError _ _autorelease*, 也就是说指针所指的对象会在方法执行完毕后自动释放。这个对象必须自动释放,因为 “doSomething:” 方法不能保证调用者可以把此方法中创建的NSError 释放掉,所以必须加入autorelease。
第 22 条:理解NSCopying 协议
1. 使用对象经常需要拷贝它,此操作通过copy 方法完成。如果想令自己的类支持拷贝操作,那就实现NSCopying 协议,该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone
2. 以前开发程序,会把内存分成不同的 “区”(zone),而对象会创建在不同区里面,现在不用了,每个程序只有一个区:“默认区”(default zone)。
3. NSMutableCopying 协议跟NSCopying 类似,也只有一个方法:
- (id)mutableCopyWithZone:(NSZone *)zone
4. 如果你的类分可变版本与不可变版本,这两个协议你都应该实现。
5. 注意:在可变对象上调用copy 方法返回另外一个不可变类的实例。
6. 在编写拷贝方法时,还要确定一个问题:应该执行 “深拷贝”(deep copy)还是 “浅拷贝”(shallow copy)。
7. 深拷贝是指在拷贝对象自身时,将其底层的数据也一并复制过去;浅拷贝只对拷贝对象的指针,并不会拷贝底层的数据。Foundation 框架中的所有collection 类默认都执行浅拷贝。
8. 没有专门定义深拷贝的协议,所以具体执行方式由每个类来确定。另外不要假设遵从了NSCopying 协议的对象都会执行深拷贝。绝大多数情况下,执行的都是浅拷贝。
若想令自己所写的对象具备拷贝功能,则需实现NSCopying 协议。
如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying 与 NSMutableCopying 协议。
复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
如果你所写的对象需要深拷贝,那么可以考虑新增一个专门执行深拷贝的方法。