Effective Objective-C 2.0笔记(接口设计/协议/框架)
Effective Objective-C 2.0笔记(runtime/内存/多线程)
第二章 对象 消息 运行期
第6条:理解“属性”这一概念
属性是OC的一项特性,用于封装对象中的数据(通常数据保存为各种实例变量)。实例变量一般通过存取方法getter和setter方法读取和写入,使用属性,编译器自动编写了相关的存取方法(自动合成),我们也可以自己编写存取方法,但一定要满足命名规范。使用属性,编译器还自动向类中添加了适当类型的实例变量,并在属性名前加_作为实例变量的名称。同时属性引入了“点语法”(点语法其实就是调用了getter和setter方法),我们可以容易的访问存放于属性中的数据。
属性有关的关键字:@synthesize @dynamic
@synthesize可以指定实例变量的名称,@dynamic编译器不会创建实现属性的实例变量,也不会合成存取方法。
属性的特质会影响到存取方法,属性拥有四种特质:
- 原子性:atomic是“原子的”,合成的方法中会加同步锁。nonatomic是“非原子的”,合成方法不会加同步锁。一般情况下使用nonatomic,因为加同步锁意义不大,因为并不能确保线程安全,而且使用同步锁开销较大,会带来性能问题。
- 读写权限:readwrite,属性拥有setter和getter方法;readonly,属性仅拥有获取方法,外部不能更改数据。
- 内存管理语义:assign,setter方法只会执行针对“纯量类型”的简单赋值操作;strong,属性定义了一种拥有关系,setter方法会保留新值,释放旧值,再将新值设置上去;weak,属性定义了一种非拥有关系,setter方法不会保留新值,也不释放旧值,当属性指向的对象销毁时,属性值会自动清空;unsafe_unretained,和weak类似,区别是当属性指向的对象销毁时,属性值不会自动清空(不安全);copy,和strong类似,然而setter方法并不保留新值,而是将其拷贝。当类型有可变和不可变类型时,不可变的属性一般需要使用copy特质,因为设置的新值可能指向可变的类型,如果不用copy,属性值可能在不知情的情况下遭人修改。
- 方法名:getter=<name> 指定获取方法的方法名;setter=<name>指定设置方法的方法名。
第7条:在对象内部尽量直接访问实例变量
直接访问实例变量和通过属性访问实例变量区别:
- 直接访问实例变量速度相对较快,因为不需要经过“方法派发”。编译器所生成的代码会直接访问保存对象实例变量的那块内存。
- 直接访问实例变量,不会调用getter,setter方法,这就绕过了属性所定义的内存管理语义。(修饰属性的strong,copy等没用了)
- 直接访问实例变量,不会触发KVO。
- 通过属性访问实例变量,可以通过getter,setter方法监控属性的调用者和其访问时机,方便调试。
综合以上,折中的选择是:
- 读取数据时,应该直接通过实例变量读取;写入数据时,应该通过属性写入(setter方法)。
- init方法及dealloc方法中,应该直接通过实例变量来读写数据。
- 使用懒加载时,必须通过属性读写数据,而懒加载方法内必须直接通过实例变量读取数据(避免死循环)。
- 使用KVO时,需通过属性读取数据。
第8条:理解“对象等同性”这一概念
关于“等同性”,==操作符比较的是两个指针本身,而不是其所指的对象。比如我们要判断俩个字符串是不是一样的,就不能使用==操作符,而必须使用内建的isEqualToString:方法。NSObject协议提供了两个判断等同性的关键方法:
- (BOOL)isEqual: (id) object;
- (NSUInteger)hash;
我们可以实现协议的这两个方法,实现我们定义的“等同性”,类似于NSString类的isEqualToString:
- isEqual: 方法实现判断相等的条件。
- 如果isEqual: 方法判定两个对象相等(返回YES),那么hash方法也必须返回同一个值。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。最常用的算法是把类的每个判等的属性的哈希值进行异或运算,这样做既能保持较高的效率,又能使生成的哈希码至少位于一定范围内,而不会频繁的重复。
第9条:以类族模式隐藏实现细节
类族(类簇):基类提供创建各子类的方法,并提供接口,子类通过接口实现相应的方法。用户无需自己创建子类实例,只需要调用基类方法创建即可。这样将子类的实现细节隐藏在抽象基类后面。工厂模式是创建类族的方法之一。系统框架中普遍使用类族,例如NSArray(大部分集合类),NSArray的alloc方法获取实例时,该方法首先会分配一个属于某类的实例充当“占位数组”。该数组稍后会转为另一个类的实例,而这个类就是NSArray的实体子类。
又如UIButton:
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
UIButton通过这个类方法创建button,button的类型取决于传入的按钮buttonType,他会返回对应类型的UIButton的子类对象,它们的基类都是UIButton。而具体的每个子类的实现细节,即各种类型button的创建过程不需要我们考虑,我们只需调用基类的方法即可。
类族的一个注意点:类似NSArray alloc创建的对象,其实返回的是对应的子类而非NSArray基类,所以判断创建的对象是否是NSArray类时会有问题:
- (BOOL)isKindOfClass:(Class)aClass; // YES
- (BOOL)isMemberOfClass:(Class)aClass; // NO
第10条:在既有类中使用关联对象存放自定义数据
- 可以通过objc_setAssociatedObject方法给对象关联其他对象,objc_getAssociatedObject获取关联的对象。
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
参数object为关联的对象;key是区分关联对象的“键”,key一般需要声明为static全局变量;policy为存储策略,是用来维护相应的“内存管理语义”,和属性的内存管理语义是等效的。
- 通过关联对象,我们可以在分类中实现添加属性的功能。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难以查找的bug
第11条:理解objc_msgSend的作用
OC是一门消息型语言,在对象上调用方法,就是传递消息的过程。消息有“名称name”或“选择器selector”,可以接受参数而且可能有返回值:消息由接收者,选择子及参数构成。如果向某对象传递消息,那就会使用“动态绑定”机制来决定需要调用的方法,究竟该调用哪个方法完全于运行期决定,甚至在程序运行时改变,这使得OC能成为一门真正的动态语言。发送消息时,编译器会将消息转换为标准的C语言函数调用,这个函数即消息传递机制中的核心函数objc_msgSend,其原型如下:
void objc_msgSend(id self, SEL cmd, ...)
这是个可变参数的函数,能接受两个及以上的参数。第一个参数为接受者,第二个参数cmd为选择器,后续参数为消息本来的那些参数,顺序也一致。
objc_msgSend函数会依据接受者与选择器来调用适当的方法:首先需要在接收者所属的类中搜寻“方法列表”(选择器的名称则是查表所用的key),如果能找到与选择器名称相符的方法,就跳至对应的实现代码。若是找不到,那就沿着继承体系向上查找,找到了合适的方法后再跳转。如果最终找不到,就会执行消息转发操作。
第12条:理解消息转发机制
当对象接收到无法解读的消息后(对象无法响应某个选择器),则进入消息转发流程。
消息转发分为以下阶段:
- 动态方法解析:这时可以通过 + (BOOL)resolveInstanceMethod:(SEL)sel方法动态添加一个方法。
- 备援接收者:当前面一个阶段仍没处理时,会有第二次机会处理未知的选择器,这一步可以通过- (id)forwardingTargetForSelector:(SEL)aSelector返回备援对象将消息转给其他接收者来处理。
- 完整的消息转发:当以上两步都未处理时,那么唯一能做的就是启用完整的消息转发机制。首先通过- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法返回函数签名由后续方法- (void)forwardInvocation:(NSInvocation *)anInvocation执行:NSInvocation对象anInvocation里封装了未处理的消息有关的全部细节(选择器,target及参数)。通过这个方法可以某种方式改变消息内容,如添加参数,更换选择器,替换target(和备援接收者等效)等。
-
当前面几步都未处理消息时,会调用- (void)doesNotRecognizeSelector:(SEL)aSelector以抛出异常,表明选择器最终未能得到处理。
接收者在每一步中都有机会处理消息,步骤越后,处理消息的代价就越大。最好在前面的步骤完成处理。
第13条:黑魔法:“方法调配”(method swizzling)
与给定的选择器名称相对应的方法实现也可以在运行期改变,通过这个特性,我们既不需要源代码也不需要通过继承子类来覆写就能改变这个类的本身功能。这种方案就是“方法调配”(method swizzling)。
类的方法列表会把选择器的名称映射到相关的方法实现上,使得“动态消息派发系统”能够据此找到应该调用的方法,这些方法均以函数指针的形式来表示,这种指针叫做IMP,
id (* IMP) (id, SEL, ...)
OC运行期系统提供了几个方法能够操作这张表:
// 新增方法
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
// 交换方法
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
但是这种方法不宜滥用,若是滥用反而令代码变得不易读懂且难于维护。
第14条:理解“类对象”的用意
每个OC对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个“”。对于通用对象类型id,由于它本身已经是指针了,不用带“”。
描述OC对象使用的数据结构如下:
typedef struct objc_object *id;
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
由此可见,每个对象结构体都有一个指向Class对象的指针isa(通常称为“is a”指针),其定义了对象所属的类,表明其类型,而Class对象则构成了类的继承体系。Class结构如下:
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
结构体存放类的元数据:类的实例变量(ivars),类的实例实现方法(methodLists)等信息。同样,它也有isa指针,这说明Class本身也是OC对象,这个isa指针指向的是类对象所属的类型,叫做“元类”。类方法就定义在“元类”中。super_class定义了本类的超类。
在类继承体系中查询类型信息:
- isMenberOfClass 能够判断对象是否为某个特定类的实例;
- isKindOfClass 能够判断出对象是否为某类或其派生类的实例。
(第9条也有提及)
第五章 内存管理
第29条:理解引用计数
- OC使用引用计数管理内存,引用计数机制通过递增递减的计数器来管理内存。对象创建后,保留计数为1。若保留计数为正,则对象继续存活,保留计数降为0时,对象被销毁
- 对象有三个方法用于操作计数器:
retain
:递增保留计数,release
:递减保留计数,autorelease
:待自动释放池销毁时,再递减保留计数; - MRC下对象调用release,保留计数如果降为0,对象所占内存会被回收;再访问对象可能使程序崩溃;因为对象所占内存dealloc后,只是放回“可用内存池”中;若果对象内存未被覆写,那么该对象仍然有效,程序不会崩溃;反之才会造成崩溃;为了防止访问野指针造成的程序崩溃,在对象release后应手动将指针置nil
[object release];
object = nil;
- 属性存取方法中的内存管理:若属性为“strong”类型,setter方法的处理方式为:保留新值再释放旧值,然后更新实例变量令其指向新值;
- (void)setFoo:(Foo *)foo {
[foo retain];
[_foo release];
_foo = foo;
}
这个顺序极其重要,假如还未保留新值就释放旧值,两个对象又指向了同一个对象,那么先执行的release操作可能导致系统将此对象永久回收,后续的retain操作则无法令这个被彻底回收的对象复生;
- autorelease对象的释放时机:如果创建了自己的自动释放池,对象会在自动释放池销毁时释放(即出了@autoreleasepool { }作用域后释放);否则就是等到当前线程的下一次事件循环(Runloop)时释放,因为一次事件循环会开启新的自动释放池;由此,autorelease能延长对象生命周期;
第30条:以ARC简化引用计数
- ARC会自动执行retain, release,autorelease等内存管理操作,在ARC下调用remain,release,autorelease,dealloc,ratinCount等方法是非法的,编译会报错;
- ARC在调用内存管理方法时,是直接调用底层C语言,这样性能更好,因为保留,释放操作比较频繁,直接调用底层函数能节省很多CPU周期;
- ARC确定的硬性规定:方法名以
alloc
,new
,copy
,mutableCopy
开头的方法创建对象,返回的对象归调用者所有(负责释放对象);若方法名不以这四个词语开头,则表示所返回的对象不归调用者所有,返回的对象会自动释放;实际上就是调用了autorelease方法;因为ARC的这个硬性规定,所以声明属性名称时不能以new等开头,因为这样属性setter方法的内存语意会分不清; - ARC也会执行手工操作无法完成的优化:在编译期,ARC会把能够相互抵消的retain,release,autorelease操作约简,如果发现同一个对象上执行了多次保留与释放操作,ARC有时可以成对移除这两个操作;
- 应用程序中,可用下列修辞符来改变局部变量与实例变量的语义:
__strong
:默认语义,保留值;
__unsafe_unretained
:不保留值,变量不会自动清空,可能不安全,会发生野指针问题;
__weak
:不保留值,但变量会自动清空,是安全的;
__autorelease
:自动释放; - ARC只负责管理OC对象的内存,对于OC对象,ARC会自动生成回收对象所执行的代码,但是对于非OC对象,如CoreFoundation中的对象或由malloc()分配在堆中的内存,那么仍需手动清理。
- (void)dealloc {
CFRelease(_coreFoundationObject);
free(_heapAllocaatedMemoryBlob);
}
第31条:在dealloc方法中只释放引用并解除监听
在dealloc方法中,应该做的是:释放指向其他对象的引用;移除KVO或NSNotificationCenter通知;
第32条:编写“异常安全代码”时留意内存管理问题
异常一般只应在严重错误后才抛出(第21条);
在@try块中,如果先保留了某个对象,然后在释放它之前又抛出异常,除非@catch块能处理该问题,否则就发生内存泄露了;
- MRC模式下解决方法:在@finally块中调用release释放对象,因为@finally块,无论是否抛出异常,代码都会运行;
Object *ojbect;
@try {
object = [[Object alloc] init];
exception
....
}
@catch(NSExpression *expression) {
...
}
@finally {
[object release];
}
- ARC模式,默认情况下ARC不会自动处理这种情况,又不能调用release。对于异常处理更加棘手;解决方法是:通过设置
-fobjec-arc-exceptions
编译标志来开启ARC生成安全处理异常代码的模式;这可以使ARC安全处理异常;
第33条:以弱引用避免保留环(循环引用)
- 循环引用会导致内存泄露,避免循环引用的最佳方式就是弱引用;这种引用表示“非拥有关系”;
- “非拥有关系”的修辞符有
weak
,assign
,unsafe_unretained
;assign一般用于基本类型(int ,float,struct),unsafe_unretained一般用于对象类型,是不安全的;weak也用于对象类型,但是安全的;assign也可以用于对象类型,但也是不安全的;weak不能用于基本类型; - weak之所以安全,是因为变量指向的实例被释放回收时,weak属性会指向nil,而unsafe_unretained属性仍然指向已被回收的对象,这时在访问属性时会发生错误;
- weak属性自动置为nil的原理:weak属性对象会被写入一张哈希表中,以weak属性指向的对象地址为key,weak指针为value;当指向的对象销毁时,会根据对象地址去表中查找weak指针并置为nil;
第34条:以自动释放池降低内存峰值
非alloc,new,copy,mutableCopy词开头方法创建的对象,都是autorelease对象,如果没有手动创建自动释放池,那么autorelease对象要等到下个Runloop后才释放;如果在下个Runloop之前,autorelease对象已经很多(比如for循环创建对象),那么内存峰值将会很高;那么,可以在适当的时机,手动创建autorelease pool使得对象及时释放以降低内存:
for (int i = 0; i < 100000; i ++) {
@autoreleasepool {
Object *obj = [Object ojbect];
}
}
第35条:用“僵尸对象”调试内存管理问题
- 僵尸对象:内存已被回收的对象;指向僵尸对象的指针就是野指针
- 向僵尸对象发送消息是不安全的,有可能造成程序崩溃;崩溃与否,取决于对象所占内存有没有为其他内容所覆写;如果内存未被覆写,那访问仍然有效;如果被覆写了,消息会发送给新对象,新对象可能刚好能应答那访问也有效;(29条有类似描述)
- 可以使用Xcode开启僵尸对象选项,避免访问僵尸对象发生的不安全行为;
- 僵尸对象选项原理:勾选Zombie Object选项后,运行时系统会把已经回收的实例转化为特殊的“僵尸对象”,而不会真正回收它们。没有回收,也就不可能被覆写。僵尸对象收到消息后,会抛出异常,且准确说明发送的消息及描述回收前的那个对象信息;
- Zombie Object运行时系统处理过程:系统在回收对象时,如果发现环境变量设置了Zombie Object选项,那么会把该对象转化为有“僵尸”标识的对象;具体做法是:使用黑魔法swizzle method替换dealloc方法,通过这个dealloc方法动态的创建一个以
_NSZombie_
前缀+原类名为类名的新类,然后将这个新建的类设置为原实例的class,对象就变成了“僵尸”标识的类;_NSZombie_
前缀的新类,并未实现任何方法,所以发给它的消息就要经过“消息转发”,这个过程中将收到的消息及原来所属的类打印出来,抛出异常,程序终止。(整个过程,其实和KVO实现过程有点类似)
第36条:不要使用retainCount
第六章 块(block)与大中枢派发(GCD)
block和GCD是当前Objective-C编程的基石;
第37条:理解“块”这一概念
- block与函数指针类似;
- 在block声明的范围内,所有变量都可以被其捕获;
- 默认情况下,被block捕获的局部变量,是不可以在block内修改的;在声明变量时加上
__block
修饰符,就可以在block修改了; - 块分为
栈块
,堆块
,全局块
;栈块内存分配在栈中,离开相应的范围后,会自动释放,再执行block就会有问题了;栈块可以直接调用copy方法,拷贝至堆里变为堆块;堆块和标准的OC对象一样;
第38条:为常用的块类型创建typedef
@property (nonatomic, copy) void (^callback)(int value);
typedef void (^Block)(int value);
@property (nonatomic, copy) Block callback;
- 以typedef定义块类型,可令block变量用起来更简单,重构block变量时也更方便;
第39条:用handler块降低代码分散程度
- 回调的方式有delegate,block等,相比代理模式的代码相比,block的代码更为整洁,更紧致;
- 在有多个实例需要监控时,采用代理模式,经常需要根据传入的对象来判断;使用block实现,可直接将块与相关对象放在一起;
- 使用block回调时,有成功/失败状态时,建议使用同一个块来处理两种情况;
- 使用block回调时,可以考虑增加一个参数,使调用者可通过这个参数来决定把块安排在哪个队列上执行;
第40条:用块引用其所属对象时不要出现保留环
block可以捕获self变量,如果self强引用了block,block里访问了self,则形成了循环引用;为了避免这种循环引用,可以使用__weak创建一个weakSelf变量,block里使用这个weakSelf;
第41条:多用派发队列,少用同步锁
- 使用 @synchronized(self)以及NSLock等锁,会降低代码效率,而且极端情况下会出现死锁;使用GCD能以更简单,更高效的方式为代码加锁;
_syncqueue dispatch_queue_create("syncqueue", NULL);
- (void)setText:(NSString *)text {
dispatch_sync(_syncqueue, ^{
_text = text;
});
}
- (NSString *)text {
__block NSString *text;
dispatch_sync(_syncqueue, ^{
text = _text;
});
return text;
}
第42条:多用GCD,少用performSelector系列方法
-
performSelector:
是动态执行的; -
performSelector:
方法,最多只能接受两个参数,而且参数类型是对象才行,如果要支持多参数或基本类型参数,就必须将参数打包至字典中,然后调用方法再将其取出来,这样会增加开销而且可能有bug;总之,局限颇多; - 使用
performSelector:
,编译器不知道将要调用的选择器是什么,也就不了解其方法签名及返回值,由于不知道方法名,所以没办法运用ARC的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC不添加释放操作,这样就导致内存泄露了。 - performSelector系列方法所提供的线程功能,GCD都能实现;
第43条:掌握GCD及操作队列的使用时机
主要是GCD和NSOperation的选择:
- GCD是纯C的API,任务是用block表示,block是个轻量级数据结构;
- NSOperation是重量级OC对象,但对象所带来的开销微乎其微;而且NSOperationQueue的好处远远超过其缺点;
- NSOperation能调用cancel方法取消操作(已启动的任务无法取消),GCD无法取消;
- NSOperation可以指定操作间的依赖关系;
- NSOperation可以使用KVO监听属性变化,如
isCancelled
,isFinished
- NSOperation很多地方胜过GCD,很多功能都封装好了了,直接能使用;
第44条:通过Dispatch Group机制,根据系统资源状况执行任务
- 一系列任务可归入一个dispatch group之中,开发者可以在这组任务执行完毕时获得通知;一个使用场景是在并行队列里同时下载多张图片,在所有图片全下载完毕后做界面处理等;
dispatch_group_t group = dispatch_group_create();
for (NSURL *url in self.urls) {
// 并行下载图片
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
[self downloadImage:url];
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 图片全部下载完毕...
});
- 如果所有任务都排在同一个串行队列里,那dispatch group用处不大了;因为串行队列任务本来就是逐个执行的,也即
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
queue一般都是并行队列; - 并行队列中任务的执行,GCD会根据系统资源状况来调度任务,会在适当的时机自动创建新线程或复用旧线程,以达到最优化;
第45条:使用dispatch_once来执行只需运行一次的线程安全代码
- dispatch_once:只会执行一次的线程安全代码;可以简化代码并且彻底保证线程安全;
- dispatch_once常用的用处的是实现单例模式,dispatch_once的实现方式几乎是@synchronized方式的两倍;
第46条:不要使用dispatch_get_current_queue
- dispatch_get_current_queue函数的行为与开发者所预期的不同:派发队列是按层级来组织的,开发者预期的是dispatch_get_current_queue调用API时的那个队列,但实际上返回的却是API内部的另一个层级的队列;
- dispatch_get_current_queue已废弃,只应做调试之用;
关于GCD,之前也总结过:GCD多线程的使用及分析(Swift)