本文是本人看完《高效编写iOS的52条方法》的一些小结及笔记。
第 1 条
1.使用消息结构的语言,其运行时所应执行的代码由运行环境来决定(——动态绑定);而使用函数调用的语言,则由编译器决定。
2.Objectivew-C是C的超集(superset)。Objective-C语言中的指针是用来指向对象的。
3.对象所占内存总是分配在“堆空间(heap space)”中,不能在栈中分配Objective-C对象。
4.分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则在其栈帧弹出时自动清理。
5.Objective-C将堆内存管理抽象出来,不需要用malloc和free来分配或释放对象所占内存。Objective-C运行期环境吧这部分工作抽象为一套内存管理架构——“引用计数”。
第 2 条
1.向前声明(forward declaring):@class className;
a.将引入头文件的时间尽量延后,只在确认有需要时引入,减少类的使用者所需引入的头文件数量;
b.解决了两个类互相引用的问题;
2.只是需要知道某个类 的存在时用@class,需要知道该类的详细内容时,用#import引入。
3.最好把协议单独放在一个头文件中(delegate protocol除外:在此情况下,协议只有和接受协议委托的类放在一起定义才有意义)。
4.可以用向前声明取代引入,那就不要引入。
第 3 条
1.使用字面量语法(literal syntax)可以缩减源代码长度,使其更为易读。例:NSNumber *num = @1;
2.取下标操作。
3.arrayWithObjects:方法会一次处理各个参数,直到发现nil未知,如果中间某个对象为nil,方法会提前结束(而程序猿很可能没有注意到这个问题,从而引起错误)。
如果使用字面量语法等来完成数组、字典等的创建,出现以上情况,程序会立即抛出异常令应用终止——更安全。
4.字典中的对象和键必须都是Objective-C对象。
5.字面量语法的限制:除了字符串以外,所创建出来的对象必须属于Foundation框架才行。
6.使用字面量语法创建出来的字符创、数组、字典对象都是不可变对象(immutable),若需要可变版本对象,用-mutableCopy复制一份即可。
第 4 条
1.多用类型类型常量,少用#define预处理指令 例:static const int kNo = 007;
2.在Objective-C语境下,“编译单元”一词通常指每个类的实现文件(以.m为后缀名)。
3..常量的命名法:
若常量局限于某“编译单元(translation unit——即.m)”之内,则在前面加字母k;若常量在类之外可见,则通常以类名为前缀。
4.若不打算公开某个常量,变量一定要同时用static与const来声明。
5.如果一个变量既声明为static又声明为const,那么编译器根本不会创建符号,而是会像#define预处理指令一样,把所有遇到的变量都替换为常值。但是这种方式定义的常量带有类型信息。
6.有时候需要公开变量——此类变量通常放在“全局符号表(constant variable)”中,以便可以在定义该常量的编译单元值外使用(全局,因此命名需要注意!!!)。
定义方式:extern NSString *const ClassNameConstant;
这个常量在头文件中声明,且在实现文件中“定义” (必须定义,且只能定义一次)—— NSString *const ClassNameConstant = @“Value”;
由实现文件生成目标文件时,编译器会在“数据段”(data section)为字符串分配存储空间。链接器会把此目标文件与其他目标文件相链接,已生成最终的二进制文件。
7.常量定义应从左至右解读。
第 5 条 用枚举表示状态、选项、状态码
1.C++11标准扩充并修改了枚举的特性,其中一项是:
可以指明用何种“底层数据类型(underlying type)”来保存枚举类型的变量。这样做的好处是:可以向前声明枚举变量了,若不指定底层数据类型,则无法向前声明枚举变量,因为编译器不清楚底层数据类型的大小,所以在用到此枚举类型时,也就不知道究竟应该给变量分配多少空间。
typedef enum EOCConnectionState EOCConnectionState;
指定底层数据类型所用的语法是:
enum EOCConnectionState : NSInteger { /* */};
向前声明:enum EOCConnectionState : NSInteger;
2.枚举值
用“按位或操作符”(bitwise OR operator)可以组合多个选项。用”按位与操作符“(bitwise AND operator)即可判断出是否已启用某个选项。
3.如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将个选项值定义为2的幂,以便通过按位或操作将其组合起来。
4.若是用枚举来定义状态机,最好不要有default分支,如果稍后又加了一种状态,那么编译器就会发出警告信息。
第 6 条 属性
1.用Objective-C等面向对象语言编程时,“对象”(object)就是“基本构造单元”(building block)。在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messaging)。
2.点语法 dot syntax
3.偏移量问题:(如果修改了某个类的定义,偏移量也会跟着改变,如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。)
Objective-C把实例变量当做一种存储偏移量所用的“特殊变量”(special variable),交由“类对象”(class object)保管。偏移量会在运行期查找。
4.应用程序二进制接口Application Binary Interface,ABI。
5.属性是一种简称:编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。
6.使用“点语法”和直接调用存取方法相同。self.name ==> [self name];区别是:如果点语法用在赋值号左边,表示存方法,用在右边则代表取方法。
7.@property属性优势:
编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis) 。
需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法(synthesized method)”的源代码。
此外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加”_”,以此作为实例变量的名字。
可以在类的实现代码里通过@synthesize语法来指定实例变量的名字:@synthesize oldName = _newName;
@dynamic关键字,不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。
8.属性特质:
原子性
由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。若没有nonatomic特质,默认为atomic。自定义存取方法也应当遵从与属性特质相符的原子性。
读/写权限
readwrite、readonly
—>可以利用此特质吧某个属性对外公开为只读属性,然后在“class-continuation分类”中将其重新定义为读写属性
内存管理语义(数据要有“具体的所有权语义”(concrete ownership semantic))
assign:设置方法只会针对“纯量类型”的简单赋值操作
strong:表明该属性定义了一种“拥有关系”(owning relationship),为这种属性设置新值时,设置方法会先保留新值,病释放旧值,然后再将新值设置上去。
weak:“非拥有关系”(nonowning relationship),为这种属性设置新值时,既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
unsafe_unretained:此特质的语义和assign相同,但是它适用于“对象类型”(object type),“非拥有关系”(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全,unsafe”),这一点与weak有区别。
copy:表达是所属关系与strong类似。然而设置方法并不保留新值而是将其“拷贝”。
方法名
通过以下方法指定方法的方法名:
getter=(比如,为属性Boolean型的属性加上“is”前缀)
setter=(此用法不太常见)
9.atomic与nonatomic的区别:
若是不加锁的话(或者说使用nonatomic语义),那么当其中一个线程正在修改某属性值时,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来)
10.在iOS中使用同步锁的开销比较大,会带来性能问题,开发iOS程序时一般都会使用nonatomic属性。但是在开发Mac OS X程序时,使用atomic属性通常都不会有性能瓶颈。
第 7 条 在对象内部尽量直接访问实例变量
1.直接访问实例变量和点语法写法的区别
a.不经过方法派发(method dispatch),所以直接访问实例变量的速度更快。编译器所生成代码会直接访问保存实例变量的那块内存。
b.直接访问实例变量时,不会调用其“设置方法”,绕过了为相关属性所定义的内存管理语义。
c.如直接访问实例变量,那么不会触发Key-Value Observing KVO通知。
d.可以通过在属性之中新增“断点”(breaking point),监控该属性 的调用者以及其访问时间来帮助排查相关的错误。
2.在对象内部读取数据时,写入实例变量时,通过其“设置方法”来做,应该直接通过实例变量来读取。
3.惰性初始化(lazy initialisation)情况下,必须通过“获取方法”来访问属性,否则实例变量永远不会初始化。
第 8 条 “对象同等性”
1.==操作符比较的是两个指针本身,而不是其所指的对象。应当使用NSObject协议中声明的“isEqual:”方法来判断对象等同性。
2.NSObject协议中有两个用于判断等同性的关键方法:
-(BOOL)isEqual:(id)object;
-(NSUInteger)hash;
a.NSObject类对这两个方法的默认实现是:当且仅当其“指针值”(point value)完全相等时,这两个对象才相等。
b.某些类有特定的等同判断方法,如 -isEqualString: 、 - isEqualDictionary:等等。。。
c.也可以自己创建等同性判断方法,常见的实现方式为:如果受测的参数与接收该消息的对象都属于同一个类,那么就调用自己编写的方法,否则就交由超类来判断。
3.根据同性约定:若两对象相等,则其哈希码(hash)也相等,但是两个哈希码相同的对象却未必相等。 ——>>>反过来就是:不同的对象的哈希码不相同
哈希码生成的三种方法:
1)所有对象统一返回相同的哈希码。
- (NSUInteger)hash
{
return 1997;
}
2)将NSString对象中的对象都塞入另一个字符串,然后令hash方法返回该字符串的哈希码。这样做是符合约定的,因为两个相等的对象总会返回相同的哈希码。但是这样做还需负担创建字符串的开销,所以比返回单一值慢。
- (NSUInteger)hash
{
NSString *stringToHash = [NSString stringWithFormat:@”%@:%@:%@”, @”FirstName”, @”LastName”, @(10)];
return [stringToHash hash];
}
3)- (NSUInteger)hash
{
NSUInteger firstNameHash = [@”FirstName” hash];
NSUInteger lastNameHash = [@”LastName” hash];
NSUInteger ageHash = 10;
return firstNameHash ^ lastNameHash ^ ageHash;
}
总结:
collection在检索哈希表(hash table)的时候,使用当前对象的哈希码作为索引,先找到相同的哈希码的对象,在判断该对象是否相等。
a.如果所有对象都返回相同的哈希码,(那么当前对象就要与所有哈希码相同对象进行比较),很容易就产生性能问题。
b.把这种对象放入collection中必须先计算其哈希码,因此也会产生性能问题。
c.能保持较高效率,又能使生成的哈希码至少位于一定范围内,不会过于频繁的重复。但是,此算法生成的哈希码仍然会碰撞(collision),不过至少可以保证哈希码有多重可能的取值。
如果相同哈希码的对象很多,就很容易引起性能问题。编写hash方法是,应当用当前的对象做做实验,以便在减少碰撞频度与降低算法复杂度之间取舍。
4.深度等同性判定(deep equality),不要盲目的逐个检测每条属性,而是应该遗照具体需求来指定检测方案。
5.注意:在容器中放入可变类对象的时候,把某个对象放入到collection之后,就不应再改变其哈希码。如果把某对象放入set之后又修改其内容,那么后面的行为将很难预料。
6.编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
第 9 条 以“类族模式”隐藏实现细节
1.“类族”(class cluster)是一种很有用的pattern,可以隐藏“抽象基类”(abstract class)背后的实现细节,Objective-C的系统框架中普遍使用此模式。该模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类的后面,以保持接口的简洁,用户无需自己创建子类实例,只需调用基类方法来创建即可。
2.类方法class method的作用通常是创建对象,或者获取类的某些全局属性。实例方法instance method则用来操作类的对象。
3.工厂模式Factory Patterm是创建类族的方法之一。
4.Objective-C这门语言没办法指明某个类是abstract,于是开发者通常会在文档中写明类的用法。这种情况下,基类接口一般都没有名为init的成员方法。
5.在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某类的实例,此实例充当”占位数组“(placeholder array)。该数组稍后会转为另一个类的实例,而那个类则是NSArray的实体子类。
6.系统框架中有很多类族,大部分collection类都是类族。
7.向类族中新增实体子类,需要遵守规则:
1)子类应该继承自类族中的抽象基类。
2)子类应该定义自己的数据存储方式。
3)子类应当覆写超类文档中指明需要覆写的方法。
在类族中实现子类时所需遵循的规范一般都会定义于基类的文档之中,编码前应该先看看。
第 10 条 在既有类中使用关联对象存放自定义数据
1.管理关联对象方法
1)void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
此方法以给定的键和策略为某对象设置关联对象值。定义关联对象时,可指定内存管理语义,用以模仿定义属性时做采用的”拥有关系“与”非拥有关系“。
2)id objc_getAssociatedObject(id object, void *key)
此方法根据给定的键从某对象中获取相应的关联对象值。
3)void objc_removeAssociatedObjects(id object)
此方法移除指定对象的全部关联对象。
存取关联对象的值就相当于NSDictionary对象调用[object setObject:value forKey:key]与[object objectForKey:key]方法。然而两者之间有个重要差别:设置关联对象时用的key是个opaque pointer不透明指针(其所指向的数据结构不局限于某种特性类型的指针)。如果在两个键上调用-isEqual:方法的返回值是YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值是,通常使用静态全局变量做键。
2.存储策略由名为objc_AssociationPolicy的枚举所定义。
3.只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。
第 11 条 objc_msgSend的作用
1.C语言使用”静态绑定“static binding,也就是说,在编译期就能决定运行时所应调用的函数。
2.函数地址实际上是硬编码在指令之中的。
3.函数指令调用,运行期才能读取函数地址——>>>需要动态绑定dynamic binding。
4.消息传递机制中的核心函数,叫做objc_msgSend,原型如下:
void objc_msgSend(id self, SEL cmd, …) ———>>>参数个数可变函数,能接受两个或两个以上的参数
self:receiver
cmd:selector(选择子指的就是方法的名字)
… :parameters
此方法需要在接受者所属的类中搜寻其”方法列表“list of methods。如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行message forwarding操作。
5.objc_msgSend会将匹配结果缓存在fast map里面。但是这种快速执行路径fast path还是不如静态绑定的函数调用操作statically bound function call 那样迅速。不过只要把选择子缓存起来了,也就不会慢很多。消息派发message dispatch并非应用程序的瓶颈所在。
6.边界情况edge case:
objc_msgSenf_stret:待发送的消息要返回结构体(CPU寄存器能容纳的下)
objc_msgSend_fpret:消息返回的是浮点数,在某些架构的CPU中调用函数时,需要对”浮点数寄存器floating-point register“做特殊处理。
objc_msgSendSuper:给超类发送消息
7.Objective-C对象的每个方法都可以视为简单的C函数,原型大概如下:
Class_selector(id self, SEL _cmd, …)
每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表时所用的“键”,原型的样子和objc_msgSend函数很像,是为了利用尾调用优化tail-call optimization技术,令“跳至方法实现”这一操作变得更简单些。
8.只有当某函数的最后一个操作仅仅是调用其它函数而不会将其返回值另作他用时,才能执行“尾调用优化”,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”stack trace。以此说明栈回溯backtrace信息中总是出现objc_msgSend的原因。
第 12 条 消息转发机制
1.消息转发分为两个阶段:
第一阶段先征询receiver所属的类,看起是否能动态添加方法,以处理当前这个unknown selector,这叫做动态方法解析dynamic method resolution。
第二阶段涉及完整的消息转发机制full forward mechanism。这又细分为两小步:首先,请接收者看是都有其他对象能处理这条消息,若有,则运行期系统会把消息转给那个对象,消息转发过程结束。若没有replacement receiver,则启动完整的消息转发机制,运行期系统会把鱼消息有关的全部细节封装到NSInvocation对象中。
2.dynamic method resolution
对象在搜到无法解读的方法后,首先调用其所属类的类方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法
+ (BOOL)resolveClassMethod:(SEL)sel;
使用这种方法的前提是:
相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以。
class_addMethod(self, selector, (IMP)autoDictionarySetter,”v@:@”);
(IMP)autoDictionarySette —— >>> C语言语法实现的方法
v@:@ —— >>> 类型编码type encoding
3.replacement receiver
- (id)forwardingTargetForSelector:(SEL)aSelector
若receiver能找到replacement receiver,则将其返回,否则返回nil。如果想在发送给replacement receiver之前先修改消息内容,那就得通过完整的消息转发机制了。
4.full forward mechanism
首先,创建NSInvocation对象,把尚未处理的那条消息有关的全部细节封装于其中,此对象包含selector、target、以及参数。在触发NSInvocation对象时,消息派发系统message-dispatch system把消息指派给目标对象。
调用下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation;
比较有用的实现方法:在触发消息时,先以某种方式改变消息内容。实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。
5.消息转发全流程
步骤越往后,处理消息的代价就越大。
6.在iOS的CoreAnimation框架中,CALayer类就用了相似实现方式,使得CALayer成为“兼容于简直编码的”key-value-coding-complaint容器类,能够随意的添加属性,然后以键值对的形式来访问。
7.example:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
class_addMethod(self, sel, (IMP)test, “v@:@”);
return YES;
}
void test()
{
}
第 13 条 用“方法调配技术method swizzling”调试“黑盒方法”
1.类的方法列表会把selector的名称映射到相关的实现方法之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法以函数指针(IMP)的形式来表示。
2.
void method_exchangeImplementations(Method m1, Method m2)
Method class_getInstanceMethod(Class cls, SEL name)
只有调试程序时才需要在运行期修改方法实现,这种做法不易滥用。
第 14 条 类对象
1.在运行期检视对象类型 — >>> 类型信息查询(introspection,“内省”),这个特性内置于Foundation框架的NSObject协议里。凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。
2.每个Objective-C对象实例都是指向某块内存数据的指针。
3.typedef struct objc_object {
Class isa;
}*id;
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;
}
此结构存放类的“元数据metadata”。此结构体的首个变量也是isa指针,说明class本身亦为Objective-C对象。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类metaclass”,用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的元类。
4.判断出对象是否为某个特定类的实例:
- (BOOL)isMemberOfClass:(Class)aClass;
判断出对象是否为某类或者其派生类的实例:
- (BOOL)isKindOfClass:(Class)aClass;
5.也可以通过比较类对象是否等同的方法来做,那就要使用==操作符。原因在于,类对象是“单例singleton”,在应用程序范围内,每个类的class仅有一个实例。
6.仍应尽量使用introspection,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。
7.某个对象可能会把其收到的所有选择子都转发给另外一个对象。这样的对象叫做“代理proxy”,此种对象均以NXProxy为根类。
第 15 条 用前缀避免命名空间冲突
1.Objective-C没有其他语言那种内置的namespace机制。
2.命名冲突naming clash,那么应用程序的链接过程就会出错 —— duplicate symbol error
3.Apple宣称其保留使用所有“两字母前缀two-letter prefix”的权利。
4.类的实现文件中所用的纯C函数及全局变量,在编译好的目标文件中,这些名称是要算作“顶级符号top-level symbol”的。
5.若自己所开发的程序中用到了第三方库,则应为其中的名称加上前缀。
第 16 条 提供“全能初始化方法”
1.为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”designated initializer,其他初始化方法均应调用此方法。
2.只有在全能初始化方法中,才会存储内部数据。
3.若全能初始化方法与超类不同,则需覆写超类的全能初始化方法。
4.NSCoding协议为例,此协议提供了“序列化机制serialisation mechanism”,对象可依此指明其自身的编码encode以及解码decode方式。Mac OS X的APPKit与iOS的UIKit这两个UI框架都广泛运用此机制,将对象序列化,并保存至XML格式的“NIB”文件中。加载NIB文件通常用来存放视图控制器及其视图布局。加载NIB文件时,系统会在解压缩unarchiving的过程中解码视图控制器。NSCoding协议定义了下面这个初始化方法,遵从该协议者都应实现:
- (instancetype)initWithCoder:(NSCoder *)aDecoder;
5.每个子类的全能初始化方法都应该先调用超类的相关方法,然后再执行与本类有关的任务。
6.如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
7.每个初始化方法的方法名都以init开头。
8.designated initialiser要确保对象的每个实例变量都处在一个有效的状态。
第 17 条 实现description方法
1.用NSDictionary帮助实现description。
2.用NSDictionary来实现此功能可以令代码更易维护:如果以后还要向类中新增属性。并且要在description方法中打印,那么只需要修改字典即可。
3.NSObject协议中还有个方法要注意,那就是debugDescription,此方法的用意与description非常相似。区别在于:debugDescription方法是开发者在调试器debugger中以控制台命令打印对象时才调用。
第 18 条 尽量使用不可变对象
1.尽量创建不可变对象。
2.若某属性仅可用于对象内部修改,则在“class-continuation分类”中将其readonly属性扩展为readwrite属性。
3.不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。
第 19 条 使用清晰而协调的命名方式
1.驼峰式大小写命名法camel casing
2.只有一个词的名字通常用来表示属性。
3.方法命名:
1)如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型。
2)应该把表示参数类型的名词放在参数前面。
3)如果方法要在当前对象上执行,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。
4)不要使用str这样的简称,应该用string这样的全称。
5)Boolean属性前应加is前缀,如果某方法返回非属性的Boolean值,那么应该根据其功能,选择has或is当前缀。
6)将get这个前缀留给那些借由“输出参数”来保存返回值的方法。(输出参数:将值赋给传入的参数)
4.类与协议的命名:应该为类与协议的名称加上前缀,以避免命名空间冲突。
第 20 条 为私有方法名加前缀
1.用前缀把私有方法标出来
2.不应该单用一个下划线做私有方法的前缀,这种做法是预留给苹果公司用的。
第 21 条 Objective-C错误模型
1.如果想生成“异常安全exception safe”的代码,可以通过设置编译器的标志来实现——打开的编译器标志叫做-fobjc-arc-exceptions。
2.如果抛出异常,那么本应在作用域末尾释放的对象就无法自动释放,会导致内存泄露。
3.异常只应该用于极其严重的错误,抛出之后,无需考虑恢复问题,此时应用程序也应该退出。
4.对于其他nonfatal error非致命错误,Objective-C语言所用的编程范式为:令方法返回nil/0,或是使用NSError。以表明其中有错误发生。
5.NSError对象封装了三条信息:
1)Error domain 错误范围,类型为字符串
2)Error code 错误码,类型为整数
3)User info 用户信息,类型为字典
6.在设计API时,NSError的第①种常见用法是通过委托协议来传递此错误。第②种是:经由方法的“输出参数”返回给调用者。
7.在使用ARC时,编译器会把方法签名中的NSError *转换成NSError *autoreleasing,也就是说指针所指的对象会在方法执行完毕后自动释放。
8.空指针解引用会导致段错误segmentation fault并使应用程序崩溃。
第 22 条 NSCopying
- 如果想令自定义类支持拷贝操作,就要实现:
NSCopying协议,该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone;
NSMutableCopying协议,也只有一个方法: - (id)mutableCopyWithZone:(NSZone *)zone;
zone:以前开发程序时,会据此把内存分成不同的zone,而对象会创建在某个区里面。但是现在每个程序只有一个区:default zone。
- (id)copyWithZone:(NSZone *)zone;
- “->”语法:内部使用的实例变量。example:newFriends->_name = [_name copy];
3.在可变对象上调用copy方法会返回另一个不可变类的实例。
Example:
- [NSMutableArray copy] => NSArray
- [NSArray mutableCopy] => NSMutableArray
4.Deep copy:在拷贝对象自身时,将其底层数据也一并复制过去。
5.Foundation框架中的所有collection类在默认情况下都执行浅拷贝。复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
第 23 条 通过委托与数据源协议进行对象间的通信
1.delegate 对象需定义成weak——销毁时自动清空(或者是unsafe unretained——销毁时不自动清空),而非strong,因为两者之间必须为“非拥有关系nonowning relationship”。
2.可选方法,使用@optional关键字。
3.dataSource与delegate可以是两个不同对象,但一般情况下,都用同一个对象来扮演这两种角色。
4.若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一消息缓存至其中。实现缓存功能的所用的代码可以写在delegate属性所对应的设置方法里。
Example:
- (void)setDelegate:(id)delegate
{
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(selectorName)];
//…
}
这样的话,每次调用delegate的相关方法之前,就不用检测委托对象是否能响应给定的selector,而是直接查询结构体里的标志。
第 24 条 将类的实现代码分散到便于管理的数个分类之中
1.使用分类机制把类的实现代码划分成易于管理的小块。
2.将应该视为“私有”的方法归入名叫private的分类中,以隐藏细节。
第 25 条 总是为第三方类的分类名称加前缀
第 26 条 勿在分类中声明属性
1.除了在class-continuation分类之外,其他分类都无法向类中新增实例变量,因为它们无法将属性所需的实例变量合成出来。关联对象能够解决在分类中不能合成实例变量的问题。可行,但是不太理想。正确的做法是把所有属性都定义在主接口里。
2.分类机制应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。
3.属性是用来封装数据的。
第 27 条 使用class-continuation分类隐藏实现细节
1.唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。
2. “.mm”扩展名表示编译器应该将此文件按Objective-C++来编译。
3.在public接口中声明为readonly的属性扩展为readwrite,以便在类的内部设置其值。通常不直接访问实例变量,而是通过设置访问方法来做,这样能够触发Key-Value Observing KVO通知。出现在class-continuation分类或其他分类中的属性必须与同类接口的属性具备相同的attribute。
4.对象所遵从的协议只应视为私有,则可在class-continuation中声明。
第 28 条 通过协议提供匿名对象 anonymous object
1.在字典中,键的标准内存管理语义是“设置是拷贝”,值的语义是“设置时保留”。因此在可变版本的字典中,设置键值对所用的方法的签名是:
- (void)setObject:(id)object forKey:(id)key;
2.协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。
3.使用匿名对象来隐藏类型名称(或类名)。
4.如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可以使用匿名对象来表示。
第 29 条 引用计数
1.从Mac OS X 10.8开始,垃圾收集器garbage collector正式废弃。而iOS从未支持过garbage collector。
2.Retain 递增保留计数;Release 递减保留计数;autorelease 待稍后(通常是在下一次事件循环event loop)清理autorelease pool时,在递减保留计数。
3.如果对象持有指向其他对象的强引用strong reference,那么前者own后者。
4.按引用树回溯,最终会发现一个根对象root object。在Mac OS X 应用程序中就是NSApplication对象,而在iOS应用程序中,则是UIApplication对象。
5.绝不应说保留计数一定是某个值,只能说所执行的操作递增还是递减了该计数。
6.为避免在不经意间使用了无效对象,一般调用完release之后都会清空指针。
[p release];
p = nil;
7.autorelease能延长对象生命周期,使其在跨越方法调用边界后仍然可以存活一段时间。
第 30 条 以ARC简化引用计数
1.使用ARC需要记住的是,引用计数还是要执行的,只不过保留与释放操作是由ARC自动添加的。
2.由于ARC会自动执行retain、release、autorelease、dealloc等操作,所以直接在ARC下调用这些内存管理方法是非法的。
3.以下规则现均由ARC自动管理:
1)若方法名以下列词语开头,则其返回的对象归调用者所有:
alloc、new、copy、mutableCopy。
归调用者所有的意思是:这些操作是的保留计数递增,那么调用者就应当负责抵消这次递增的操作。
2)若方法名不以上述四个词语开头,则表示其返回值的对象并不归调用者所有。在这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效,要是对象多存活一段时间,必须令调用者保留它。
4.ARC包含运行期组件。
5.ARC会把能够互相抵消的retain、release、autorelease操作约简。
Example:
Person *tmp = [Person new];
_myPerson = [tmp retain];
=== >>> ARC在运行期检测到这一对多余的操作,为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊的函数。此时不直接调用对象的autorelease方法,而是改为调用objc_autoreleaseReturnValue。此函数会检视当前方法返回之后即将要执行的那段代码,若发现那段代码要在返回的对象上执行retain操作,则设置全局数据结构中一个标志位,而不执行autorelease操作。与之相似,如果方法返回了一个自动释放的对象,而调用的方法要保留其对象,那么此时不直接执行retain,而是改为执行objc_retainAutoreleaseReturnValue函数。此函数要检测刚才提到的那个标志位,若以置位,则不执行retain操作。设置并检测标志位,要比调用autorelease和retain更快。
6.默认情况下,每个变量都是指向对象的强引用。
7.ARC下的安全设置:先保留新值,在释放旧值,最后设置实例变量。(MRC:若旧值与实例变量已有的值相同,只有当前对象还在引用这个值,那么先释放旧值,就会使得该值的保留计数降为0,导致系统回收,接下来在执行保留操作,就会令应用程序崩溃。)
8.在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:
__strong:默认语义,保留此值。
__unsafe_unretained:不保留此值。不安全。
__weak:不保留此值,但是安全,如果系统将它回收了,变量也会自动清空。
__autoreleasing:把对象pass by reference给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放。
9.ARC会借用Objective-C的一项特性来生成清理例程cleanup routine。回收Objective-C对象时,待回收的对象会调用所有C++对象的析构函数destructor。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct的方法。而ARC则借助此特性,在该方法中生成清理内存所需的代码。
10.ARC只负责管理Objective-C对象的内存。CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。
第 31 条 在dealloc方法中只释放引用并解除监听
1.系统并不保证每个创建出来的对象的dealloc都会执行。
2.不应该随便在dealloc中调用其他方法,因为对象此时in a winding-down state,若对象已摧毁,就会导致很多错误。
3.调用dealloc的方法的那个线程会执行final release,令对象的保留计数降为0。
4.在dealloc里也不要调用属性的存取方法。因为有人可能会覆写这些方法,在其中做一些无法在回收阶段安全执行的操作。此外,属性可能正处于Key-Value Observation监控之下,该属性的observer可能会在属性值改变时retain或使用这个即将回收的对象。
5.在dealloc方法里,应该做的事情就是释放指向其它对象的引用,并取消原来订阅的KVO或者NSNotificationCenter等通知,不要做其他事情。
6.如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法。
7.执行异步任务的方法不应该在dealloc里调用;只能在正常状态下执行的那些方法也不应再dealloc里调用。此时对象已处于deallocating state。
第 32 条 编写“异常安全代码”时留意内存管理问题
1.@finally块:无论是否抛出异常,其中的代码都保证会运行,且只运行一次。捕获异常时,一定要注意将try块内所创立的对象清理干净。
2.-fobjc-arc-exceptions这个编译器标志用来开启异常安全for ARC。默认不开启的原因是:只有当应用程序必须因异常状况而终止时才应抛出异常。应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码是没有意义的。开启编译器标志后,生成代码会导致应用程序变大,而且会降低运行效率。
3.处于C++模式时,编译器会自动打开标志。
4.在发现大量异常捕获操作时,应考虑重构代码。
第 33 条 以弱引用避免保留环
1.只要系统把属性回收,weak属性值就会自动设为nil。而unsafe_unretained属性仍然指向那个已经回收的实例。
2.将引用设为weak,可避免出现保留环。
第 34 条 以“自动释放池块”降低内存峰值
1、释放对象有两种方式:
1)调用release方法,使其保留计数立即递减;
2)调用autorelease方法,将其加入自动释放池中。drain自动释放池时,系统会想其中的对象发送release消息。
2.系统会自动创建一些线程,这些线程都有默认的自动释放池,每次执行事件循环event Loop时,就会将其清空。
3.通常只有一个地方需要创建自动释放池,那就是在main函数里。用自动释放池来包裹应用程序的主入口点main application entry point。
4.自动释放池可以嵌套。
5.内存峰值high-memory waterline是指应用程序在某个特定时间段内的最大内存用量highest memory footprint。
6.NSAutoreleasePool对象更为重量级heavy weight,通常用来创建那种偶尔需要清空的池。
7.@autoreleasepool的好处:每个自动释放池均有其范围,可以避免无意间误用了那些清空池后已为系统所回收的对象;创建更为轻便的自动释放池。
8.自动释放池机制就像stack。系统创建好自动释放池后,就将其入栈。清空时就将其出栈。
9.自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
第 35 条 用“僵尸对象”调试内存管理问题
1.启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象Zombie Object”,而不会真正回收。
2.这种对象所在的核心内存无法重用,因此不可能遭到覆写。
3.僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
4.开启:编辑应用程序的Scheme,在对话框左侧选择Run,然后切换至Diagnostics,最后勾选Enable Zombie Objects。
将NSZombieEnabled环境变量设为YES。在Mac OS X系统中用bash运行应用程序时,可以这么做:
export NSZombieEnabled=“YES”
./app
5.zombie class是在运行期生成的,当首次遇到某类对象要变成zombie objects时,就会创建这样的类。创建过程中用到了运行期程序库里的函数,他们的功能很强大,可以操作类列表。zombie class是从名为NSZombie的模板类里复制出来的。只是充当一个标记。
6.运行期系统如果发现NSZombieEnabled环境变量已设置。那么就把dealloc方法swizzle成僵尸类生成代码。代码的关键之处在于:对象所占内存(通过调用free()方法)释放,因此,这块内存不可复用。虽然内存泄露了,但这只是个调试手段。
7.创建新类的工作由运行期函数objc_duplicateClass()来完成,他会把整个NSZombie类结构拷贝一份,并赋予其新的名字。副本类的超类、实例变量及方法都和复制前相同。还有一种做法也能保留旧类名,就是创建继承自NSZombie的新类,但是用相应函数完成此功能,其效率不如直接拷贝高。
8.NSZombie类及所有从该类拷贝出来的类并未实现任何方法。此类没有超类,是和NSObject一样的root class,该类只有一个实例变量isa,所有的root class都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的全部消息都要经过full forward mechanism。其中_ _ forwarding _ 是核心。它首先要做的事情就包括检查接受消息对象所属的类名。若名称前缀为_NSZombie,则表明消息接收者是僵尸对象,需要特殊处理。
9.系统会修改对象的isa指针,令其指向特殊的zombie class,从而使该对象变成zombie objects。zombie class能够响应所有的selector,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止程序。
第 36 条 不要使用retainCount
1.原因:
1)它所返回的保留计数只是某个给定时间点上的值。绝对保留计数absolute retain count无法反映对象生命期的全貌。
2)retainCount可能永远不返回0。
2.系统会尽可能把NSString实现成单例对象。NSNumber使用了一种叫做标签指着tagged pointer的概念来标注特定类型的数值。这种优化只能在某些场合使用。
3.单例对象,其保留计数绝对不会变。这种对象的保留及释放操作都是空操作no-op。两个单例对象之间,其保留计数也各不相同。
4.不应该总是依赖保留计数的具体值来编码。
5.引入ARC之后,retainCount就正式废止了。
第 37 条 理解“块”
1.多线程编程的核心就是block和Grand Central Dispatch。GCD提供过来对线程的抽象,而这种抽象则基于派发队列dispatch queue。根据系统资源情况,适时地创建、复用、摧毁后台线程background thread,以便处理每个队列。
2.块用“^”(caret)符号来表示,后面跟着一对花括号,括号里面是块的实现代码。
^{
//code…
};
3.块类型的语法结构如下:
return_type (^blockname)(parameters)
4.块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。默认情况下,为块所捕获的变量,是不可以在块里修改的。声明变量时加上__block修饰符,就可以在块内修改了。
5.如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候也会将其一并释放。
6.块总能修改实例变量,所以在声明时无须加__block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量是与self所指带的实例关联在一起。直接访问实例变量和通过self来访问时时等效的。
7.块本身也是对象。
块
void*
isa
int
flags
int
reserved
void()(void,..)
invoke
struct*
descriptor
捕获到的变量
下图为descriptor里的结构描述
块描述符
unsigned long int
reserved
unsigned long int
size
void ()(void ,void*)
copy
void()(void,void*)
dispose
1)在内存布局中,最重要的是invoke变量,这是个函数指针,指向块的实现代码,函数原型至少要接受一个void*型的参数,此参数代表块。块就是一种代替函数指针的语法结构。原来使用函数指针时,需要用不透明的void指针来传递状态。而改用块之后,则可以把原来标准C语言特性所编写的代码封装成简易且易用的接口。
2)descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比如,前者要保留捕获的对象,而后者则将之释放。
3)块还会把它捕获的所有指针变量都拷贝一份。这些拷贝被放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke函数为何需要把块对象作为参数传进来呢?—>>>执行块时,要从内存中把这些捕获到的变量读出来。
8.定义块的时候,其所占的内存区域是分配在栈中的。—— >>>块只在定义它的那个范围内有效。
为了解决这个问题,可给块对象发送copy消息以拷贝,这样就可以把块从栈复制到堆了。一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。若不再使用,ARC下回自动释放,MRC则需要自己调用release释放。当引用计数降为0后,分配在堆上的块heap block会像其他对象一样,为系统所回收,而分配在栈上stack block则无须明确释放,栈内内存本来就会自动回收。
9.全局块global block不会捕捉任何状态,运行时也无须有状态来参与。块所使用的整个区域,在编译期已经确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到的时候于栈中创建。另外,全局块的拷贝是个空操作,因为全局块绝不可能为系统所回收。全局块相当于单例。
全局块:
void (^block)() = ^{
NSLog(@”This is a block”);
};
9.块是C、C++、Objective-C中的词法闭包。可接受参数,也可返回值。
第 38 条 为常用的块类型创建typedef
1.每个块都具备其“固有类型inherent type”,因此可将其赋给释放类型的变量。
2.为了隐藏复杂的块类型,用到C语言中typedef definition的特性,给类型起个易读的别名。
3.最好在使用块类型的类中定义这些typedef,而且还应该把这个类的名字加在typedef所定义的新类型名前面,阐述块的用途。
4.为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需要修改相应typedef中的块签名即可,无须改动其他typedef。
第 39 条 用handler块降低代码分散程度
1.一种常用的范式:异步执行任务perform task asynchronously。好处:处理用户界面的显示及触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程main thread。在某情况下,如果应用程序在一定时间内无响应,就会自动终止。
2.与使用委托模式的代码相比,用块写出来的代码显然更为整洁。异步任务执行完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。而且,由于块声明在创建获取器的范围内里,所以它可以访问范围内的所有变量。
3.
1)可以分别用两个处理程序来处理操作成功、失败的情况。把对应成功和失败的情况分开来写,这将令代码更易读懂。
2)也可以分别把处理成功、失败情况所用代码都封装到同一个块里。
缺点:由于全部逻辑都写在一起,会令代码变得比较长,且比较复。
优点:更为灵活;调用API的代码可能会在处理成功响应的过程中发现错误,需要失败情况按同一方式处理。
4.基于handler来设计API还有个原因:某些代码必须运行在特定的线程上。最好能由调用API的人来决定handler应该运行在哪个线程上。
第 40 条 用块引用其所属对象时不要出现保留环
1.如果块所捕获的对象直接或间接地保留了块本身,那么就得担心保留环问题。
2.一定要找个适当的时机接触保留环,而不是把责任推给API的调用者。
第 41 条 多用派发队列,少用同步锁 ——— 多看两遍
1.如果有多个线程要执行同一份代码,在GCD出现之前,有两种方法:
1)采用内置的同步块synchronisation block,滥用它会降低代码效率。
- (void)synchronizedMethod
{
@synchronized (self) {
//safe
}
}
2)直接使用NSLock对象
NSLock *lock = [NSLock new];
[lock lock];
//safe
[lock unlock];
3)也可以使用NSRecursiveLock这种递归锁recursive lock,线程能够多次持有该锁,而不会出现死锁deadlock现象。
2.串行同步队列serial synchronisation queue能够简单而高效的代替同步块或锁,将读取、写入操作都安排在同一个队列里,即可保证数据同步。
3.设置方法可以改为异步派发。从调用者的角度来看,这个小改动能够提升设置方法的执行速度,而读取操作与写入操作依然会按顺序执行。这么做有个坏处:执行异步派发时,需要拷贝块。若是派发给队列的块要执行的任务更加繁重的任务,就可以考虑这种备选方案。
4.多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行。利用这一特性,并发队列concurrent queue也可以实现。用一个简繁的GCD功能栅栏barrier解决。在队列中,栅栏块总是单独执行,不能与其他块并行,这只对并发队列有意义。因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来要处理的块是个栅栏块barrier block,那么就一直等到当前所有并发块都执行完毕,才会单独执行这个栅栏块,待栅栏块执行过后,再按正常方式继续向下处理。
5.将同步与异步派发结合起来,可以实现与普通机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
6.使用同步队列以及栅栏块,可以令同步行为更加高效。
第 42 条 多用GCD,少用performSelector系列方法
1.performSelector系列方法在内存管理方面容易有疏失,他无法确定将要执行的selector具体是什么,因此ARC编译器也就无法插入释放的内存管理方法。
2.performSelector系列方法所能处理的selector太过局限,selector的返回值类型及发送给方法的参数个数的都收到限制。最主要的替代方案就是使用块。
第 43 条 掌握GCD及操作队列的使用时机
1.操作队列operation queue在GCD之前就有了,其中某些设计原理因操作队列而流行,GCD就是基于这些原理构建的。
2.在两者的诸多差别中,首先要注意:GCD是纯C的API,而操作队列则是Objective-C 的对象。在GCD中,任务用块来表示,而块是个轻量级数据结构。与之相反,操作operation则是个更为重量级的Objective-C对象。
3.使用NSOperation及NSOperationQueue的好处如下:
1)取消某个操作。但是已经启动的任务无法取消。GCD无法取消——fire and forget;
2)指定操作间的依赖关系;
3)通过KVO监控NSOperation对象的属性;
4)指定操作的优先级;GCD也有优先级,不过那是针对整个队列来说的,而不是针对每个块来说的。NSOperation对象也有线程优先级thread priority,这决定了运行此操作的线程处在何种优先级上,用GCD也可以实现,但是采用操作队列更简单,只需要设置一个属性。
5)重用NSOperation对象。这些类可以在代码中多次使用,符合软件开发中的DRY原则——Don’t Repeat Yourself。
4.NSNotificationCenter选用的是操作队列而非派发队列。
第 44 条 通过Dispatch Group机制,根据系统资源状况来执行任务
1.创建组
dispatch_group_t dispatch_group_create(void);
2.
1)把任务编组方法,使用函数变体;
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
2)指定任务所属的dispatch group
voiddispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
调用了相应的enter方法之后,必须有与之对应的leave。
3)等待dispatch group执行完毕: —————————>>>>>>阻塞
longdispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
如果执行dispatch group所需的时间小于timeout,则返回0,否则返回非0值。此参数也可以取常量DISPATCH_TIME_FOREVER,这表示函数会一直等待dispatch group执行完,而不会真的超时time out。
4)传入block,在dispatch group执行完后得到通知 ——————————>>>>>>不阻塞
voiddispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
回调时所选队列根据具体情况来定。可以使用主队列、自定义的串行队列或全局并发队列。
5)此函数会将块反复执行一定次数,每次传给块的参数值都会递增,从0开始,直至iterations - 1。
voiddispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
dispatch_apply会持续阻塞,直到所有的任务都执行完为止。
3.未必总需要使用dispatch group。
第 45 条 dispatch_once
1.使用dispatch_once来执行只需运行一次的线程安全代码。
2.单例模式singleton
3.该函数相关的块必定会执行,且仅执行一次。对于仅执行一次的块来说,每次调用函数时传入的标记都必须完全相同。因此,开发者通常将标记变量生命在static或global作用域里。此函数采取原子访问atomic access来查询标记,以判断其所对应的代码原来是都已经执行过。
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
第 46 条 不要使用dispatch_get_current_queue
1.返回正在执行代码的队列。(现已正式废弃)
dispatch_queue_t dispatch_get_current_queue(void);
2.应该确保同步操作所用的队列绝对不会访问属性。这种队列只应该用来同步属性。由于派发队列是一种极为轻量的机制,所以为了确保每项属性都有专用的同步队列,我们不妨创建多个队列。
3.队列之间会形成一套层级体系,这意味着排在某队列中的块会在上级队列(parent queue,也叫父队列)里执行,层级里地位最高的那个队列总是全局并发队列global concurrent queue。
4.设定队列特有数据queue-specific data,此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于假如根据指定的键取不到关联数据,那么系统就会沿着层级体系向上查找,直至找到数据或者到达根队列为止。
Example:
dispatch_queue_t queueA = dispatch_queue_create(“com.effectiveobjectivec.queueA”, NULL);
dispatch_queue_t queueB = dispatch_queue_create(“com.effectiveobjectivec.queueB”, NULL);
dispatch_set_target_queue(queueB, queueA);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR(“queueA”);
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{NSLog(@”No deadlock”);};
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) {
block();
} else {
dispatch_sync(queueA, block);
}
});
函数原型:
void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);
注意:函数是按指针值来比较键的,而不是按照其内容,所以,队列特定数据的行为与NSDictionary对象不同,后者是比较键的对象同等性。队列特定数据更像是关联引用associated reference。值(在函数原型里叫做context)也是不透明的void指针,于是里面可以存放任意数据。然而,必须管理该对象的内存,这使得在ARC环境下很难使用Objective-C对象作为值,因为ARC并不会自动管理CoreFoundation对象的内存。所以说,这种对象非常适合充当队列特定数据,他们可以根据需要与相关的Objective-C Foundation类无缝衔接。
函数的最后一个参数是析构函数destructor function,对于给定的键来说,当队列所占内存为系统所回收,或者有新的值与键相关联时,原有的值就会移除,而析构函数也会于此时运行。
dispatch_function_t的类型定义如下:
typedef void (dispatch_function_t)(void );
由此可知,析构函数只能带有一个指针参数且返回值必须w为void。
5.dispatch_get_current_queue已经废弃,只应做调试之用。
6.dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。
第 47 条 熟悉系统框架
1.将一系列代码封装为动态库dynamic library,并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。
2.Foundation框架中的类,使用NS这个前缀,此前缀是在Objective-C语言用作NeXTSTEP操作系统的编程语言时首度确定的。
3.NSLinguisticTagger字符串处理
4.CoreFoundation不是Objective-C框架。
5.无缝桥接toll-free bridging,可以把CoreFoundation中的C语言数据结构平滑转换为Foundation中的Objective-C对象,也可以反向转换。
6.除了Foundation与CoreFoundation之外,还有很多系统库,例如:
CFNetWork:此框架提供了C语言级别的网络通信能力,它将“BSD套接字BSD socket”抽象成易于使用的网络接口。
CoreAudio:该框架所提供的C语言API可用来操作设备上的音频硬件。
AVFoundation:此框架所提供的Objective-C对象可用来回放并录制音频及视频
CoreData:此框架所提供的Objective-C对象可将对象放入数据库,便于持久保存。
CoreText:此框架提供的C语言接口可以高效执行文字的排版及渲染操作。
Objective-C编程的特点:经常需要使用底层的C语言级API。
CoreAnimation是QuartzCore框架的一部分。
CoreGraphics框架是以C语言写成的。
MapKit
Social
第 48 条 多用块枚举,少用for循环
1.使用Objective-C 1.0的NSEnumerator来遍历
NSEnumerator是个抽象基类,只定义了两个方法,供具體子類concrete class來實現:
@property (readonly, copy) NSArray *allObjects;
- (nullable ObjectType)nextObject;
其中的关键方法是nextObject,它返回枚举里的下个对象。每次调用该方法时,其内部数据结构都会更新,使得下次调用方法时能返回下个对象。等到枚举中的全部对象都已返回之后,再调用就将返回nil,这表示达到枚举末端了。
好处:
1)其真正的优势在于:不论哪种collection,都可以采用这套类似的语法。
2)使用NSEnumerator还有个好处就是:有多种枚举器enumerator可供使用。比如反向枚举 - reveseObjectEnumerator
2.快速遍历
3.基于块的遍历————快速枚举Fast Enumeration—————如果需要在循环体中添加或删除对象,就不能使用快速枚举,否则程序会抛出异常。
快速遍历为for循环开设了in关键字;
支持快速遍历的对象即支持NSFastEnumeration协议,该协议只定义了一个方法:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;
该方法允许类实例同时返回多个对象,这就使得循环遍历操作更为高效了。
由于NSEnumerator对象也实现了NSFastEnumeration协议,所以能用来执行反向遍历。若要反向遍历数组,可采用下面这种写法:
NSArray anArray = / */;
for (id object in [anArray reverseObjectEnumerator]) {
//do something with object
}
但是这种遍历方式无法轻松获取当前遍历操作所针对的下标
4.基于块的遍历方式
- (void)enumerate…;系列方法
此方法胜过其他方式的地方在于:
遍历时可以直接从块里获取更多信息;
可以修改块签名——因为id类型特殊。
此中遍历方式还有个版本,向其传入选项掩码option mask,可以实现反向遍历:
- (void)enumerate…WithOption:( NSEnumerationOptions)option …;
NSEnumerationOption类型是个enum,其各种取值可用“按位或bitwise OR”连接,用以表明遍历方式。如果当前系统资源状况允许,可以通过NSEnumerationConcurrent选项开启以并发方式执行迭代的块。反向遍历通过NSEnumerationReverse选项来实现。要注意:只有在遍历数组或有序set等有顺序的collection时,这么做才有意义。
第 49 条 对自定义其内存管理语义的collection使用无缝桥接
1.NSArray在CoreFoundation中等价于CFArray。
2.CFArray用过CFArrayRef来引用,这是执向struct__CFArray的指针。CFArrayGetCount可以操作此struct,以获取数组大小。
3.无缝桥接
NSArray *anArray;
CFArrayRef aCFArray = (__bridge CFArrayRef)anArray;
NSLog(@”%li”, CFArrayGetCount(aCFArray));
CFRelease(aCFArray); //用完需要释放其内存
4.__bridge的意思是:ARC仍具备这个Objective-C对象的所有权,而__bridge_retained则与之相反,意味着ARC将交出对象的所有权。与__bridge相似,反向转换通过__bridge_transfer来实现。这三种转换方式称为桥式转换—bridged cast。
5.使用无缝桥接在改变NSDictionary中的键拷贝,值保留语义
Example:
创建CFMutableDicitonary时,可以通过下列方法来指定键和值的内存管理语义:
CFMutableDictionaryRef CFDictionaryCreateMutable(
CFAllocatorRef allocator,
CFIndex capacity,
const CFDictionaryKeyCallBacks *keyCallBacks,
const CFDictionaryValueCallBacks *valueCallBacks
);
首个参数表示将要使用的内存分配器allocator。CoreFoundation对象里的数据结构需要占用内存,而分配器负责分配及回收这些内存。通常传入NULL表示采用默认分配器。
第二个参数定义了字典的初始大小,它并不会限制字典的最大容量,只是向分配器提示一开始应该分配多少内存。
最后两个参数都是指向结构体的指针,它们定义了许多回调函数,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。二者对应的结构体如下:
typedef struct {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
CFDictionaryHashCallBack hash;
} CFDictionaryKeyCallBacks;
typedef struct {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
} CFDictionaryValueCallBacks;
version参数目前应设为0——苹果公司预留——用于检测新版与旧版数据结构之间是否兼容。
结构体中的其余成员都是函数指针,定义了当各种事件发生时应该采用哪个函数来执行相关任务。
retain参数的函数类型定义如下:
typedef const void * (*CFDictionaryRetainCallBack)(CFAllocatorRef allocator, const void *value);
由此可见,这是个函数指针。接受两个参数,开发者可以用一下代码来实现回调函数:
const void * customCallBack(CFAllocatorRef allocator, const void *value){
return value;
}
这么写是把即将加入字典中的值照原样返回。如果用它充当retain回调函数来创建字典,那么该字典就不会保留键与值了。
完整实例:
const *void VCRetainCallBack(CFAllocatorRef allocator, const void *value)
{
return CFRetain(value);
}
const *void VCReleaseCallBack(CFAllocatorRef allocator, const void *value)
{
CFRelease(value);
}
CFDictionaryKeyCallBacks keyCallBacks = {
0,
VCRetainCallBack,
VCReleaseCallBack,
NULL,
CFEqual,
CFHash
};
CFDictionaryValueCallBacks valueCallBacks = {
0,
VCRetainCallBack,
VCReleaseCallBack,
NULL,
CFEqual,
CFHash
};
CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable(NULL, 0, keyCallBacks, valueCallBacks);
NSMutableDictionary *anNSDictionary = (__bridge_transfer NSMutableDictionary)aCFDictionary;
在设定回调函数时,copyDescription取值为NULL,因为采用默认实现就很好。而equal与hash回调函数分别设为CFEqual与CFHash,因为这二者所采用的做法与NSMutableDictionary的默认实现相同。CFEqual最终会调用NSObject的“isEqual:”方法,而CFHash则会调用hash方法,由此可以看出无缝桥接技术更为强大的一面。
。。。。。。以下省略一万字。。。。。
6.在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素。然后可运用无缝桥接技术将其转换成具备日特殊内存管理语义的Objective-C collection。
第 50 条 构建缓存时选用NSCache而非NSDictionary
1.NSCache胜过NSDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。NSCache还会先行删减最久未使用lease recently used对象。
2.NSCache并不会“拷贝”键,而是“保留”它。不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。
3.NSCache是线程安全的:在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问NSCache。对缓存来说,线程安全通常很重要。
4.开发者可以操控缓存删减其内容的时机。有两个与资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的总开销overall cost。仅对NSCache其指导作用。开发者可以在将对象加入缓存时,为其指定开销值(只有在很快计算出开销值的情况下,才应采用这个尺度,若计算复杂,就违背了缓存的本意——增加应用程序响应用户操作的速度)。
[cache setObject:data forKey:@”key” cost:data.length];开销值以字节为单位。
5.NSPurgeableData,此类是NSMutableData的子类,实现了NSDiscardableContent协议。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。NSDiscardableContent协议里定义了名为isContentDiscardable的方法,可以用来查询相关内存是否已释放。如果需要访问某个NSPurgeableData对象,可以调用其beginContentAccess方法,告诉他现在还不应丢弃自己所占据的内存,用完之后,调用endContentAccess方法,告诉他下必要时丢弃自己所占据的内存。这些调用可以嵌套,就像递增递减引用计数所用的方法那样。将NSPurgeableData对象加入NSCache,那么当对象为系统所丢弃时,也会自动从缓存中移除。通过NSCache的evictsObjectsWithDiscasrdedContent属性,可以开启或关闭此功能。创建好NSPurgeableData对象之后,其purge引用计数会多1,因此无需调用beginContentAccess,而后必须调用endContentAccess来抵消。
6.只有那种重新计算起来很费事的数据,才值得放入缓存。
第 51 条 精简initialize与load的实现代码
1.在Objective-C中,绝大多数类都继承自NSObject这个根类,该类中有两个方法,可用来实现初始化操作:
+ (void)load;
1)对与加入运行期系统中的每个class以及category来说,必定调用此方法,而且仅调用一次。若程序是为iOS平台设计的,会在程序启动的时候执行此方法,Mac OS X应用程序更自由一些,它们使用动态加载dynamic loading之类的特性,等应用程序启动好之后再去加载程序库。
2)load方法的问题在于,执行该方法时,运行期系统处于fragile state。以下省略n个字…在load方法中使用其他类是不安全的。
3)load方法并不遵从那套继承规则,如果某个类本身没实现load方法,那么不管其各级超类是否实现此方法,系统都不会调用。此外,分类和其所属的类里,都可能出现load方法,此时两种实现代码都会调用,类的实现要比分类的实现先执行。
4)load方法务必精简。
+ (void)initialize;
对每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。是由运行期系统来调用的,绝不应该通过代码直接调用。
与load的区别:
1)惰性调用。而对于load方法来说,应该程序必须阻塞并等着所有类的load都执行完,才能继续。
2)运行期系统确保该方法一定会在线程安全的环境thread-safe environment中执行。只有执行initialize的那个线程可以操作类或实例。其他线程都要先阻塞,等着initialize执行完。
3)initialize遵循通常的执行规则。
2.必须精简,有助于保持应用程序的响应能力,也能减少引入依赖环interdependency cycle的几率。
3.initialize方法只应该用来设置内部数据。若某个全局状态无法在编译期初始化,则可以放在initialize里来做。
4.整数可以在编译期定义,对象不行。无法再编译期设定的全局常量,可以放在initialize方法里初始化。
第 52 条 NSTimer会保留其目标对象
1.计时器要和run loop相关联。
2.计时器会保留其目标对象target,等到自身失效时在释放此对象。调用invalidate方法可令计时器失效;相关任务执行完之后,一次性计时器也会失效。
3.类对象——单例——无须回收。
4.反复执行任务的计时器repeating timer很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。
5.可以扩充NSTimer的功能,用块来打破保留环。不过,除非NSTimer将来在公共接口里提供此功能,否则必须创建分类,将相关实现代码加入其中。