当前位置: 游戏平台 > 互联网科技 > 正文

《Effective Objective C 2.0 编写高素质iOS与OS X代码的53个有效方法》读书笔记

时间:2020-04-15 14:56来源:互联网科技
oc使用动态绑定的消息结构,在运行时才会检查对象类型。接收消息后,执行代码,由运行环境而非编译器来决定。 面向对象语言,"对象"就是"基本构造单元",开发者通过对象来存储并

oc使用动态绑定的消息结构,在运行时才会检查对象类型。接收消息后,执行代码,由运行环境而非编译器来决定。

面向对象语言,"对象"就是"基本构造单元",开发者通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做"消息传递"。

使用消息结构的语言,其运行时所执行的代码由运行环境来决定;而使用函数调用的语言,由编译器决定。如果调用的函数是多态的,那么在运行时就按照“虚方法表”来查出到底应该执行哪个函数实现。而采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。实际上,编译器甚至不关心接收消息的对象是何种类型。接收消息的对象问题也要在运行时处理,甚至过程叫做“动态绑定”。

23. 通过委托与数据源协议进行对象间通信

我们实际编码时已经经常使用到protocol的技术了(委托代理模式)

定义代理属性时,切记使用weak而非strong,避免“保留环”

@property (nonatomic, weak) id<EOCSomeDelegate> delegate;

第一章 熟悉Objective-C

第1条:了解OC起源
消息结构,运行时所执行代码有运行时环境决定,而函数调用,则有编译器决定。

第2条:类的头文件中尽量少引用其他头文件
向前声明 @class 的好处:

1、是延迟引入,减少类的使用者所需的引入的头文件数量

2、解决类之间的相互引用

第3条:多用字面量语法,少用与之等价的方法
NSNumber *number = [NSNumber numberWithInt:1];
NSNumber *number = @1;

优势:
1、简单、易读、防Nil;特别是NSArray、NSDictionary生成时遇到nil会报错,可以提前排查出问题
局限:
2、生成的是不变量,如果需要可变量,需要mutbleCopy;
3、也紧紧局限Foudation框架

第4条:多用类型常量,少用#define预处理指令
类型常量,用static const 声明 = #define

预处理定义出来的常量不包含类型信息,编译器只是会在编译前根据此执行查找与替换。即使有人重新定义常量值,编译器也不会警告,这样会导致常量不一样。

第5条:用枚举表示状态,选项,状态码

1. 在类的头文件中尽量少引入其他头文件

  • 在编译使用PersonTest类文件时,不需要知道PersonTest类的全部细节,只需知道类名就好,使用向前声明(@class PersonTest;)就行。
  • 除非确有必要,否则不要引入头文件。一般应在某个类的头文件中使用向前声明(@class 类名)来提及别的类,并在实现文件中引用那些类的头文件。这样做尽量降低类之间的耦合。
  • 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况,尽量吧“该类遵循某协议”的这条声明移至“calss-continuation分类”中。若不行,就把协议单独放在一个头文件中,然后将其引入。

24. 将类的实现代码分散到便于管理的几个分类中

为了避免一个实现文件太大,实现的方法太多,可以根据功能将类的实现分到不同的分类中。

EOCPerson类可以分成几个不同的实现文件:

EOCPerson+Friendship(.h/.m)
EOCPerson+Work(.h/.m)
EOCPerson+Play(.h/.m)

如果要使用分类中的方法,记得引入分类的头文件。

这样分散到分类中的好处是:

  1. 便于调试,编译后的符号表中,分类中的方法符号会出现分类的名称。
  2. 如果将私有方法放在名为Private的分类中,那很容易看到调试错误原因,并且在编写通用库供他人使用时,私有分类的头文件不公开,只能程序库自己能用。

第二章 对象,消息,运行机制

第6条:熟悉“属性”

第7条:在对象内部尽量直接访问实例变量
在对象内部,读实例变量时用下划线,写时用存取方法(属性)来写;

2. 多用字面量语法,少用与之等价的方法

  • 使用字面量语法创建字符串、数值、数组、字典,与常规方法比更简明扼要
  • 应该通过取小标操作来访问数组下标或字典中键所对应的元素
  • 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常,终止程序,其语法更安全
  • 局限性:除字符串外,所创建的对象必须属于Foundation框架才行
 NSNumber *number = [NSNumber numberWithInt:1]; NSNumber *num2 = @1; NSNumber *booln = @YES; NSNumber *cn = @'a'; NSArray *ary = [NSArray arrayWithObjects:@"cat",@"tom",@"mouse", nil]; NSArray *ary1 = @[@"cat",@"tom"]; NSString *dog = [ary objectAtIndex:1]; NSString *dog1 = ary[1]; NSDictionary * pd = [NSDictionary dictionaryWithObjectsAndKeys:@"Tom",@"name",[NSNumber numberWithInt:26],@"age", nil]; NSDictionary *pd1 = @{@"name":@"Tom", @"age":@26};

25. 为第三方类的分类名称加前缀

分类机制常用在向无源码的类中新增功能。

将分类方法加入源类的操作是在运行期间系统加载分类时完成的。

如果分类中的方法名称与类中已有的方法名一样,分类中的方法就会覆盖原来的实现。解决办法就是给方法加前缀。

在整个应用程序中,类的每个实例都可以调用分类的方法。

直接访问

1、直接访问速度更快,无需方法派发
2、初始化,dealloc用下划线读写数据。
3、直接访问,不会调用设置方法,copy属性,不会拷贝属性,而且保留新值释放旧值
4、不能KVO
5、不便于调试

3. 多用类型常量,少用 #define 预处理指令

  • 不要用预处理指令定义常量。这样定义出来的常量不含类性信息,编译器只会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告
  • 在实现文件中使用static const 来定义"只在编译单元内可见的常量"。由于此类常量不会在全局符号表中,所以无需为其名称加前缀
  • 在头文件中使用extern 来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀
 #define ANIMATION_DURATION 0.3 static const NSTimeInterval kAnimationDuration = 0.3; extern const NSTimeInterval TestAnimationDuration; const NSTimeInterval TestAnimationDuration = 0.3;

26. 勿在分类中声明属性

除了"class-cotinuation分类",其他分类都无法向类中新增实例变量,它们无法把实现属性所需的实例变量合成。

当然,使用关联对象可以解决这种无法合成实例变量的问题:

#import <objc/runtime.h>

static const char *kFriendsPropertyKey = "kFriendsPropertyKey";
@implementation EOCPerson (Friendship)

- (NSArray *)friends {
    return objc_getAssociatedObject(self,kFriendsPropertyKey);
}

- (void)setFriends:(NSArray *)friends {
    objc_setAssociatedObject(self,kFriendsPropertyKey,
                            friends,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

但是这样不理想,因为内存管理语义容易出错。万一你修改了属性的内存管理语义,还要记得在设置方法中修改关联对象所用的内存管理语义。所以不推荐这样做。

属性应该都定义在主接口里。分类的目的在于扩展功能,而非封装数据。

存取方法:

1、init中不要用存取方法,防止子类覆盖
2、惰性初始化一定要用存取方法

第8条:理解“对象等同性”
== 判断指针是否相等, isEqualTo判断类型、属性和hash值 (isEqual会根据类,进行方法分发,工厂方法),在复写isEqual方法时,需要注意其他情况调用super

关于hash值:如果collection类型的属性,直接写死固定值,会造成该固定值的对应的value变多,而影响性能。如果通过整体求hash,也出现中间变量,存在性能损耗。可以多每个属性求hash,在进行与或处理
注意:把对象放入collection之后,改变其内容会造成很严重的后果

第9条:以“类簇模式”隐藏实现细节
Cocoa里面很多类族实现,这种工厂方式的实现,因此不能用[subA class] == [A class]的方式进行判断,应该使用类型查询方式isKindOfClass进行类型判断。

第10条:在既有类中使用关联对象存放自定义数据
objc_setAssociateObject(id object , const void *key , id value , objc_AssociationPolicy policy)

object:被关联的对象;

key:唯一key;

value:关联的对象;

police:状态内存策略(copy,retain)

与Dictionary比较 设置关联对象的key一般是“不透明指针”,所以用静态全局变量作为key;同时要指定内存管理语义,用于模仿拥有 和 非拥有关系

objc_getAssociateObject

objc_removeAssociateObject

不需要主动调用remove来移除关联对象,一般调用objc_setAssociateObject来设置为nil,objc_removeAssociateObject会将所有的关联对象都移除。

第11条:理解objc_msgSend的作用
objc_msgSend(id self , SEL cmd , ...) 参数可变的函数

消息由接受者、选择子、参数组成,给对象发送消息,相当于对象调用方法
每个类都有一张函数调用表,key为选择子,value为实际调用的函数值。尾调用优化技术,使跳转更加简单:直接跳转,不需要调用堆栈,进行优化。
方法列表若是没有找到,进入消息转发,三次挽救机会:
1、+ (BOOL)resolveInstanceMethod:(SEL)sel
2、- (id)forwardingTargetForSelector:(SEL)aSelector
3、- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

  尾递归优化?

尾递归的判断标准时:函数运行的最后一步是否调用自身,而不是是否在函数最后一行调用自身。

优化:不需要保存自身(外尾)的调用帧(即调用栈)。

第12条:理解消息转发机制
1、动态方法解析

  • (BOOL)resolveInstanceMethod:(SEL)sel
    2、备援接收者
  • (id)forwardingTargetForSelector:(SEL)aSelector
    3、完整的消息转发
  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

第13条:用“方法调配技术”调试“黑盒方法”
使用动态消息转发,不用通过继承子类,覆写子类方法实现新功能。使用另一份实现替换原有的方法实现。

操作选择器映射表,可以新增选择子,添加新功能,无需编写子类,只需修改“方法表”布局。

交换方法实现:

void method_exchangeImplementations(Method m1,Method m2)

获得方法实现:

Method class_getInstanceMethod(class aclass , SEl aSelector)

通过这样的方式,就可以为既有的方法增加新功能,一般为黑盒方法增加日志记录功能,有助于调试。

第14条:理解“类对象”
isMemberOfClass 判断对象是否为某个对象特点定类的实例

isKindOfClass 判断对象是否为某类或其派生类的实例

尽量使用类型信息查询方法来确定对象的类型,而不要直接比较类对象,因为某些对象实现了消息转发。

第三章 接口与API设计
第15条:用前缀避免命名空间冲突
在自己开发的程序中用到了第三方库,则应为其中的名称加上前缀。(前三个字母都大写)

第16条:提供“全能初始化方法”
designated initializer OR Initializer from NSCoding;子类与超类不同,子类需要覆盖,超类需要在方法中写Assert。

第17条:实现description方法 和 debugDescription方法

第18条:尽量使用不可变的对象
1、.h 中readonly, .m中readwrite,
但是在对象外面还可以通过KVC的方式(setValue forKey)进行更改(hack);更加brutal 是通过类型查询信息找到对应实例变量在内存中的偏移量,从而进行设置
2、可变的collection,应该通过相关方法,修改可变对象

第19条:使用清晰而协调的命名方式
驼峰命名法

第20条:为私有方法加前缀
不要用下划线开头,那是苹果公司的。

第21条:理解OC错误模型
ARC不是异常安全的,抛出异常,未释放的对象不能自动释放。如果想“异常安全”,增加-fobc-arc-exception标志;
严重错误,抛出NSException;不严重用nil,0、NSError

第22条:理解NSCoding协议
若想令自己的对象具有拷贝功能,需要实现NSCoding协议,实现copyWithZone:(NSZone *)zone方法。

如果对象分为可变与不可变两种版本,即要同时实现NSCoping和MutableCopying协议。

复制对象一般进行浅拷贝,深拷贝可单独写一个方法。深拷贝会将底层数据一起拷贝,包括实例变量。

4. 用枚举表示状态、选项、状态码

  • 应该使用枚举来表示状态机的状态、传递给方法的选项以及状态码等值
  • 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将个选项值定义为2的幂,以便通过按位或操作将其组合起来
  • 用NS_ENUM与NS_OPTIONS宏来定义枚举,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现的,而不会采用编译器所选的类型
  • 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举

27. 使用"class-continuation分类"隐藏实现细节

"class-continuation分类"就是我们常写在实现文件中的这样一段代码:

@interface EOCPerson ()
//property here
@end

这样,可以将方法或者实例变量隐藏在本类中使用,而不暴露给公共接口。

如果属性在主接口中声明为只读,而类内部又要修改属性值,就可以在class-continuation分类中将其扩展为可读写。

第四章 协议与分类

第23条:通过委托和数据源协议进行对象间通信
1、把需要处理的事件方法定义成协议
2、对象从另外一个对象获取数据时,定义成数据源协议
3、若有必要,可实现含有位段的结构体,将委托对象是否响应相关协议缓存其中,(直接在setDelegate方法中进行缓存

第24条:将类的实现分散至各个便于管理的分类中
1、划分成不同的功能区

2、调试方便,因为分类名会出现在类名后面;

3、私有的可以考虑private分类

第25条:为第三方的分类名称加前缀
1、如果二个分类提供的方法重名,后编译分类方法会覆盖前面分类方法,分类编译顺序与添加到工程中的顺序有关;

2、如果方法名相同,分类会覆盖苹果自带的方法

第26条:勿在分类属性中声明属性
1、在“class-continuation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性,虽然技术上可行。

2、如果声明,会出现warning,原因是分类中无法合成与声明属性相关的变量,所以需要在分类中实现存取方法,并且实现中声明为@dynamic,意思就运行时在提供。当然关联对象也可以实现这种需求,但是仍然建议只在分类中提供方法,

3、把封装的数据所用的全部属性都定义在主接口里。

第27条:使用“class-continuation分类”隐藏实现细节
为什么要有这种分类:因为可以定义方法和实例变量,
隐藏实例方法和方法,也可以避免不必要头文件的引入,特别是对于OC++而言,引入的C++头文件。这样.h中进行向前声明,避免引入不必要的头文件,
也可以将类遵循的协议放在class-continuation,但是向前声明delegate却会有警告,因为引入.h文件,编译器看不到协议的定义及包含的方法。
为什么可以定义方法和实例变量:因为ABI机制,我们无须知道对象大小也可以使用。

第28条 通过协议提供匿名对象

1、如果具体类型不重要,只是能响应特定方法,那么可使用匿名对象表示,声明为id类型,来隐藏类型名称

5. 理解"属性"这一概念

"属性"用于封装对象中的数据。对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过"存取方法"来访问。其中,"获取方法"用于读取变量值,而"设置方法"用于写入变量值.开发者可令编译器自动编写与属性相关的存取方法。此特性引入了一种新的“点语法”,使开发者可以更容易的依照类对象来访问其中的数据。

存取方法有着严格的命名规范,所以OC语言才能根据名称自动创建存取方法,@property语法等同与写一套存取方法,@property NSString*name就是编译器自动写出一套存取方法。

若不想令编译器自动合成存取方法,则可以自己实现,如果你只实现了其中一个存取方法,那么另一个还是由编译器来合成。使用@dynamic关键字,可以阻止编译器自动合成存取方法。

属性特质: 原子性、读/写权限、内存管理语义、方法名

@property(nonatomic,readwrite,copy) NSString *firstName@property(nonatomic,readwrite,copy,getter=isOn) BOOL on
  • 原子性默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性。atomic与nonatomic区别?

  • 具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性,就是说,如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值。若是不加锁的话(或者使用nonatomic语义),那么当其中一个线程在改写某属性值时,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来。发生这种情况时,线程读到的属性值可能不对。

  • 使用nonatomic历史原因:在iOS中使用同步锁的开销较大,会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这样并不能保证“线程安全”,若要实现“线程安全”的操作,还需采用更深层次的锁定机制。如一个线程在连续多次读取属性值的过程中有别的线程在同时修改该值,即使用atomic,也还是会读到不同的属性值。

  • 读写权限

  • readwrite特质属性拥有"获取方法"与"设置方法";

  • readonly特质属性只拥有"获取方法";

  • 内存管理语义

  • assign "设置方法"只会执行针对"纯量类型"(如CGFloat、NSInteger等)的简单赋值操作

  • strong 此特质表明该属性定义了一种"拥有关系"。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后将新值设置上去

  • weak 此特质表明该属性定义了一种"非拥有关系"。 为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁时,属性也会清空

  • unsafe_undertained 此特质的语义和assign相同,但是它适合用于"对象类型",该特质表达一种"非拥有关系",当目标对象遭到摧毁时,属性值不会自动清空,这一点与weak区别

  • copy 此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其"拷贝"。

  • 方法名

  • getter =<name>

  • setter=<name>

  • 多使用nonatomic属性,因为atomic属性会影响性能

  • 通过"特质"来指定存储数据所需的正确语义

28. 通过协议提供匿名对象

协议可以在某种程度上提供匿名类型。具体的对象类型可以淡化成遵守某协议的id类型。

使用匿名对象来隐藏类型名称。

如果具体类型不重要,重要的是对象能够响应特定方法,那么可使用匿名对象来表示。

第五章 内存管理

第29条:理解引用计数

第30条:ARC简化引用计数

第31条 在dealloc中只释放非OC对象引用并解除监听
运行时会在适当的时候调用dealloc,不要主动调用dealloc,但是手动 需要最后调用 super dealloc

1、开销较大或者系统内资源(比如文件描述符、套接字、大块内存等)不在dealloc中进行,应该单独提供方法进行释放,还有一个原因是系统并不保证每个创建出来的对象dealloc都会执行,也可以考虑在Appdelegate中种植方法执行清理,防止内存泄露
2、在dealloc不建议调用其他函数,防止调用过程中对象已经销毁。也不要调用属性的存取方法,因为有人会对其进行覆盖,也可能处于KVO下。(这个存取方法待讨论)
3、self.tableView.delegate 设置为nil

第32条:编写异常代码时,留意内存管理
MRC时,可以将内存释放写在finnal里面(提问:为什么不能写在try 和 catch 里面)【因为在写在try中,抛异常会跳到catch中,内存泄漏】,但是变量就必须放在块的外面
ARC时:不会自动处理try catch的内存管理,因为ARC不能调用release,所以需要很多样板代码,进行跟踪清理对象,影响运行时性能。因为ios认为因为异常而终止程序,内存管理也就没有必要了。
1、通过-fobjc-arc-exceptions进行开启安全异常处理,默认情况是关闭的。OC++模式是默认开启的
2、建议通过NSError方式进行错误捕捉。

第33条:以弱引用避免保留环

第34条:以“autoreleasepool”降低内存峰值

第35条:僵尸对象调试,内存管理问题
当对象已经释放,但是还没被覆盖时,调用这块内存会正常工作,但是存在很大风险。Cocoa:系统在回收对象时,可以不真的将其回收,而是把它转化为僵尸对象。NSZombieEnabled设置为yes,或者在scheme中勾选。
僵尸类是从NSZombie模板复制出来的,并且可以保留原类名字,不采用继承的方式,是因为效率因素的考虑。

其实现原理是:
修改对象的isa指针,让它指向僵尸类,使对象变成僵尸对象,僵尸类能影响所有的选择子:打印相关消息,并且终止程序。
创建新类,并且转化为僵尸对象

第36条:不要使用retainCount

6. 在对象内部尽量直接访问实例变量

不经过OC的"方法派送"步骤,所以直接访问实例变量的速度比较快,此情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。直接访问实例变量不会触发“键值观测”通知;直接访问实例变量有助于排查与之相关的错误,加“断点”,监控该属性的调用者及访问时机;

直接访问实例变量不会调用其"设置方法",绕过了相关属性所定义的"内存管理语义"。

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则通过那个属性来写
  • 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据
  • 有时会使用惰性初始化技术配置某份数据,此情况,需要通过属性来读取数据

29. 引用计数

引用计数变为0后“可能”就释放内存了,其实只是放回“可用内存池”,如果没有被覆写之前仍然可以访问,但这是很危险的,因为这样很可能出现野指针,造成程序崩溃。

弱引用避免保留环。

第六章 块与大中枢派发

第37条:理解“块”

第38条:为块创建typedef
return_type (^block_name)(parameters)
优点:
1、定义变量一样定义block
2、重构时如果给Block多增加参数,那么只需修改相应的块签名(typedef),其他引用block的地方也会自动报错,避免遗漏

第39条:用handler块降低代码分散程度
1、使用delegate 会使代码结构过于分散,可以直接只用回调块,使块和相关对象放在一起,避免通过delegate透传数据
2、通过handler,增加队列参数,决定放在哪个队列上。
3、Error块和Succes块 放在一起,可以处理返回结果中数据异常的情况 VS Error 和 Success分开处理,更加清晰。

第40条:用块引用其所属性避免循环引用
1、设计API时,可以考虑在调用完complete的块之后,将环中的某个对象设置为nil,解除环,避免API调用者没有处理保留环的问题。也可以通过weakify的方法。
2、API调用者可以通过weakify的方法,解除保留环的问题;

第41条:多用派发队列,少用同步锁

7. 理解"对象等同性"概念

  • 若检测对象的等同性,提供"isEqual:"与"hash"方法
  • 相同对象具有相同的哈希码,但是两个哈希码相同的对象却未必相同
  • 不要盲目的逐个检测每条属性,而是应该依照具体需求来制定检测方案
  • 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法

30. 以ARC简化引用计数

ARC是会自动执行retain、release、autorelease的,所以在ARC模式下是不可以直接调用内存管理方法的,具体如下方法:

  • retain
  • release
  • autorelease
  • dealloc

实际上,ARC不是通过OC的消息派发机制的,而是直接调用底层的C语言版本比如objc_retain。可以节省很多CPU周期。

若方法名以下列词语开头,则返回的对象归调用者所有, 即调用的代码要负责释放对象。

  • alloc
  • new
  • copy
  • mutableCopy

若方法名不以上述四个词语开头,则表示返回的对象不归调用者所有,返回的对象会自动释放。

这些规则所需要的内存管理事宜都有ARC自动处理。

ARC在运行期还可以起到优化作用, 比如在autorelease之后立马又调用retain的场景下,ARC在运行期可以检测这种多余的操作,利用全局数据结构中的一个标志位来决定是否需要真正执行autorelease和retain操作。

ARC只负责管理OC对象的内存,而CoreFoundation对象不归ARC管理,还需要开发者适当调用CFRetain/CFRelease。

派发队列更加单实现同步语义。
  GCD之前,有2种方法:
        1、同步块 @synchrnoized(someObject)一般是对self创建锁,但是其中会涉及与self无关的代码,降低代码效率。
        2、NSLock对象,通过[_lock lock] [lock unlock]加锁,解锁;NSRecuresiveLock 递归锁
       同一个锁的同步块,顺序执行

通过atomic 属性同步,这是通过synchrnoized的方式实现的?
1、同步和异步派发结合可以实现加锁机制一样的同步问题,但是却不阻塞异步派发的进程,但是仍然无法正确同步
2、使用同步队列及栅栏块可以令同步行为更加高效。

3、异步派发,需要拷贝块,因此异步派发不一定会比同步快,需要考虑拷贝块与执行块的时间

第42条:多用GCD,少用performSelector系列方法
1、会发生warning,导致内存泄漏,因为编译器不知道调用什么选择子,方法签名、返回值,无法通过ARC对返回值进行管理。
2、选择子太过局限,返回类型(void或者id,不能是struct)和参数都有局限。
3、如果把任务放在指定线程执行,用GCD和块,毕竟块可以捕获外部变量。

第43条:掌握GCD和操作队列的使用时机
NSOperation 好处:
1、取消操作 2、指定依赖关系 3、通过KVO监控NSoperation对象的属性 4、指定操作的优先级 5、可以复用NSOperation对象
NSNotifationCenter 就是使用的操作队列

第44条:使用dispatch group进行任务分组
1、dispatch_group_async 包含block,用于回调
2、dispatch_group_enter && dispatch_group_leave
dispatch_group_wait (阻塞)使用表示group可以阻塞的时间;dispatch_group_notify(不阻塞),使用group结束的回调。
dispatch_apply 用于重复执行的次数

第45条:dispatch_once 只执行一次、线程安全
1、之前通过@synchronized(self) 创建单例,比dispatch-once慢二倍
2、需要一个标记,标记声明为static 或者global,目的是标记都相同

第46条 不要使用dispatch_get_current_queue
dispatch_get_current_queue 已经废弃,只做调试用

8. 以"类族模式"隐藏实现细节

  • 类族模式可以把实现细节隐藏在一套简单的公共接口后面
  • 系统框架常使用类族
  • 从类族的公共抽象类中继承子类要当心
* 子类应该继承自类族中的抽象基类* 子类应该定义自己的数据存储方式* 子类应当覆写超类文档中指明需要覆写的方法

31. 在dealloc方法中只释放引用并解除监听

每个对象生命周期结束后最终为系统回收,执行一次且仅一次dealloc方法。

在此方法中释放对象所拥有的所有引用,ARC会自动生成.cxx_destruct方法。

此方法还要做一件重要的事情,就是把配置的observation behavior都清理掉,比如NSNotificationCenter给此对象订阅过某种通知,那么应该在此注销。否则继续给对象发送通知的话会导致crash。

- (void)dealloc {
    CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

如果非ARC模式,最后还要调用[super dealloc],ARC模式下就不需要。

对于开销较大或者系统稀缺的资源(如文件描述符、套接字、大块内存等),应该使用"清理方法"而非dealloc来释放。比如网络连接使用完毕后,调用close方法。
这样做的原因是:

  1. 避免保留稀缺资源的时间过长。
  2. 系统为了优化程序效率,不保证每个对象的dealloc方法都被执行。

在dealloc中,可以检测资源是否执行了清理操作,没有的话可以输出错误信息并执行一次清理操作。

有些方法不应该在dealloc方法里调用,比如

  • 执行异步任务的方法
  • 需要切换到特定线程执行的方法
  • 属性的存取方法

第七章 系统框架

第47条:熟悉系统框架

第48条:多用枚举块,少用for循

9. 在既有类中使用关联对象存放自定义数据

有时候类的实例可能是有某种机制所创建的,而开发者无法令这种机制创建出自己所写的字类实例,这时候就需要关联对象解决问题。

* void objc_setAssociatedObject(id object,void *key,id value,objc_AssociationPolicy policy)此方法以给定的键和策略为某对象设置关联对象值* id objc_getAssociatedObject(id object,void *key)此方法根据给定的键从某对象中获取相应的关联对象值* void objc_removeAssociatedObject(id object)此方法移除指定对象的全部关联对象

例子将创建警告视图与处理操作结果的代码放在一起:#import <objc/runtime.h>static void *EOCMyAlertViewKey = "ECOMyAlertViewKey";- askUserAQuestion{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What are you doing?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil]; void(NSUInteger) = ^(NSUInteger buttonIndex){ if(buttonIndex == 0 ){ NSLog(@"doCancel"); }else{ NSLog(@"doContinue"); } }; objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY); [alert show]; }- alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ void(NSUInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey); block(buttonIndex);}
  • 可以通过"关联对象"机制来把两个对象连起来
  • 定义关联对象时可指定内存管理语义,用以模仿定义属性所采用的"拥有关系"与"非拥有关系"
  • 只有在其他做法不可行时才因选用关联对象,因为这种做法通常会引入难于查找的bug

给分类添加属性://使用前记得#import <objc/runtime.h>

- setName:(NSString *)name{ // 保存name // 动态添加属性 = 本质:让对象的某个属性与值产生关联 /* object:保存到那个对象中 key:用什么属性保存 属性名 value:保存值 policy:策略,strong,weak */ objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // _name = name;}- (NSString *)name{ return objc_getAssociatedObject(self, "name"); // return _name;}

32. 编写“异常安全代码”时留意内存管理问题

虽然OC中异常只应发生在严重的错误中,但是有时候还是要编写代码来捕获异常。

在捕获异常时要管理好内存,防止泄漏。

比较下面的两种处理方法,明显后者更合适:

(1)

@try {
   EOCSomeClass *object = [[EOCSomeClass alloc] init];
   [object doSomethingThatMayThrow];
   [object release];//此处可能执行不到,造成内存泄漏
}
@catch (...) {
   NSLog(@"exception");
}

(2)

EOCSomeClass *object;
@try {
   object = [[EOCSomeClass alloc] init];
   [object doSomethingThatMayThrow];
}
@catch (...) {
   NSLog(@"exception");
}
@finally {
   [object release];//此处总会执行到。
}

以上是非ARC模式下的做法,如果是ARC模式也这样try/catch就有很大问题了,因为ARC针对这种情况不会自动处理release,这样做的代价很大。但是ARC还是可以生成安全处理异常所用的代码,只需要打开-fobjc-arc-exceptions编译器标志。

所以,总的来说,如果非ARC模式下必须捕获异常,那就设法保证代码能把对象清理干净; 如果是ARC下必须捕获异常,就要打开-fobjc-arc-exceptions标志。当然,如果发现程序有大量异常捕获操作时,说明你的代码需要重构了。

枚举方式:

1.for循环
2.NSEnumerator 遍历
3.快速遍历、块枚举
4.块枚举,支持GCD来并发执行遍历操作

typedef NS_OPTIONS(NSUInteger, NSEnumerationOptions) {
    NSEnumerationConcurrent = (1UL << 0),
    NSEnumerationReverse = (1UL << 1),
};

如果提前知道collection对象类型,应修改块签名,指出对象具体的类型

第49条:对自定义其内容管理语义的collection使用无缝桥接

第50条:构建缓存时选用NSCache,而非NSDictionary

第51条:精简initialize与load实现代码

第52条:NSTimer会保留其目标对象

10. 理解objc_msgSend的作用

对象调用方法,oc术语叫"消息传递",消息有"名称"或"选者子",可以接受参数,可有返回值。

void objc_msgSend(id self,SEL cmd,...)这个是"参数个数可变的函数",能接受两个或两个以上的参数.第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后续参数是消息中的参数,其顺序不变。选择子指的就是方法的名字。

objc_msgSend函数会根据接收者与选择子来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其"方法列表",如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行"消息转发"操作。

  • 消息由接收者、选择子及参数构成。给某对象"发送消息"也就是相当于在该对象上"调用方法"
  • 发给某对象全部消息都要由"动态消息派发系统"来处理,该系统会查出对应的方法,并执行起代码

11. 消息转发机制

消息转发分为两大阶段:第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理这个"未知的选择子"(unknow selector),这叫做"动态方法分析"(先判断这个类是否能新增一个实例方法用以处理此选择子)。第二个阶段涉及"完整的消息转发机制"。如果运行期系统已经把第一阶段执行完了,那么接收者自己无法在已动态新增方法的手段来响应包含该选择子的消息。此时,运行期系统会请求接收者已其他手段来处理与消息相关的方法调用。细分两小步1.请接收者看看有没有其他对象能处理这条消息。若有,则运行系统会把消息传给那个对象,于是消息转发结束。若没有“备援接收者”则启动完整的消息转发机制,运行系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一个机会,令其设法解决当前还未处理的这条消息.

动态方法解析:对象在收到无法解读的消息后,首先将调用其所属类的类方法:+resolveInstanceMethod:selector该方法的参数就是那个未知的选择子,返回类型为bool类型,表示这个类是否能新增一个实例方法用以处理此选择子。使用这方法前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就行了。此方案常用来实现@dynamic属性,比如要访问CoreData框架中NSManagedObjects对象的属性时就可以用,因为实现这些属性所需的存取方法在编译器就能确定。

备援接收者:当前接收者还有第二次机会能处理未知的选择子,这一步中,运行期系统会问他:能不能把这条消息转发给其它接收者来处理。该步骤对应处理方法:-forwardingTargetForSelector:selector方法参数代表未知的选择子,若当前选择子能找到备援对象,则将其返回,若找不到,返回nil。

完整的消息转发:若转发算法来到这一步,那么只能启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封装与其中。此对象包含选择子、目标及参数。在触发NSInvocation对象时,"消息派发系统"将亲自出马,把消息指派给目标对象。此步骤会调用下列方法来转发消息:- forwardInvocation:(NSInvocation*)invocation此方法很简单:只需改变调用目标,使其消息在新目标上得以调用即可。实现此方法,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样,继承体系中的每个类都有机会处理此调用方法,直至NSObject。若最后调用NSObject类的方法,那么该方法还会继续调用"doesNotRecognizeSelector:"以抛出异常,此异常表明选择子最终未能得到处理。

  • 若对象无法响应某个选择子,则进入消息转发流程
  • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法再将其加入类中
  • 对象可以把其无法解读的某些选择子转交给其他对象来处理
  • 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制

12. 用"方法调配技术"调试"黑盒方法"

类的方法列表会把选择子的名称映射到相关的方法实现上,使得"动态信息派发系统"能够据此找到应该调用的方法。这些方法均已函数指针的形式来表示,此指针叫做IMP。oc运行期系统提供了几种方法可以让我们操作IMP,开发者可以向其新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。

交换实现方法void method_exchangeImplementations(Method m1,Method m2)获取实现方法Method class_getInstanceMethod(Class aClass,SEL aSelector)
  • 在运行期,可以向类中新增或替换选择子所对应的方法实现
  • 使用另一份实现来替换原来的方法实现,这道工序叫做"方法调配",开发者常用此技术向原有的实现中添加新功能
  • 一般只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用

13. 理解"类对象"的用意

"在运行期检视对象类型"这一操作也叫做"类型信息查询",这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类继承而来的对象都要遵从此协议。

每个OC对象对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要加上"*"字符:NSString * s = @"someString";

对象数据结构typedef struct objc_object{ Class isa;} *id;

每个对象数据结构的首个成员时Class类的变量。该变量定义了对象所属的类,通常称为"is a"指针。

Class对象也定义在运行期程序库的头文件中:typedef struct objc_class *Class;struct objc class{ Class isa; Class super_class; const char *name; long version; long info; long instance_size; struct objc_ivar_list *ivars; struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protocol_list *protocols;};

此结构体存放类的"元数据",例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为OC对象。结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做"元类",用来表述对象本身所具备的元数据。"类方法"就定义在此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个"类对象",而每个"类对象"仅有一个与之相关的"元类"。super_class指针确立了继承关系,而isa指针描述里实例所属的类。

  • 每个实例都有一个指向Class对象的指针,用以表面其类型,而这些Class对象则构成了类的继承体系
  • 如果对象类型无法在编译期确定,那么就应该使用类信息查询方法来探知
  • 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象实现了消息转发功能

14. 用前缀避免命名空间冲突

  • 选择公司、应用程序或二者关联之名称作为类名前缀
  • 若自己所开发的程序库中用到了第三方库,则应该为其中的名称加上前缀

15. 提供"全能初始化方法"

  • 若全能初始化方法与超类不同,则需要覆写超类中的对应方法
  • 若超类的初始化方法不适应于子类,那么应该覆写这个超类方法,并在其中抛出异常

16. 实现description方法

  • 实现description方法返回一个有意义的字符串,用以描述该实例
  • 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法

17. 尽量使用不可变对象

  • 若某属性仅可与对象内部修改,则在"class-continuation分类"中将其由readonly属性扩展为readwrite属性
  • 不要把可变的collection作为属性公开,而应该提供相关方法,以此修改对象中的可变collection

编辑:互联网科技 本文来源:《Effective Objective C 2.0 编写高素质iOS与OS X代码的53个有效方法》读书笔记

关键词: