读书笔记:Effective Objective-C

读书笔记:Effective Objective-C

语法

1.Objective-C 的起源

  • Objecitve-C 为 C 语言添加了面向对象特性,是其超集。Objective-C 使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接受一条消息后,执行何种代码由运行期环境而非编译期来决定。

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

  • 除非却有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用 @class 来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
  • 有时无法使用向前声明,比如声明类遵循协议。这种情况可以将协议的声明移至“class-continuation 分类”。

3. 使用字面量语法而不是与之等价的方法

  • 创建字符串、数值、数组、字典。
NSNumber *someNumber = @1;
NSArray *animals = @[@"cat",@"dog",@"mouse"];
NSDictionary *personData = @{ @"firstName":@"Luk",
                              @"lastName":@"Kingyu",
                              @"age":@25 };
  • 通过取下标操作来访问数组下标或字典中键所对应的元素。
NSString *dog = animals[1];
NSString *lastName = personData[@"lastName"];
  • 用字面量语法创建数组或字典时,若值中有nil则会抛出异常,方便排错。

4. 多用类型常量,少用 #define

  • 定义私有常量,在实现文件中定义
static const NSTimeInterval kAnimationDuration = 0.3;
  • 公开常量
// header file
extern NSString *const EOCStringConstant;
// .m file
NSString *const EOCStringConstant = @"VALUE";

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

  • 定义选项时用二进制进行枚举,各选项间可通过“按位或”组合。
  • 用 NS_ENUM 与 NS_OPTIONS 宏来定义枚举类型,可以确保枚举使用开发者所选的底层数据类型实现的。
  • 处理枚举型的switch语句中不要实现 default 分支。这样的话,加入新枚举时编译器会提示补全 switch 分支。

对象、消息、RunTime

6. 属性

  • 用 @property 语法来定义对象中所封装的数据

如果使用了属性,编译器会自动编写访问这些属性的存取方法,此过程称为(自动合成),在编译期执行,同时会在类中添加适当类型的实例变量,以属性名前面添加 ‘_’ 作为实例变量的名字。可以用**@dynamic**关键字阻止编译器合成存取方法。

  • 通过属性特质(attribute)指定存储数据所需的正确语义

属性拥有的特质分为四类

* 原子性:**nonatomic**,属性默认为 atomic 特质,可以保证属性的**可见性。**

开发iOS程序应该使用 nonatomic 特质,atomic 特质会严重影响性能。

* 读写权限

readwrite特质的属性拥有 getter 和 setter

readonly特质的属性仅拥有 getter

* 内存管理语义

assign只会执行简单的赋值操作,针对“纯量类型”。

strong为这种属性设置新值时,setter 会先保留新值,并释放旧值,再把新值设置上去。

weak设置属性时,既不保留新值,也不释放旧值,当所指的对象被释放后,会指向 nil

copy所属关系与 strong 类似,但设置方法不保留新值,而是将其拷贝。只要实现属性所用对象是可变的,就应该在设置新属性时拷贝一份。

unsafe_unretained语义与 assign 相同,但适用于对象类型,当目标对象释放后,属性值不会清空,造成空悬指针,引起程序崩溃。

* 指定存取方法的方法名

getter=

setter=

  • 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。

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

  • 在写实例变量时,通过其 setter 来做,可以确保相关属性的 内存管理语义 正确;在读取实例变量时,则直接访问。
  • 在初始化方法和 dealloc 中,总是应该直接访问实例变量,因为子类可能会覆写 setter。

但某些情况下必须在初始化方法中调用设置方法:待初始化的实例声明在超类中,而我们又无法在子类中直接访问此实例变量,就需要调用 setter。

  • 使用惰性初始化配置某份数据时,必须通过 getter 来访问属性,否则实例变量永远不会初始化。

8. 对象等同性

  • == 比较指针地址是否相同,应该使用 NSObject 中声明的isEqual方法来判断两个对象的等同性。还应提供 hash 方法。
  • 相同的对象必须拥有相同的哈希码,哈希码相同的对象未必相同。
  • 按照具体需求指定检测方案。

自己创建等同性方法因为无须检测参数类型,所以能大大提高检测速度。

  • 编写 hash 方法时,使用计算速度快且哈希碰撞几率低的算法。

hash方法不应该是根据可变部分计算出来的。否则放入 collection 后改变其内容会造成哈希值改变,那么对象所处的位置就是错误的。

9. 以“类簇模式”隐藏实现细节

  • 类簇是一种设计模式,可以隐藏“抽象基类”背后的实现细节。该模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保证接口的简介。用户无须自己创建子类实例,只需调用基类方法创建即可。

工厂模式

  • 系统框架中经常使用类族,大部分 collection 类都是某个类簇的抽象基类。
  • 从类簇的公共抽象基类中继承子类时要当心,若有开发文档应首先阅读。

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

  • 可以通过关联对象机制来把两个对象关联起来;
  • 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
  • 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难以查找的 bug。

11. objc_msgSend 的作用

  • 消息由接受者、选择子及参数构成。给某个对象发送消息就相当于在该对象上调用方法。
objc_msgSend(id self, SEL cmd, ...)

会在接收者所属的类中搜寻其方法列表,如果能找到与 选择子 名称相符的方法,就跳转至其实现代码,若找不到则沿着继承体系向上查找。如果最终还是找不到,则进行消息转发
objc_msgSend 会将匹配结果缓存在“fast map”里,每个类都有这样一块缓存。

利用了“尾调用优化(tail-call optimization)”技术,当某函数的最后一项操作是调用另一个函数且不会将其返回值作他用时,不会向调用堆栈中推入新的栈祯,而是生成跳转至另一函数所需的指令码。

  • 发送给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。

12. 消息转发机制

ObjC 可以在运行期继续向类中添加方法,所以编译期时编译期无法确知类中会不会又某方法实现。当对象接收到无法解读的消息后,就会启动消息转发机制,可以经由此过程告诉对象应该如何处理未知消息。

消息转发分为两大阶段

一:先征询接收者所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”。这叫做动态方法解析;

二:如果运行期系统已经把第一阶段执行完,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时 RunTime 会请求接收者以其他手段处理与消息相关的方法调用。分为两步,首先请接收者看看有没有其他对象能处理这条消息,若有,RumTime 将消息转发给那个对象;若没有则启动完整的消息转发机制,RunTime 会把与消息有关的全部细节封装到 NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析

对象收到无法解读的消息后,首先将调用其所属类的类方法

+ (BOOL)resolveInstanceMethod:(SEL)selector

表示这个类是否能新增一个实例方法用于处理此选择子。可以在这里新增一个处理该选择子的方法。
使用此方法的前提是:相关方法的实现代码已写好,只等着运行的时候动态插在类里面就可以了,此方案常用来实现 @dynamic 属性。

备援接收者

当前接收者还有第二次机会处理未知的选择子,在这一步中,RunTime 会问它:能不能把这条消息转给其他接收者来处理。

- (id)forwardingTargetForSelector:(SEL)selector

若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。
通过此方案可以用 “组合”模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样在外界看来,就像是该对象亲自处理了这些消息。

完整的消息转发

如果消息转发算法来到这一步,就会启动完整的消息转发机制。首先创建 NSInvocation 对象,把与尚未处理的那条消息有关的全部细节封于其中,包含选择子、目标以及参数。消息派发系统把消息派给目标对象。

- (void)forwardInvocation:(NSInvocation *)invocation

该方案可以改变调用目标,使消息在目标上得以调用即可。但这种做法与第二种类似,很少有人这么做。这一步的区别在于可以在触发消息前,以某种方式改变消息的内容,比如追加另一个参数,或是改换选择子等等。
若发现某调用操作不应由本类处理,则需调用同名的超类方法。继承体系的每个类都有机会处理此调用请求。

小结
  • 若对象无法响应某个选择子,则进入消息转发流程;
  • 通过运行期的动态方法解析,可以在需要某个方法时再将其加入类中;
  • 对象可以把无法解读的选择子转交给其他对象处理;
  • 上述两步都无法处理选择子则启动完整的消息转发机制。
  • 接收者在每一步均有机会处理消息,越往后处理消息的代价越大,最好能在第一步处理完,RunTime 就可以将此方法缓存起来,后续再收到相同的选择子就无须启动消息转发流程。

13.用 Method Swizzling 调试“黑盒方法”(谨慎使用)

  • 在运行期,可以向类中新增或替换选择子所对应的方法实现。
  • 使用另一份实现来替换原有的方法实现,可以用此技术向原有实现中添加功能。
  • 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

通过此技术,可以为那些不知道其具体实现的黑盒方法增加日志记录功能,有助于程序调试。

14. 理解“类对象”的用意

类型信息查询特性内置于 NSObject 协议里。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。

声明对象时若指定了具体类型,在该类实例上调用其所没有的方法时,编译器会探知此信息并发出警告。

ObjC 对象所用的数据结构定义在 RunTime 库的头文件里。

对象结构体中的首个成员是 Class 类的变量 isa 指针,定义了对象所属的类。

Class 对象也定义在 RunTime 库的头文件中

此结构体存放类的“元数据”**,**例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体首个变量也是 isa 指针,即 Class 本身也是 ObjC 对象。

结构体里还有变量 super_class,定义了本类的超类。

类对象所属的类型(isa 指针所指向的类型)是另外一个类,叫做**“元类”(metaclass)**,用来表述类对象本身具备的元数据。“类方法”就定义于此处,因为这些方法可以理解为类对象的实例方法。每个类仅有一个“类对象”,每个“类对象”仅有一个与之相关的“元类”。

图片

super_class 指针确立了继承关系,而 isa 指针描述了实例所属的类。通过这张关系图即可执行“类型信息查询”,可以查出对象是否能响应某个选择子,是否遵从某项协议,并能看出对象位于类继承体系的哪一部分。

类型信息查询方法

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

**isMemberOfClass:**判断对象是否为某个特定类的实例。

由于 ObjC 使用动态类型系统,从 collection 中获取对象时,其类型通常是 id,所以需要查询类型信息。

也可使用 == 操作符比较对象的类对象是否等同,但不能使用比较对象时常用的 isEqual: 方法。因为类对象是单例,每个类的 Class 仅有一个实例,所以判断指针地址就可以精确判断出来。

但应该尽量使用类型信息查询方法,因为可以正确处理那些使用了消息传递机制的对象。

比如,某个对象可能会把其收到的所有选择子都转发给另一个对象,这样的对象叫做“代理”,此种对象均以 NSProxy 为根类。
若在此种代理对象上调用 class 方法,返回的是代理对象本身,而非接受代理的对象所属的类。若是改用 isKindOfClass,那么就会把消息转给“接受代理的对象”。也就是说,这条消息的返回值与直接在接受代理的对象上面查询其类型所得的结果相同。因此,这样查出来的类对象与通过 class 方法返回的类对象不同, class 方法返回的类表示发起代理的对象,而非接受代理的对象。

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

接口与 API 设计

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

16. 提供“全能初始化方法”

  • 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法并抛出异常。

17. 实现 description 方法

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

18. 尽量使用不可变对象

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

19. 使用清晰而协调的命名方式

20. 为私有方法名加前缀

21. 理解 Objective-C 错误模型

  • 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。(因为会导致内存泄漏)
  • 在错误不是那么严重的情况下,可以指派“委托方法”来处理错误,

也可以把错误信息放在 NSError 对象里,经由“输出参数”返回给调用者:

- (BOOL)doSomething: (NSError **)error{
    // Dosomething that may cause an error
    if(/* there was an error*/){
        if(error){
            // Pass the error through the out-parameter
            *error = [NSerror errorWithDomain:domain
                                         code:code
                                     userInfo:userInfo]
        }
        return NO;
    } else{
        return YES;
    }
}

传递给方法的参数是个指针,该指针本身又指向另外一个指针,那个指针指向 NSError 对象。这样就能经“输出参数”把 NSError 对象回传给调用者。
用法:

NSError *error = nil;
BOOL ret = [object doSomething:&error];
if(error){
}

实际上,在使用 ARC 时,编译器会把 NSError ** 转换成 NSError * __autoreleasing*,也就是说,指针所指的对象会在方法执行完毕自动释放。即将方法中创建的 NSError 加入 autorelease。

22. 理解 NSCopying 协议

如果想令自己的类支持 copy 操作,就要实现 NSCopying 协议,该协议只有一个方法:

- (id)copyWithZone:(NSZone *)zone;

zone 是历史遗留问题,现在每个程序只有一个“default zone”,可以不用管这个参数。
copy 方法由 NSObject 实现,该方法只是以 default zone 为参数调用 copyWithZone,所以真正该覆写的是 copyWithZone 方法。

如果对象中存在可变的成员,那么拷贝时也应该拷贝这个成员;若是不可变的,就不用担心这个问题,拷贝反而会造成浪费。

通常情况下,可以采用全能初始化方法来初始化待拷贝的对象。有的时候如果全能初始化方法中设置了一个复杂的数据结构,而拷贝后的对象立刻需要用其他数据来覆写,就会造成浪费。

  • 如果自定义的对象分为可变与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
  • 大部分实现 NSCopying 协议的对象都是浅拷贝。如果需要深拷贝,可考虑新增一个专门执行深拷贝的方法。

Foundation 框架下的 collection 默认情况下都执行浅拷贝。

协议与分类

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

  • 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
  • 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法。
  • 当某对象需要从另外一个对象获取数据时,可以使用委托模式。
  • 若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中。

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

  • 使用分类机制把类的实现代码划分成易于管理的小块。
  • 将应该视为“私有”的方法归入名为 Private 的分类中,以隐藏实现细节。

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

将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,分类中的方法就会覆盖原来的实现代码。若个多个分类中有同名方法,则会发生多次覆盖,覆盖结果以最后一个分类为准。

如果像某个类的分类中加入方法,那么在应用程序中,该类的每个实例均可调用这些方法。

  • 向第三方类中添加分类时,总应给其名称加上你专用的前缀,总应给其中的方法加上你专用的前缀。

26. 勿在分类中声明属性

技术上分类可以声明属性,但除了“class-continuation 分类”之外,其他分类无法向类中新增实例变量,因此无法把实现属性所需的实例变量合成出来。

可以用关联对象解决分类中不能合成实例变量的问题。但并不推荐。

  • 把封装数据所用的全部属性定义在主接口中。
  • 在“class-continuation 分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性。

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

  • 通过“class-continuation 分类”向类中新增实例变量。

因为有“稳固的 ABI”机制,使得我们无需知道对象大小即可使用它。

这样做相当于把实例变量隐藏起来。

  • 如果某属性在主接口中声明为“readonly”,而类的内部又要用设置方法修改此属性,那么可以在“class-continuation 分类”中将其扩展为“readwrite”。
  • 把私有方法的原型声明在“class-continuation 分类”中。
  • 若想使类所遵循的协议不为人所知,则可于“class-continuation 分类”中声明。
  • 可以用此模式在实现中使用 C++ 代码,对外展示出一套 ObjC 接口。

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

与其他语言中“匿名类”的概念不同。

id ,可以不用指明具体使用哪个类,只需要这个对象遵循协议就行。

  • 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵循某协议的 id 类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称。
  • 如果具体类型不重要,重要的是对象能够响应特定方法,那么可以用匿名对象来表示。

内存管理

29. 理解引用计数

自动释放池

调用 release 会立刻递减对象的引用计数,调用 autorelease 会稍后递减计数,通常是在下一次“事件循环”时递减,不过也可能会更早一些。

autorelease 可以保证对象在跨越 方法调用边界 后一定存活。

retain cycle

两个对象都持有彼此的强引用,系统将不能销毁这两个对象。

父对象持有子对象的强引用,子对象持有父对象的弱引用,就可以打破循环引用问题。

弱引用,当引用的对象被释放时,弱引用会被自动设置为 nil。

弱变量能够和代理很好地协作。创建一个代理的弱引用,如果代理对象被销毁,变量就会被清零。

  • 引用计数机制通过可以递增递减的计数器来管理内存。
  • 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增和递减引用计数。
  • 通常采用“弱引用”来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。

30. 以 ARC 简化引用计数

使用 ARC 时必须遵循的方法命名规则

以 alloc、new、copy、mutableCopy 词语开头的方法,其返回的对象归调用者所有。即调用上述四种方法的那段代码要负责释放方法返回的对象。

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

ARC 可以在编译期把能够互相抵消的 reatin、release、autorelease 操作约简。可以减少操作 autoreleasePool 的次数,提高性能。

变量的内存管理语义

在应用程序中,可以用下列修饰符来改变局部变量与实例变量的语义:

  • __strong:默认语义,保留其值;
  • __unsafe_unretained:不保留此值,可能不安全,等到再用的时候对象已经回收了,造成指针空悬;
  • __weak:不保留此值,安全,如果对象回收了,变量会设置为 nil

runtime 对注册的类,会进行布局,对于 weak 对象会放入一个 hash 表中。用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是 a,那么就会以 a 为键, 在这个 weak 表中搜索,找到所有以 a 为键的 weak 对象,从而设置为 nil。

  • __autoreleasing:把对象“按引用传递”给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。

属性默认是 unsafe_unretained (相当于 assign)

编译器会保证在 RunLoop 中通过对赋值执行 retain 操作使 strong 属性能够存活下来,assign 和 weak 的属性不会执行这些操作。

  • ARC 环境下,变量的内存管理语义可以通过修饰符说明。
  • 由方法返回的对象,其内存管理语义总是通过方法名来体现。这是必须遵守的规则。
  • ARC 只负责管理 Objective-C 对象的内存。CoreFoundation 对象不归 ARC 管理。

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

  • 在 dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或 NSNotificationCenter 等通知,不要做其他事情。
  • 如果对象持有文件描述符等系统资源,应该专门编写一个方法来释放此种资源。
  • 不要在 dealloc 调用其他方法,此时对象已处于正在回收状态了。

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

  • 默认情况下,ARC 不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,但是会导致应用程序变大,而且会降低运行效率。

33. 以弱引用来避免保留环

一般来说,如果不拥有某对象,那就不要保留它。这条规则对 collection 例外,collection 虽然并不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。有时,对象中的引用会指向另外一个并不归自己所拥有的对象,比如 Delegate 模式。

34. 以 @autoreleasepool 降低内存峰值

自动释放池用于存放那些需要在稍后某时刻释放的对象,清空自动释放池时,系统会向其中的对象发送 release 消息。

主线程或是 GCD 机制中的线程默认都有自动释放池,每次执行“事件循环”时,就会将其清空。因此不需要自己来创建“自动释放池块”。

iOS应用运行在 Runloop 中,为了处理新的事件,系统会创建一个新的自动释放池块,调用到应用中的一些方法用于处理事件,再从方法返回,系统会继续等待下一个事件的发生。

在 main 函数里会用自动释放池来包裹应用程序的主入口点,这个池可以理解成最外围捕捉全部自动释放对象所用的池。

指令围住的语句块定义了自动释放池的上下文,位于自动释放池范围内的对象,会在此范围的末尾收到 release 消息。

自动释放池可以嵌套,可以此降低内存峰值。

  • 自动释放池排布在栈中,系统创建好自动释放池后,就将其推入栈中,而清空自动释放池,就将其出栈。对象收到 autorelease 消息后,系统将其放入栈顶的池里。
  • 监控内存用量,判断是否需要应用自动释放池优化应用程序。

35. 用“僵尸对象”调试内存管理问题

  • 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量 NSZombieEnabled 可开启此功能。
  • 系统会修改对象的 isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有选择子,响应方式为:打印一条包含消息内容及其接收者的信息,然后终止程序。

36. 不要使用 retainCount

ARC 已经将此方法废弃了。即使不开启 ARC 也不要用,因为对象可能处在自动释放池中,其保留计数未必精确,且其他程序库也可能自行保留或释放对象,都会扰乱保留计数的具体数值。

在任何给定时间点上的绝对保留计数都无法反映对象生命期的全貌。

Block 与 GCD

Block 与 GCD 是多线程编程的核心。

37. 理解 Block

详见另一篇博客:Block 的内存管理

38. 为常用的块创建 typedef

typedef int (^SomeBlock)(BOOL flag, int value);
SomeBlock block = ^(BOOL flag,int value){};
  • 可以为同一个块签名定义多个别名,方便重构时增添删减功能。

39. 用 handler 块降低代码分散程度

  • 创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用 handler 块来实现,则可直接将块与相关对象放在一起。
  • 设计 API 时如果用到了 handler 块,可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

40. 用块引用其所属对象时不要出现保留环

  • 如果 Block 所捕获的对象直接或间接地保留了 Block 本身,就要当心保留环问题。
  • 找到适当的时机解除保留环。

41. 多用派发队列,少用同步锁

synchronized(self) 会根据给定的对象自动创建一个锁,这种写法会降低代码效率,粒度太大。

NSLock 与 NSRecuriveLock 效率也不高,遇到死锁会非常麻烦。

GCD能以更简单、高效的形式为代码加锁。
详见另一篇博客:GCD 的使用

  1. 同步、异步决定是否创建子线程,同步任务不创建子线程,都是在主线程中执行,异步任务创建子线程。
  2. 串行、并行决定创建子线程的个数,串行创建一个子线程,并行创建多个子线程(具体几个由系统决定)
    串行队列 异步任务,会创建子线程,且只创建一个子线程,异步任务执行是有序的。
    串行队列 同步任务,不创建新线程,同步任务执行是有序的。
    并行队列 异步任务 创建子线程,且多个子线程,异步任务打印结果无序。
    并行队列 同步任务,不创建新线程,同步任务执行是有序的。

用串行队列实现 getter, setter

_syncQueue = dispatch_queue_create("com.kingyu.syncQueue", NULL);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void) setSomeString:(NSString *)someString {
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

执行异步派发的时候,需要拷贝块,若拷贝块的时间明显超过执行块的时间,就会比同步派发慢。
也可用并发队列实现,需要使用 barrier block。性能会比串行队列快。

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void) setSomeString:(NSString *)someString {
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}
  • 推荐用派发队列表述同步语义,这种做法会比 @synchronized 或 NSLock 更简单且更快。
  • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,且不会阻塞执行异步派发的线程。

42. 多用 GCD,少用 performSelector 系列方法

  • performSelector 系列的方法容易导致内存泄漏。

因为 ARC 无法知道调用的选择子是否具有返回值,或是不知道返回的对象应该由谁来释放,所以 ARC 不会添加 release 操作,这么做就可能会导致内存泄漏,因为方法在返回对象时可能将其保留了。

  • 返回值只能是 void 或 id 类型,若要返回基本数据类型就要进行转换,若返回值为结构体就不能使用该方法,传参时同样有此限制。
  • 如果想把任务放在另一个线程执行,最好不要用 performSelector 系列方法,而是应该把任务封装到 Block 里,然后调用 GCD 的相关方法来实现。
    • 延后执行:dispatch_after
    // Using performSelector
    [self performSelector:@selector(doSomething)
               withObject:nil
               afterDelay:5.0];
    
    // Using dispatch_after
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 5.0 * NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^(void){
        [self doSomething];
    })
* 在另一线程中执行:dispatch_sync 及 dispatch_async
    // Using performSelector
    [self performSelectorOnMainThread:@selector(doSomething)
                           withObject:nil
                        waitUntilDone:NO];
    
    // Using dispatch_async
    dispatch_async(dispatch_get_main_queue(), ^(void){
        [self doSomething];
    })

43. 掌握 GCD 及操作队列的使用时机

GCD 是纯C的API,而 NSOpearationQueue 是 ObjC 的对象。在 GCD 中任务用 Block 来表示,而块是个轻量级的数据结构。但GCD并不总是最佳方案,有时候采用对象所带来的开销微乎其微,并能带来许多好处。

NSOperationQueue

底层是用 GCD 实现的。使用 NSOperation 及 NSOperationQueue的好处:

  • 取消某个操作,可以取消尚未启动的任务,在 NSOperation 对象上调用 cancel 方法,会设置对象内的标志位,用以表示此任务不需执行。而块安排到GCD队列就无法取消了,只能在应用程序层自己实现取消。
  • **指定操作间的依赖关系。**一个操作可以依赖其他多个操作,使特定的操作必须在另外一个操作顺利执行完毕后方可执行。
  • **通过 KVO 监控 NSOperation 对象的属性。**NSOperation 的许多属性都适合通过 KVO 机制来监听,比如可以通过 isCancelled 属性来判断任务是否已取消。可以实现在某个任务变更其状态时得到通知,或是用比 GCD 更精细的方式控制要执行的任务。
  • **指定操作的优先级。**即此操作与队列中其他操作的优先关系。GCD 的优先级是针对整个队列而不是针对每个 Block 的。

NSOperation 对象也有“线程优先级”,只需设置一个属性即可,用GCD也可实现此功能。

  • 重用 NSOpearation 对象。系统内置了一些 NSOperation 的子类,也可以由自己创建。
  • 在解决多线程与任务管理问题时,派发队列并非唯一方案。
  • NSOperationQueue 提供了一套高层的 ObjC API,能实现纯 GCD 所具备的绝大部分功能,而且还能完成一些更复杂的操作。

44. 通过 DIspatch Group 机制,根据系统资源状况来执行任务

dispatch group 能够把任务分组,调用者可以等待这组任务执行完毕,也可以在提供回调函数后继续往下执行,这组任务完成时,调用者会得到通知。

  • 把将要并发执行的多个任务合为一组放入 dispatch group 中,开发者就可以在这组任务执行完毕时获得通知。
    • 使用 dispatch_group_wait 等待 dispatch group 执行完毕,会阻塞线程;
    • 使用 dispatch_group_notify 可以向此函数传入块,等 dispatch group 执行完毕后,块会在特定线程上执行,不会阻塞当前线程。
  • 通过 dispatch group,可以在并发式派发队列里同时执行多项任务,GCD 会根据系统资源来调度这些并发执行任务。

dispatch_apply 可以是实现类似效果,但是dispatch_apply 会持续阻塞,直到所有任务都执行完毕,所以假如把块派给了当前队列,就将导致死锁。若想在后台执行任务,则应使用 dispatch group。

45. 使用 dispatch_once 来执行只需运行一次的线程安全代码

实现单例模式

+ (id) sharedInstance {
    static XXObject *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

dispatch_once_t 是 int 类型的指针,对于只需执行一次的块,每次调用函数传入的标记的必须完全相同,因此通常将标记变量声明在 static 或 globla 作用域里。

46. (似懂非懂)不要使用 dispatch_get_cuurent_queue

iOS 系统从 6.0 版本起已经弃用此函数了。

  • dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已废弃,只应做调试用。
  • 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
  • 应改用 queue-specific data 来解决由不可重入的代码所引发的死锁。

系统框架

47. 熟悉系统框架

  • 最重要的是系统框架是 Foundation 与 CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能。

CoreFoundation 虽不是 ObjC 框架,但可以通过“无缝桥接”(tollfree bridging)把 CoreFoundation 中的 C 语言数据结构转换为 Foundation 中的 ObjC 对象,也可以反向转换。

  • 很多常见任务都能用框架来做,例如音频视频处理、网络通信、数据管理等。
  • 纯 C 写成的框架也很重要。

48. 多用块枚举,少用 for 循环

  • 遍历 collection 有四种方式。最基本的方法是 for 循环,其次是 NSEnumerator 遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”。
    • NSEnumerator 是个抽象基类,其中定义了 nextObject 方法,原理类似于 iterator。
NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [anArray objectEnumerator];
// Dictionary
NSEnumerator *enumerator = [aDictionary keyEnumerator];
// 反向遍历
NSEnumerator *enumerator = [anArray reverseObjectEnumerator];
id object;
while((object = [enumerator nextObject]) != nil){
  // Do something
}
* 快速遍历法需要类遵从 NSFastEnumeration 协议。

块枚举法优势:

遍历时可以直接从块里获取更多信息。

使用者可以向其传入“选项掩码”:NSEnumerationOptions,类型是 enum,其各种取值可用“按位或”连接,用以表明遍历方式。如以并发、反向等方式进行遍历。

  • “块枚举法”本身就能通过 GCD 来并发执行遍历操作,无须另行编写代码。
  • 若提前直到待遍历的 collection 含有何种对象,则应修改块签名,指出对象的具体类型。

49. 对自定义其内存管理语义的 collection 使用无缝桥接

  • 通过无缝桥接,可以在 Foundation 框架中的 ObjC 对象 与 CoreFoundation 框架中的 C 语言数据结构间来回转换。
  • 在 CoreFoundation 层面创建 collection 时,可以指定许多回调函数,这些函数表示此 collection 应如何处理其元素。然后运用无缝桥接,将其转换为具备内存管理语义的 ObjC collection。

Foundation 框架中的 NSDictionary 在向其添加对象时,字典会自动**“拷贝” key“保留” value**,如果用作 key 的对象不支持拷贝操作,就可以用这方法修改字典的内存管理语义。

50. 构建缓存时选用 NSCache 而非 NSDictionary

  • NSCache 可以提供自动删减功能(LRU),而且是“线程安全”的,且它和字典不同,并不会拷贝 key,可以保存不支持拷贝操作的对象。
  • 可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及“总开销 byte”,而这些尺度则定义了缓存删减其中对象的时机。但这些尺度并不是硬限制,仅对NSCache起指导作用。

只有在开销值可以很快得到的情况下,才应该采取这个尺度,比如加入缓存中的是 NSData 对象,就可以采用这个尺度,因为数据大小可以从 NSData 属性中得知。

  • 将 NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,当 NSPurgeableData 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。

NSPurgeableData 是 NSMutableData 的子类,实现了 NSDiscardableContent 协议。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。

如果需要访问某个 NSPurgeableData 对象,可以调用其 beginContentAccess 方法,告诉它现在还不应丢弃所占据的内存,用完后调用 endContentAccess 就可以告诉它在必要时可以丢弃自己所占的内存。这些调用可以嵌套,与引用计数方法类似。

  • 如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种重新计算起来很费劲的数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

51. 精简 initialize 与 load 的实现代码

类必须先执行某些初始化操作才能正常使用。

load 方法

对于加入运行期系统中的每个 类 及 分类来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,即应用程序启动时。如果分类和其所属的类都定义了 load 方法,则先调用类里的,再调用分类里的。

在 load 方法中使用其他类是不安全的,因为无法判断库中各个类的载入顺序,无法确定其他类是不是已经加载好了。

load 方法不会继承,若分类和其所属的类中都实现了 load 方法,那么这些代码都会调用,类的实现要比分类的实现先执行。

load 方法务必实现得精简,因为整个应用程序在执行 load 方法时都会阻塞。不要在里面等待锁会调用可能会加锁的方法。

其主要用途仅在于调试程序,比如判断分类是否正确载入系统中。尽量不要使用这个方法。

由于 load 方法会在类被 import 时调用一次,而这时往往是改变类的行为的最佳时机,可以在分类 load 方法中使用 method swizlling 来修改原有的方法,且load调用的时机总是已知的,是统一的。放在initialize里面的话,子类调用参与替换的方法时,实际上是不会替换的。

initialize 方法

对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。

与 load 区别:

* 惰性调用,只有程序用到了相关的类时,才会调用。对于load来说,应用程序必须阻塞并等着所有类的 load 都执行完才能继续。
* 运行期系统在执行该方法时,是处于正常状态的,此时可以安全使用并调用任意类中的任意方法,运行期系统也能确保 initialize 方法会在线程安全的环境中执行,即只有执行 initialize 的那个线程可以操作类或类实例,其他线程都要先阻塞,等 initialize 执行完。
* initialize 会被继承,如果某个类未实现它,而超类实现了,就会运行超类的实现代码。所以通常应该在里面判断当前要初始化的是哪个类。

initialize 只应该用来设置内部数据如静态变量的设置,不应该在其中调用其他方法,即便是本类自己的方法。

若某个全局状态无法在编译期初始化,则可以放到 initialize 里做,比如 NSMutableArray 实例,是 ObjC 对象,创建实例前必须先激活运行期系统。

  • load 与 initialize 方法都应该实现得精简一些,有助于保持应用程序的响应能力,也能减少引入“依赖环”的几率。
  • 无法在编译期设定的全局变量,可以放在 initizlize 方法里初始化。

52. NSTimer 会保留目标对象

NSTimer 计时器,开发者可以指定绝对的日期和时间,以便到时执行任务,也可以指定执行任务的相对延迟时间。NSTimer 还可以重复运行任务,使用间隔值来指定任务的触发频率。

计时器要和 Runloop 相关联,运行循环到时候会触发任务。可以将其预先安排在当前的运行循环中:

[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#>
                                 target:<#(nonnull id)#>
                               selector:<#(nonnull SEL)#>
                               userInfo:<#(nullable id)#>
                                repeats:<#(BOOL)#>]

target 与 selector 参数表示计时器将在哪个对象上调用哪个方法。

  • 计时器会保留其目标对象,直到计时器本身失效为止。调用 invalidate 方法可令计时器失效。此外,一次性计时器会在执行完相关任务后失效。
  • 反复执行任务的计时器很容易引入保留环,如果这种计时器的目标对象保留了计时器本身,肯定会导致保留环。
  • 可以扩充 NSTimer 的功能,用“块”来打破保留环。不过需要创建分类,将相关实现代码加入其中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值