读《EffectiveObjective‐C》的记录总结 - shenchunxing/better-ios-developer GitHub Wiki

分布图

概念篇

第1条:了解Objective-C的起源

运行期组件

对于消息结构的语言,运行时所执行的代码由运行环境来决定;在运行时才回去查找索要执行的方法。其实现原理是由运行期组件完成(runtime component),使用Objective-C的面向对象特性所需的全部数据结构以及函数都在运行期组件里面。

运行期组件本质上是一种与开发者所编写的代码相链接的动态库(dynamic library),其代码能把开发者所编写的所有程序粘合起来,所以只要更新运行期组件,就可以提升应用程序性能。

内存:对象分配到堆空间,指针分配到栈空间。 分配在队中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。

不含*的变量,可能会使用栈空间。结构体保存非对象类型。

第6条:理解“属性”这一概念

属性用于封装对象中的数据。

1. 存取方法

在设置完属性后,编译器会自动写出一套存取方法,用于访问相应名称的变量:

@interface EOCPerson : NSObject

@property NSString *firstName;
@property NSString *lastName;
@end


@interface EOCPerson : NSObject

- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;

@end

访问属性,可以使用点语法。编译器会把点语法转换为对存取方法的调用:

aPerson.firstName = @"Bob"; // Same as:
[aPerson setFirstName:@"Bob"];

NSString *lastName = aPerson.lastName; // Same as:
NSString *lastName = [aPerson lastName];

如果我们不希望编译器自动生成存取方法的话,需要设置@dynamic 字段:

@interface EOCPerson : NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end

@implementation EOCPerson
@dynamic firstName, lastName;
@end

2. 属性关键字

定义属性的时候,通常会赋予它一些特性,来满足一些对类保存数据所要遵循的需求。

原子性:

  • nonatomic:不使用同步锁
  • atomic:加同步锁,确保其原子性

读写

  • readwrite:同时存在存取方法
  • readonly:只有获取方法

内存管理

  • assign:纯量类型(scalar type)的简单赋值操作
  • strong:拥有关系保留新值,释放旧值,再设置新值
  • weak:非拥有关系(nonowning relationship),属性所指的对象遭到摧毁时,属性也会清空
  • unsafe_unretained :类似assign,适用于对象类型,非拥有关系,属性所指的对象遭到摧毁时,属性不会清空。
  • copy:不保留新值,而是将其拷贝

注意:遵循属性定义

如果属性定义为copy,那么在非设置方法里设定属性的时候,也要遵循copy的语义

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName {
         if ((self = [super init])) {
            _firstName = [firstName copy];
            _lastName = [lastName copy];
        }
       return self;
}

第8条:理解“对象等同性”这一概念

1. 同等性判断

==操作符比较的是指针值,也就是内存地址。

然而有的时候我们只是想比较指针所指向的内容,在这个时候,就需要通过isEqual:方法来比较。

而且,如果已知两个对象是字符串,最好通过isEqualToString:方法来比较。 对于数组和字典,也有isEqualToArray:方法和isEqualToDictionary:方法。

另外,如果比较的对象类型和当前对象类型相同,就可以采用自己编写的判定方法,否则调用父类的isEqual:方法:

- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson {
     //先比较对象类型,然后比较每个属性
     if (self == object) return YES;
     if (![_firstName isEqualToString:otherPerson.firstName])
         return NO;
     if (![_lastName isEqualToString:otherPerson.lastName])
         return NO;
     if (_age != otherPerson.age)
         return NO;
     return YES;
}

- (BOOL)isEqual:(id)object {
    //如果对象所属类型相同,就调用自己编写的判定方法,如果不同,调用父类的isEqual:方法
     if ([self class] == [object class]) {    
         return [self isEqualToPerson:(EOCPerson*)object];
    } else {    
         return [super isEqual:object];
    }
}

2. 深度等同性判定

比较两个数组是否相等的话可以使用深度同等性判断方法:

1.先比较数组的个数 2.再比较两个数组对应位置上的对象均相等。

第11条:理解objc_msgSend的作用

在OC中,如果向某对象传递信息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数.

然而对象收到 消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。

在OC中,给对象发送消息的语法是:

id returnValue = [someObject messageName:parameter];

这里,someObject叫做“接收者(receiver)”,messageName:叫做"选择子(selector)",选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:

void objc_msgSend(id self, SEL cmd, ...)

第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的,所以这个函数就是参数个数可变的函数。

因此,上述以OC形式展现出来的函数就会转化成如下函数:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

这个函数会在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就去实现代码,如果找不到就沿着继承体系继续向上查找。如果找到了就执行,如果最终还是找不到,就执行消息转发操作。

注意:如果匹配成功的话,这种匹配的结果会缓存在“快速映射表”里面。每个类都有这样一块缓存。所以如果将来再次向该类发送形同的消息,执行速度就会更快了。

第12条:理解消息转发机制

如果对象所属类和其所有的父类都无法解读收到的消息,就会启动消息转发机制(message forwarding)。

尤其我们在编写自己的类时,可在消息转发过程中设置挂钩,用以执行预定的逻辑,而不应该使应用程序崩溃。

消息转发分为两个阶段:

  1. 征询接受者,看它能否动态添加方法,以处理这个未知的选择子,这个过程叫做动态方法解析(dynamic method resolution)。

  2. 请接受者看看有没有其他对象能处理这条消息:

    2.1 如果有,则运行期系统会把消息转给那个对象。 2.2 如果没有,则启动完整的消息转发机制(full forwarding mechanism),运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接受者最后一次机会,令其设法解决当前还未处理的这条消息。

图片来自:《Effective Objective-C 》

类方法+(BOOL)resolveInstanceMethod:(SEL)selector:查看这个类是否能新增一个实例方法用以处理此选择子

实例方法- (id)forwardTargetForSelector:(SEL)selector;:询问是否能找到未知消息的备援接受者,如果能找到备援对象,就将其返回,如果不能,就返回nil。

实例方法- (void)forwardInvocation:(NSInvocation*)invocation:创建NSInvocation对象,将尚未处理的那条消息 有关的全部细节都封于其中,在触发NSInvocation对象时,“消息派发系统(message-dispatch system)”就会将消息派给目标对象。

下面来看一个关于动态方法解析的例子:

#import <Foundation/Foundation.h>

@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end

#import "EOCAutoDictionary.h"
#import <objc/runtime.h>
@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end

@implementation EOCAutoDictionary

@dynamic string, number, date, opaqueObject;

- (id)init {
 if ((self = [super init])) {
    _backingStore = [NSMutableDictionary new];
}
   return self;
}

+ (BOOL)resolveInstanceMethod:(SEL)selector {
     NSString *selectorString = NSStringFromSelector(selector);
     if ([selectorString hasPrefix:@"set"]) {
         class_addMethod(self,selector,(IMP)autoDictionarySetter, "v@:@");
     } else {
         class_addMethod(self,selector,(IMP)autoDictionaryGetter, "@@:");
    }
     return YES;
}

在本例中,EOCAutoDictionary类将属性设置为@dynamic,也就是说编译器无法自动为其属性生成set和get方法,因此我们需要动态给其添加set和get方法。

我们实现了resolveInstanceMethod:方法:首先将选择子转换为String,然后判断字符串是否含有set字段,如果有,则增加处理选择子的set方法;如果没有,则增加处理选择子的get方法。其中class_addMethod可以给类动态添加方法。

实现增加处理选择子的get方法:

id autoDictionaryGetter(id self, SEL _cmd) {

     // Get the backing store from the object
     EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
     NSMutableDictionary *backingStore = typedSelf.backingStore;

     // The key is simply the selector name
     NSString *key = NSStringFromSelector(_cmd);

     // Return the value
     return [backingStore objectForKey:key];
}

复制代码

在这里,键的名字就等于方法名,所以在取出键对应的值之前,要将方法名转换为字符串。

实现增加处理选择子的set方法:

void autoDictionarySetter(id self, SEL _cmd, id value) {
     // Get the backing store from the object
     EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
     NSMutableDictionary *backingStore = typedSelf.backingStore;

     /** The selector will be for example, "setOpaqueObject:".
     * We need to remove the "set", ":" and lowercase the first
     * letter of the remainder.
     */
     NSString *selectorString = NSStringFromSelector(_cmd);
     NSMutableString *key = [selectorString mutableCopy];

     // Remove the ':' at the end
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];

     // Remove the 'set' prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];

     // Lowercase the first character
     NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];

     if (value) {
       [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];        
    }
}

因为key的名字对应了属性名,也就是没有set,首字母小写,尾部没有:的字符串。然而,将set方法转换为字符串后,我们需要将set方法的这些“边角”都处理掉。最后得到了“纯净”的键后,再进行字典的赋值操作。

第14条:理解“类对象”的用意

在运行期程序库的头文件里定义了描述OC对象所用的数据结构:

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指针指向了对象所属的类:元类(metaclass),它是整个结构体的第一个变量。super_class定义了本类的超类。

我们也可以向对象发送特定的方法来检视类的继承体系:自身属于哪一类;自身继承与哪一类。

我们使用isMemberOfClass:能够判断出对象是否为某个特定类的实例; 而isKindOfClass:方法能够判断出对象是否为某类或其派生类的实例。

这两种方法都是利用了isa指针获取对象所属的类,然后通过super_class类在继承体系中查询。在OC语言中,必须使用这种查询类型信息的方法才能完全了解对象的真实类型。因为对象类型无法在编译期决定。

尤其注意在集合类里获取对象时,通常要查询类型信息因为这些对象不是强类型的(strongly typed),将它们从集合类中取出来的类型通常是id,也就是能响应任何消息(编译期)。

所以如果我们对这些对象的类型把握不好,那么就会有可能造成对象无法响应消息的情况。因此,在我们从集合里取出对象后,通常要进行类型判断:

- (NSString*)commaSeparatedStringFromObjects:(NSArray*)array {
         NSMutableString *string = [NSMutableString new];
             for (id object in array) {
                    if ([object isKindOfClass:[NSString class]]) {
                            [string appendFormat:@"%@,", object];
                    } else if ([object isKindOfClass:[NSNumber class]]) {
                            [string appendFormat:@"%d,", [object intValue]];
                    } else if ([object isKindOfClass:[NSData class]]) {
                           NSString *base64Encoded = /* base64 encoded data */;
                            [string appendFormat:@"%@,", base64Encoded];
                    } else {
                            // Type not supported
                    }
              }
             return string;
}

第21条:理解Objective-C错误类型

在OC中,我们可以用NSError描述错误。 使用NSError可以封装三种信息:

  • Error domain:错误范围,类型是字符串
  • Error code :错误码,类型是整数
  • User info:用户信息,类型是字典

1. NSError的使用

用法:

1.通过委托协议来传递NSError,告诉代理错误类型。

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

2.作为方法的“输出参数”返回给调用者

- (BOOL)doSomething:(NSError**)error

使用范例:

NSError *error = nil;
BOOL ret = [object doSomething:&error];

if (error) {
    // There was an error
}

2. 自定义NSError

我们可以设置属于我们自己程序的错误范围和错误码

  • 错误范围可以用全局常量字符串来定义。
  • 错误码可以用枚举来定义。
 // EOCErrors.h
    extern NSString *const EOCErrorDomain;
    
    //定义错误码
    typedef NS_ENUM(NSUInteger, EOCError) {
    
        EOCErrorUnknown = –1,
        EOCErrorInternalInconsistency = 100,
        EOCErrorGeneralFault = 105,
        EOCErrorBadInput = 500,
    };

    // EOCErrors.m
    NSString *const EOCErrorDomain = @"EOCErrorDomain"; //定义错误范围

第22条:理解NSCopying协议

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

- (id)copyWithZone:(NSZone*)zone

作者举了个:

- (id)copyWithZone:(NSZone*)zone {
     EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName  andLastName:_lastName];
    copy->_friends = [_friends mutableCopy];
     return copy;
}

之所以是copy->_friends,而不是copy.friends是因为friends并不是属性,而是一个内部使用的实例变量。

1. 复制可变的版本:

遵从协议

而且要执行:

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

注意:拷贝可变型和不可变型发送的是copymutableCopy消息,而我们实现的却是- (id)copyWithZone:(NSZone*)zone- (id)mutableCopyWithZone:(NSZone*)zone 方法。

而且,如果我们想获得某对象的不可变型,统一调用copy方法;获得某对象的可变型,统一调用mutableCopy方法。

例如数组的拷贝:

-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray

2. 浅拷贝和深拷贝

Foundation框架中的集合类默认都执行浅拷贝:只拷贝容器对象本身,而不复制其中的数据。 而深拷贝的意思是连同对象本身和它的底层数据都要拷贝。

作者用一个图很形象地体现了浅拷贝和深拷贝的区别:

图片来自:《Effective Objective-C》

浅拷贝后的内容和原始内容指向同一个对象 深拷贝后的内容所指的对象是原始内容对应对象的拷贝

3. 如何深拷贝?

我们需要自己编写深拷贝的方法:遍历每个元素并复制,然后将复制后的所有元素重新组成一个新的集合。

- (id)initWithSet:(NSArray*)array copyItems:(BOOL)copyItems;

在这里,我们自己提供了一个深拷贝的方法:该方法需要传入两个参数:需要拷贝的数组和是否拷贝元素(是否深拷贝)

- (id)deepCopy {
       EOCPerson *copy = [[[self class] alloc] initWithFirstName:_firstName andLastName:_lastName];
        copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
        return copy;
}

第29条:理解引用计数

尽管在iOS系统已经支持了自动引用计数,但仍然需要开发者了解其内存管理机制。

1. 计数器的操作:

  1. retain:递增保留计数。
  2. release:递减保留计数
  3. autorelease :待稍后清理“自动释放池时”,再递减保留计数。

注意:在对象初始化后,引用计数不一定是1,还有可能大于1。因为在初始化方法的实现中,或许还有其他的操作使得引用计数+1,例如其他的对象也保留了此对象。

有时,我们无法确定在某个操作后引用计数的确切值,而只能判断这个操作是递增还是递减了保留计数。

2. 自动释放池:

将对象放入自动释放池之后,不会马上使其引用计数-1,而是在当前线程的下一次事件循环时递减。

使用举例:如果我们想释放当前需要使用的方法返回值是,可以将其暂时放在自动释放池中:

- (NSString*)stringValue {
     NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
     return [str autorelease];
}

3. 保留循环(retain cycle)

对象之间相互用强引用指向对方,会使得全部都无法得以释放。解决方案是讲其中一端的引用改为弱引用(weak reference),在引用的同时不递增引用计数。

第30条:以ARC简化引用计数


使用ARC,可以省略对于引用计数的操作,让开发者专注于开发本身:

if ([self shouldLogMessage]) {
     NSString *message = [[NSString alloc] initWithFormat:@"I am object, %p", self];
     NSLog(@"message = %@", message);
      [message release]; ///< Added by ARC
}

显然这里我们不需要message对象了,那么ARC会自动为我们添加内存管理的语句。

因此,在ARC环境下调用内存管理语句是非法的:

  • retain
  • release
  • autorelease
  • dealloc

注意:ARC只负责管理OC对象的内存,CoreFoundation对象不归ARC管理

第37条:理解block这一概念

对于“块”的基础知识就不再赘述了,这里强调一下块的种类。

块(Block)分为三类:

  • 栈块
  • 堆块
  • 全局块

1. 栈block

定义块的时候,其所占内存区域是分配在栈中的,而且只在定义它的那个范围内有效:

void (^block)();

if ( /* some condition */ ) {
    block = ^{
     NSLog(@"Block A");
    };

} else {
    block = ^{
     NSLog(@"Block B");
    };
}

block();

上面定义的两个块只在if else语句范围内有效,一旦离开了最后一个右括号,如果编译器覆写了分配给块的内存,那么就会造成程序崩溃。

2. 堆block

为了解决这个问题,我们可以给对象发送copy消息,复制一份到堆里,并自带引用计数:

void (^block)();

if ( /* some condition */ ) {
    block = [^{
         NSLog(@"Block A");
   } copy];
} else {
    block = [^{
         NSLog(@"Block B");
    } copy];
}

block();

3. 全局block

全局块声明在全局内存里,而不需要在每次用到的时候于栈中创建。

void (^block)() = ^{
     NSLog(@"This is a block");
};

第47条:熟悉系统框架

如果我们使用了系统提供的现成的框架,那么用户在升级系统后,就可以直接享受系统升级所带来的改进。

主要的系统框架:

  • Foundation:NSObject,NSArray,NSDictionary等
  • CFoundation框架:C语言API,Foundation框架中的许多功能,都可以在这里找到对应的C语言API
  • CFNetwork框架:C语言API,提供了C语言级别的网络通信能力
  • CoreAudio:C语言API,操作设备上的音频硬件
  • AVFoundation框架:提供的OC对象可以回放并录制音频和视频
  • CoreData框架:OC的API,将对象写入数据库
  • CoreText框架:C语言API,高效执行文字排版和渲染操作

用C语言来实现API的好处:可以绕过OC的运行期系统,从而提升执行速度。

规范篇

第2条: 在类的头文件中尽量少引用其他头文件

有时,类A需要将类B的实例变量作为它公共API的属性。这个时候,我们不应该引入类B的头文件,而应该使用向前声明(forward declaring)使用class关键字,并且在A的实现文件引用B的头文件。

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;//将EOCEmployer作为属性

@end

// EOCPerson.m
#import "EOCEmployer.h"

这样做有什么优点呢:

  • 不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
  • 可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。

但是个别的时候,必须在头文件中引入其他类的头文件:

主要有两种情况:

  1. 该类继承于某个类,则应该引入父类的头文件。
  2. 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中。

第3条:多用字面量语法,少用与之等价的方法

1. 声明时的字面量语法:

在声明NSNumber,NSArray,NSDictionary时,应该尽量使用简洁字面量语法。

NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;

NSArray *animals =[NSArray arrayWithObjects:@"cat", @"dog",@"mouse", @"badger", nil];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};

2. 集合类取下标的字面量语法:

NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下标操作也应该尽量使用字面量语法。

NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];

使用字面量语法的优点:

  1. 代码看起来更加简洁。
  2. 如果存在nil值,则会立即抛出异常。如果在不用字面量语法定义数组的情况下,如果数组内部存在nil,则系统会将其设为数组最后一个元素并终止。所以当这个nil不是最后一个元素的话,就会出现难以排查的错误。

注意: 字面量语法创建出来的字符串,数组,字典对象都是不可变的。

第4条:多用类型常量,少用#define预处理命令

在OC中,定义常量通常使用预处理命令,但是并不建议使用它,而是使用类型常量的方法。 首先比较一下这两种方法的区别:

  • 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
  • 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。

我们可以看出来,使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:不具备类型 + 可以被任意修改,总之给人一种不安全的感觉。

知道了它们的长短处,我们再来简单看一下它们的具体使用方法:

预处理命令:

#define W_LABEL (W_SCREEN - 2*GAP)

这里,(W_SCREEN - 2*GAP)替换了W_LABEL,它不具备W_LABEL的类型信息。而且要注意一下:如果替换式中存在运算符号,以笔者的经验最好用括号括起来,不然容易出现错误(有体会)。

类型常量:

static const NSTimeIntervalDuration = 0.3;

这里: const 将其设置为常量,不可更改。 static意味着该变量仅仅在定义此变量的编译单元中可见。如果不声明static,编译器会为它创建一个外部符号(external symbol)。我们来看一下对外公开的常量的声明方法:

对外公开某个常量:

如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串,那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。

//header file
extern NSString *const NotificationString;

//implementation file
NSString *const  NotificationString = @"Finish Download";

这里NSString *const NotificationString是指针常量。 extern关键字告诉编译器,在全局符号表中将会有一个名叫NotificationString的符号。

我们通常在头文件声明常量,在其实现文件里定义该常量。由实现文件生成目标文件时,编译器会在“数据段”为字符串分配存储空间。

最后注意一下公开和非公开的常量的命名规范:

公开的常量:常量的名字最好用与之相关的类名做前缀。 非公开的常量:局限于某个编译单元(tanslation unit,实现文件 implementation file)内,在签名加上字母k。

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

我们经常需要给类定义几个状态,这些状态码可以用枚举来管理。下面是关于网络连接状态的状态码枚举:

typedef NS_ENUM(NSUInteger, EOCConnectionState) {
  EOCConnectionStateDisconnected,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
};

需要注意的一点是: 在枚举类型的switch语句中不要实现default分支。它的好处是,当我们给枚举增加成员时,编译器就会提示开发者:switch语句并未处理所有的枚举。对此,笔者有个教训,又一次在switch语句中将“默认分支”设置为枚举中的第一项,自以为这样写可以让程序更健壮,结果后来导致了严重的崩溃。

第7条: 在对象内部尽量直接访问实例变量

关于实例变量的访问,可以直接访问,也可以通过属性的方式(点语法)来访问。书中作者建议在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。

直接访问属性的特点:

  • 绕过set,get语义,速度快;

通过属性访问属性的特点:

  • 不会绕过属性定义的内存管理语义
  • 有助于打断点排查错误
  • 可以触发KVO

因此,有个关于折中的方案:

设置属性:通过属性 , 读取属性:直接访问

不过有两个特例:

  1. 初始化方法和dealloc方法中,需要直接访问实例变量来进行设置属性操作。因为如果在这里没有绕过set方法,就有可能触发其他不必要的操作。
  2. 惰性初始化(lazy initialization)的属性,必须通过属性来读取数据。因为惰性初始化是通过重写get方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。

第15条:用前缀 避免命名空间冲突

Apple宣称其保留使用所有"两字母前缀"的权利,所以我们选用的前缀应该是三个字母的。 而且,如果自己开发的程序使用到了第三方库,也应该加上前缀。

第18条:尽量使用不可变对象

书中作者建议尽量把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体做法是:

在头文件中,设置对象属性为readonly,在实现文件中设置为readwrite。这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。

而且,对于集合类的对象,更应该仔细考虑是否可以将其设为可变的。

如果在公开部分只能设置其为只读属性,那么就在非公开部分存储一个可变型。这样一来,当在外部获取这个属性时,获取的只是内部可变型的一个不可变版本,例如:

在公共API中:

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公开的不可变集合

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;

@end

在这里,我们将friends属性设置为不可变的set。然后,提供了来增加和删除这个set里的元素的公共接口。

在实现文件里:

@interface EOCPerson ()

@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

@end

@implementation EOCPerson {
     NSMutableSet *_internalFriends;  //实现文件里的可变集合
}

- (NSSet*)friends {
     return [_internalFriends copy]; //get方法返回的永远是可变set的不可变型
}

- (void)addFriend:(EOCPerson*)person {
    [_internalFriends addObject:person]; //在外部增加集合元素的操作
    //do something when add element
}

- (void)removeFriend:(EOCPerson*)person {
    [_internalFriends removeObject:person]; //在外部移除元素的操作
    //do something when remove element
}

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName {

     if ((self = [super init])) {
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    }
 return self;
} 

我们可以看到,在实现文件里,保存一个可变set来记录外部的增删操作。

这里最重要的代码是:

- (NSSet*)friends {
    return [_internalFriends copy];
}

这个是friends属性的获取方法:它将当前保存的可变set复制了一不可变的set并返回。因此,外部读取到的set都将是不可变的版本。

等一下,有个疑问:

在公共接口设置不可变set 和 将增删的代码放在公共接口中是否矛盾的?

答案:并不矛盾!

因为如果将friends属性设置为可变的,那么外部就可以随便更改set集合里的数据,这里的更改,仅仅是底层数据的更改,并不伴随其他任何操作。 然而有时,我们需要在更改set数据的同时要执行隐秘在实现文件里的其他工作,那么如果在外部随意更改这个属性的话,显然是达不到这种需求的。

因此,我们需要提供给外界我们定制的增删的方法,并不让外部”自行“增删。

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

在给OC的方法取名字的时候要充分利用OC方法的命名优势,取一个语义清晰的方法名!什么叫语义清晰呢?就是说读起来像是一句话一样。

我们看一个例子:

先看名字取得不好的:

//方法定义
- (id)initWithSize:(float)width :(float)height;

//方法调用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithSize:5.0f :10.0f];

这里定义了Rectangle的初始化方法。虽然直观上可以知道这个方法通过传入的两个参数来组成矩形的size,但是我们并不知道哪个是矩形的宽,哪个是矩形的高。 来看一下正确的🌰 :

//方法定义
- (id)initWithWidth:(float)width height:(float)height;

//方法调用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithWidth:5.0f height:10.0f];

这个方法名就很好的诠释了该方法的意图:这个类的初始化是需要宽度和高度的。而且,哪个参数是高度,哪个参数是宽度,看得人一清二楚。永远要记得:代码是给人看的

笔者自己总结的方法命名规则:

每个冒号左边的方法部分最好与右边的参数名一致。

对于返回值是布尔值的方法,我们也要注意命名的规范:

  • 获取”是否“的布尔值,应该增加“is”前缀:

    • isEqualToString:

获取“是否有”的布尔值,应该增加“has”前缀:

- hasPrefix:

第20条:为私有方法名加前缀

建议在实现文件里将非公开的方法都加上前缀,便于调试,而且这样一来也很容易区分哪些是公共方法,哪些是私有方法。因为往往公共方法是不便于任意修改的。

在这里,作者举了个例子:

#import <Foundation/Foundation.h>

@interface EOCObject : NSObject

- (void)publicMethod;

@end


@implementation EOCObject

- (void)publicMethod {
 /* ... */
}

- (void)p_privateMethod {
 /* ... */
}

@end

注意: 不要用下划线来区分私有方法和公共方法,因为会和苹果公司的API重复。

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

如果给委托对象发送消息,那么必须提前判断该委托对象是否实现了该消息:

NSData *data = /* data obtained from network */;

if ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)])
{
        [_delegate networkFetcher:self didReceiveData:data];
}

而且,最好再加上一个判断:判断委托对象是否存在

NSData *data = /* data obtained from network */;

if ( (_delegate) && ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)]))
{
        [_delegate networkFetcher:self didReceiveData:data];
}

对于代理模式,在iOS中分为两种:

  • 普通的委托模式:信息从类流向委托者
  • 信息源模式:信息从数据源流向类

普通的委托 | 信息源

就好比tableview告诉它的代理(delegate)“我被点击了”;而它的数据源(data Source)告诉它“你有这些数据”。仔细回味一下,这两个信息的传递方向是相反的。

第24条:将类的实现代码分散到便于管理的数个分类中

通常一个类会有很多方法,而这些方法往往可以用某种特有的逻辑来分组。我们可以利用OC的分类机制,将类的这些方法按一定的逻辑划入几个分区中。

例子:

无分类的类:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;

/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;


/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;


/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;
@end

分类之后:

#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;

@end

@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end

@interface EOCPerson (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end

@interface EOCPerson (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

其中,FriendShip分类的实现代码可以这么写:

// EOCPerson+Friendship.h
#import "EOCPerson.h"


@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end


// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"


@implementation EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person {
 /* ... */
}

- (void)removeFriend:(EOCPerson*)person {
 /* ... */
}

- (BOOL)isFriendsWith:(EOCPerson*)person {
 /* ... */
}

@end

注意:在新建分类文件时,一定要引入被分类的类文件。

通过分类机制,可以把类代码分成很多个易于管理的功能区,同时也便于调试。因为分类的方法名称会包含分类的名称,可以马上看到该方法属于哪个分类中。

利用这一点,我们可以创建名为Private的分类,将所有私有方法都放在该类里。这样一来,我们就可以根据private一词的出现位置来判断调用的合理性,这也是一种编写“自我描述式代码(self-documenting)”的办法。

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

分类机制虽然强大,但是如果分类里的方法与原来的方法名称一致,那么分类的方法就会覆盖掉原来的方法,而且总是以最后一次被覆盖为基准。

因此,我们应该以命名空间来区别各个分类的名称与其中定义的方法。在OC里的做法就是给这些方法加上某个共用的前缀。例如:

@interface NSString (ABC_HTTP)

// Encode a string with URL encoding
- (NSString*)abc_urlEncodedString;

// Decode a URL encoded string
- (NSString*)abc_urlDecodedString;

@end

因此,如果我们想给第三方库或者iOS框架里的类添加分类时,最好将分类名和方法名加上前缀。

第26条:勿在分类中声明属性

除了实现文件里的class-continuation分类中可以声明属性外,其他分类无法向类中新增实例变量。

因此,类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量的地方。

关于分类,需要强调一点:

分类机制,目标在于扩展类的功能,而不是封装数据。

第27条:使用class-continuation分类 隐藏实现细节

通常,我们需要减少在公共接口中向外暴露的部分(包括属性和方法),而因此带给我们的局限性可以利用class-continuation分类的特性来补偿:

  • 可以在class-continuation分类中增加实例变量。
  • 可以在class-continuation分类中将公共接口的只读属性设置为读写。
  • 可以在class-continuation分类中遵循协议,使其不为人知。

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

永远不要自己调用dealloc方法,运行期系统会在适当的时候调用它。根据性能需求我们有时需要在dealloc方法中做一些操作。那么我们可以在dealloc方法里做什么呢?

  • 释放对象所拥有的所有引用,不过ARC会自动添加这些释放代码,可以不必操心。
  • 而且对象拥有的其他非OC对象也要释放(CoreFoundation对象就必须手动释放)
  • 释放原来的观测行为:注销通知。如果没有及时注销,就会向其发送通知,使得程序崩溃。

举个简单的🌰 :

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

尤其注意:在dealloc方法中不应该调用其他的方法,因为如果这些方法是异步的,并且回调中还要使用当前对象,那么很有可能当前对象已经被释放了,会导致崩溃。

并且在dealloc方法中也不能调用属性的存取方法,因为很有可能在这些方法里还有其他操作。而且这个属性还有可能处于键值观察状态,该属性的观察者可能会在属性改变时保留或者使用这个即将回收的对象。

第36条:不要使用retainCount

在非ARC得环境下使用retainCount可以返回当前对象的引用计数,但是在ARC环境下调用会报错,因为该方法已经被废弃了 。

它被废弃的原因是因为它所返回的引用计数只能反映对象某一时刻的引用计数,而无法“预知”对象将来引用计数的变化(比如对象当前处于自动释放池中,那么将来就会自动递减引用计数)。

第46条:不要使用dispatch_get_current_queue

我们无法用某个队列来描述“当前队列”这一属性,因为派发队列是按照层级来组织的。

那么什么是队列的层级呢?

队列的层及分布

安排在某条队列中的快,会在其上层队列中执行,而层级地位最高的那个队列总是全局并发队列。

在这里,B,C中的块会在A里执行。但是D中的块,可能与A里的块并行,因为A和D的目标队列是并发队列。

正因为有了这种层级关系,所以检查当前队列是并发的还是非并发的就不会总是很准确。

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

当遍历集合元素时,建议使用块枚举,因为相对于传统的for循环,它更加高效,而且简洁,还能获取到用传统的for循环无法提供的值:

我们首先看一下传统的遍历:

传统的for遍历

NSArray *anArray = /* ... */;
for (int i = 0; i < anArray.count; i++) {
   id object = anArray[i];
   // Do something with 'object'
}

// Dictionary
NSDictionary *aDictionary = /* ... */;
NSArray *keys = [aDictionary allKeys];
for (int i = 0; i < keys.count; i++) {
   id key = keys[i];
   id value = aDictionary[key];
   // Do something with 'key' and 'value'
}

// Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i++) {
   id object = objects[i];
   // Do something with 'object'
}

我们可以看到,在遍历NSDictionary,和NSet时,我们又新创建了一个数组。虽然遍历的目的达成了,但是却加大了系统的开销。

利用快速遍历:

NSArray *anArray = /* ... */;
for (id object in anArray) {
 // Do something with 'object'
}

// Dictionary
NSDictionary *aDictionary = /* ... */;
for (id key in aDictionary) {
 id value = aDictionary[key];
 // Do something with 'key' and 'value'

}


NSSet *aSet = /* ... */;
for (id object in aSet) {
 // Do something with 'object'
}

这种快速遍历的方法要比传统的遍历方法更加简洁易懂,但是缺点是无法方便获取元素的下标。

利用基于block的遍历:

NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop){
   // Do something with 'object'
   if (shouldStop) {
      *stop = YES; //使迭代停止
  }

}];


“// Dictionary
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop){
     // Do something with 'key' and 'object'
     if (shouldStop) {
        *stop = YES;
    }
}];


// Set
NSSet *aSet = /* ... */;
[aSet enumerateObjectsUsingBlock:^(id object, BOOL *stop){
     // Do something with 'object'
     if (shouldStop) {
        *stop = YES;
    }

我们可以看到,在使用块进行快速枚举的时候,我们可以不创建临时数组。虽然语法上没有快速枚举简洁,但是我们可以获得数组元素对应的序号,字典元素对应的键值,而且,我们还可以随时令遍历终止。

利用快速枚举和块的枚举还有一个优点:能够修改块的方法签名

for (NSString *key in aDictionary) {
         NSString *object = (NSString*)aDictionary[key];
        // Do something with 'key' and 'object'
}

NSDictionary *aDictionary = /* ... */;

    [aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop){

             // Do something with 'key' and 'obj'

}];

如果我们可以知道集合里的元素类型,就可以修改签名。这样做的好处是:可以让编译期检查该元素是否可以实现我们想调用的方法,如果不能实现,就做另外的处理。这样一来,程序就能变得更加安全。

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

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

在构建缓存的时候很多人习惯用NSDictionary或者NSMutableDictionary,但是作者建议大家使用NSCache,它作为管理缓存的类,有很多特点要优于字典,因为它本来就是为了管理缓存而设计的。

NSCache优于NSDictionary的几点:

  • 当系统资源将要耗尽时,NSCache具备自动删减缓冲的功能。并且还会先删减“最久未使用”的对象。
  • NSCache不拷贝键,而是保留键。因为并不是所有的键都遵从拷贝协议(字典的键是必须要支持拷贝协议的,有局限性)。
  • NSCache是线程安全的:不编写加锁代码的前提下,多个线程可以同时访问NSCache。

关于操控NSCache删减内容的时机

开发者可以通过两个尺度来调整这个时机:

  • 缓存中的对象总数.
  • 将对象加入缓存时,为其指定开销值。

对于开销值,只有在能很快计算出开销值的情况下,才应该考虑采用这个尺度,不然反而会加大系统的开销。

下面我们来看一下缓存的用法:缓存网络下载的数据

// Network fetcher class
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;

@end

// Class that uses the network fetcher and caches results
@interface EOCClass : NSObject
@end

@implementation EOCClass {
     NSCache *_cache;
}

- (id)init {

     if ((self = [super init])) {
    _cache = [NSCache new];

     // Cache a maximum of 100 URLs
    _cache.countLimit = 100;


     /**
     * The size in bytes of data is used as the cost,
     * so this sets a cost limit of 5MB.
     */
    _cache.totalCostLimit = 5 * 1024 * 1024;
    }
 return self;
}



- (void)downloadDataForURL:(NSURL*)url { 

     NSData *cachedData = [_cache objectForKey:url];

     if (cachedData) {

         // Cache hit:存在缓存,读取
        [self useData:cachedData];

    } else {

         // Cache miss:没有缓存,下载
         EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];      

        [fetcher startWithCompletionHandler:^(NSData *data){
         [_cache setObject:data forKey:url cost:data.length];    
        [self useData:data];
        }];
    }
}
@end

在这里,我们使用URL作为缓存的key,将总对象数目设置为100,将开销值设置为5MB。

NSPurgeableData

NSPurgeableData是NSMutableData的子类,把它和NSCache配合使用效果很好。

因为当系统资源紧张时,可以把保存NSPurgeableData的那块内存释放掉。

如果需要访问某个NSPurgeableData对象,可以调用beginContentAccess方发,告诉它现在还不应该丢弃自己所占据的内存。

在使用完之后,调用endContentAccess方法,告诉系统在必要时可以丢弃自己所占据的内存。

上面这两个方法类似于“引用计数”递增递减的操作,也就是说,只有当“引用计数”为0的时候,才可以在将来删去它所占的内存。

- (void)downloadDataForURL:(NSURL*)url { 

      NSPurgeableData *cachedData = [_cache objectForKey:url];

      if (cachedData) {         

            // 如果存在缓存,需要调用beginContentAccess方法
            [cacheData beginContentAccess];

             // Use the cached data
            [self useData:cachedData];

             // 使用后,调用endContentAccess
            [cacheData endContentAccess];


        } else {

                 //没有缓存
                 EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];    

                  [fetcher startWithCompletionHandler:^(NSData *data){                         NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
                         [_cache setObject:purgeableData forKey:url cost:purgeableData.length];

                          // Don't need to beginContentAccess as it begins            
                          // with access already marked
                           // Use the retrieved data
                            [self useData:data];

                             // Mark that the data may be purged now
                            [purgeableData endContentAccess];

            }];
      }
}

注意:

在我们可以直接拿到purgeableData的情况下需要执行beginContentAccess方法。然而,在创建purgeableData的情况下,是不需要执行beginContentAccess,因为在创建了purgeableData之后,其引用计数会自动+1;

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

load方法

+(void)load;

每个类和分类在加入运行期系统时,都会调用load方法,而且仅仅调用一次,可能有些小伙伴习惯在这里调用一些方法,但是作者建议尽量不要在这个方法里调用其他方法,尤其是使用其他的类。原因是每个类载入程序库的时机是不同的,如果该类调用了还未载入程序库的类,就会很危险。

initialize方法

+(void)initialize;

这个方法与load方法类似,区别是这个方法会在程序首次调用这个类的时候调用(惰性调用),而且只调用一次(绝对不能主动使用代码调用)。

值得注意的一点是,如果子类没有实现它,它的超类却实现了,那么就会运行超类的代码:这个情况往往很容易让人忽视。

看一下🌰 :

#import <Foundation/Foundation.h>

@interface EOCBaseClass : NSObject
@end

@implementation EOCBaseClass
+ (void)initialize {
 NSLog(@"%@ initialize", self);
}
@end

@interface EOCSubClass : EOCBaseClass
@end

@implementation EOCSubClass
@end

当使用EOCSubClass类时,控制台会输出两次打印方法:

EOCBaseClass initialize
EOCSubClass initialize

因为子类EOCSubClass并没有覆写initialize方法,那么自然会调用其父类EOCBaseClass的方法。 解决方案是通过检测类的类型的方法:

+ (void)initialize {
   if (self == [EOCBaseClass class]) {
       NSLog(@"%@ initialized", self);
    }
}

这样一来,EOCBaseClass的子类EOCSubClass就无法再调用initialize方法了。 我们可以察觉到,如果在这个方法里执行过多的操作的话,会使得程序难以维护,也可能引起其他的bug。因此,在initialize方法里,最好只是设置内部的数据,不要调用其他的方法,因为将来可能会给这些方法添加其它的功能,那么会可能会引起难以排查的bug。

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

在使用NSTimer的时候,NSTimer会生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那么就会生成保留环。

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end


@implementation EOCClass {
     NSTimer *_pollTimer;
}


- (id)init {
     return [super init];
}


- (void)dealloc {
    [_pollTimer invalidate];
}


- (void)stopPolling {

    [_pollTimer invalidate];
    _pollTimer = nil;
}


- (void)startPolling {
   _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                 target:self
                                               selector:@selector(p_doPoll)
                                               userInfo:nil
                                                repeats:YES];
}

- (void)p_doPoll {
    // Poll the resource
}

@end

在这里,在EOCClass和_pollTimer之间形成了保留环,如果不主动调用stopPolling方法就无法打破这个保留环。像这种通过主动调用方法来打破保留环的设计显然是不好的。

而且,如果通过回收该类的方法来打破此保留环也是行不通的,因为会将该类和NSTimer孤立出来,形成“孤岛”:

孤立了类和它的NSTimer

这可能是一个极其危险的情况,因为NSTimer没有消失,它还有可能持续执行一些任务,不断消耗系统资源。而且,如果任务涉及到下载,那么可能会更糟。。

那么如何解决呢? 通过“块”来解决!

通过给NSTimer增加一个分类就可以解决:

#import <Foundation/Foundation.h>

@interface NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                         repeats:(BOOL)repeats;
@end



@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                        repeats:(BOOL)repeats
{
             return [self scheduledTimerWithTimeInterval:interval
                                                  target:self
                                                selector:@selector(eoc_blockInvoke:)
                                                userInfo:[block copy]
                                                 repeats:repeats];

}


+ (void)eoc_blockInvoke:(NSTimer*)timer {
     void (^block)() = timer.userInfo;
         if (block) {
             block();
        }
}
@end

我们在NSTimer类里添加了方法,我们来看一下如何使用它:

- (void)startPolling {

         __weak EOCClass *weakSelf = self;    
         _pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{

               EOCClass *strongSelf = weakSelf;
               [strongSelf p_doPoll];
          }

                                                          repeats:YES];
}

在这里,创建了一个self的弱引用,然后让块捕获了这个self变量,让其在执行期间存活。

一旦外界指向EOC类的最后一个引用消失,该类就会被释放,被释放的同时,也会向NSTimer发送invalidate消息(因为在该类的dealloc方法中向NSTimer发送了invalidate消息)。

而且,即使在dealloc方法里没有发送invalidate消息,因为块里的weakSelf会变成nil,所以NSTimer同样会失效。

技巧篇

第9条 以“类族模式“隐藏实现细节

在iOS开发中,我们也会使用“类族”(class cluster)这一设计模式,通过“抽象基类”来实例化不同的实体子类。

举个🌰 :

+ (UIButton *)buttonWithType:(UIButtonType)type;

在这里,我们只需要输入不同的按钮类型(UIButtonType)就可以得到不同的UIButton的子类。在OC框架中普遍使用这一设计模式。

为什么要这么做呢?

笔者认为这么做的原因是为了“弱化”子类的具体类型,让开发者无需关心创建出来的子类具体属于哪个类。(这里觉得还有点什么,但是还没有想到,欢迎补充!)

我们可以看一个具体的例子: 对于“员工”这个类,可以有各种不同的“子类型”:开发员工,设计员工和财政员工。这些“实体类”可以由“员工”这个抽象基类来获得:

1. 抽象基类

//EOCEmployee.h

typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesigner,
    EOCEmployeeTypeFinance,
};

@interface EOCEmployee : NSObject

@property (copy) NSString *name;
@property NSUInteger salary;


// Helper for creating Employee objects
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;

// Make Employees do their respective day's work
- (void)doADaysWork;

@end


//EOCEmployee.m

@implementation EOCEmployee

+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type {
     switch (type) {
         case EOCEmployeeTypeDeveloper:
            return [EOCEmployeeDeveloper new];
         break; 

        case EOCEmployeeTypeDesigner:
             return [EOCEmployeeDesigner new];
         break;

        case EOCEmployeeTypeFinance:
             return [EOCEmployeeFinance new];
         break;
    }
}

- (void)doADaysWork {
 // 需要子类来实现
}

@end

我们可以看到,将EOCEmployee作为抽象基类,这个抽象基类有一个初始化方法,通过这个方法,我们可以得到多种基于这个抽象基类的实体子类:

2. 实体子类(concrete subclass):

@interface EOCEmployeeDeveloper : EOCEmployee
@end

@implementation EOCEmployeeDeveloper

- (void)doADaysWork {
    [self writeCode];
}

@end

注意: 如果对象所属的类位于某个类族中,那么在查询类型信息时就要小心。因为类族中的实体子类并不与其基类属于同一个类。

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

我们可以通“关联对象”机制来把两个对象连接起来。这样我们就可以从某个对象中获取相应的关联对象的值。

先看一下关联对象的语法:

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)

举个例子:

#import <objc/runtime.h>
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";

- (void)askUserAQuestion {

         UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question"
                                                         message:@"What do you want to do?"
                                                        delegate:self
                                               cancelButtonTitle:@"Cancel"
                                               otherButtonTitles:@"Continue", nil];

         void (^block)(NSInteger) = ^(NSInteger buttonIndex){

                     if (buttonIndex == 0) {
                            [self doCancel];
                     } else {
                            [self doContinue];
                    }
         };

         //将alert和block关联在了一起
         objc_setAssociatedObject(alert,EOCMyAlertViewKey,block, OBJC_ASSOCIATION_COPY);
         [alert show];
}

// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
     //alert取出关联的block
      void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey)
     //给block传入index值
      block(buttonIndex);
}

第13条:用“方法调配技术”调试“黑盒方法”

与选择子名称相对应的方法是可以在运行期被改变的,所以,我们可以不用通过继承类并覆写方法就能改变这个类本身的功能。

那么如何在运行期改变选择子对应的方法呢? 答:通过操纵类的方法列表的IMP指针

什么是类方法表?什么是IMP指针呢?

类的方法列表会把选择子的名称映射到相关的方法实现上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这些指针叫做IMP。例如NSString类的选择子列表:

类方法表的映射

有了这张表,OC的运行期系统提供的几个方法就能操纵它。开发者可以向其中增加选择子,也可以改变某选择子对应的方法实现,也可以交换两个选择子所映射到的指针以达到交换方法实现的目的。

举个 :交换lowercaseStringuppercaseString方法的实现:

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class],@selector(uppercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);

这样一来,类方法表的映射关系就变成了下图:

交换两个方法

这时,如果我们调用lowercaseString方法就会实际调用uppercaseString的方法,反之亦然。

然而! 在实际应用中,只交换已经存在的两个方法是没有太大意义的。我们应该利用这个特性来给既有的方法添加新功能(听上去吊吊的):

它的实现原理是:先通过分类增加一个新方法,然后将这个新方法和要增加功能的旧方法替换(旧方法名 对应新方法的实现),这样一来,如果我们调用了旧方法,就会实现新方法了。

不知道这么说是否抽象。还是举个 :

**需求:**我们要在原有的lowercaseString方法中添加一条输出语句。

步骤一:我们先将新方法写在NSString的分类里:

@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end


@implementation NSString (EOCMyAdditions)

- (NSString*)eoc_myLowercaseString {
     NSString *lowercase = [self eoc_myLowercaseString];//eoc_myLowercaseString方法会在将来方法调换后执行lowercaseString的方法
     NSLog(@"%@ => %@", self, lowercase);//输出语句,便于调试
     return lowercase;
}
@end

步骤二:交换两个方法的实现(操纵调换IMP指针)

Method originalMethod =
 class_getInstanceMethod([NSString class],
 @selector(lowercaseString));
Method swappedMethod =
 class_getInstanceMethod([NSString class],
 @selector(eoc_myLowercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);

这样一来,我们如果交换了lowercaseStringeoc_myLowercaseString的方法实现,那么在调用原来的lowercaseString方法后就可以输出新增的语句了。

“NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: ThIs iS tHe StRiNg => this is the string”

第16条:提供"全能初始化方法"

有时,由于要实现各种设计需求,一个类可以有多个创建实例的初始化方法。我们应该选定其中一个作为全能初始化方法,令其他初始化方法都来调用它。

注意

  • 只有在这个全能初始化方法里面才能存储内部数据。这样一来,当底层数据存储机制改变时,只需修改此方法的代码就好,无需改动其他初始化方法。
  • 全能初始化方法是所有初始化方法里参数最多的一个,因为它使用了尽可能多的初始化所需要的参数,以便其他的方法来调用自己。
  • 在我们拥有了一个全能初始化方法后,最好还是要覆写init方法来设置默认值。
   //全能初始化方法
    - (id)initWithWidth:(float)width andHeight:(float)height
    {
         if ((self = [super init])) {
            _width = width;
            _height = height;
        }
        return self;
    }
    
    //init方法也调用了全能初始化方法
    - (id)init {
         return [self initWithWidth:5.0f andHeight:10.0f];
    }

现在,我们要创造一个squre类继承这上面这个ractangle类,它有自己的全能初始化方法:

- (id)initWithDimension: (float)dimension{
    return [super initWithWidth:dimension andHeight:dimension];
}

这里有问题!

然而,因为square类是rectangle类的子类,那么它也可以使用initWithWidth: andHeight:方法,更可以使用init方法。那么这两种情况下,显然是无法确保初始化的图形是正方形。

因此,我们需要在这里覆写square的父类rectangle的全能初始化方法:

- (id)initWithWidth:(float)width andHeight:(float)height
{
    float dimension = MAX(width, height);
    return [self initWithDimension:dimension];
}

这样一来,当square用initWithWidth: andHeight:方法初始化时,就会得到一个正方形。

并且,如果用init方法来初始化square的话,我们也可以得到一个默认的正方形。因为在rectangle类里覆写了init方法,而这个init方法又调用了initWithWidth: andHeight:方法,并且square类又覆写了initWithWidth: andHeight:方法,所以我们仍然可以得到一个正方形。

而且,为了让square的init方法得到一个默认的正方形,我们也可以覆写它自己的初始化方法:

- (id)init{
    return [self initWithDimension:5.0f];
}

我们做个总结:

因为子类的全能初始化方法(initWithDimension:)和其父类的初始化方法并不同,所以我们需要在子类里覆写initWithWidth: andHeight:方法。

还差一点:initWithCoder:的初始化

有时,需要定义两种全能初始化方法,因为对象有可能有两种完全不同的创建方式,例如initWithCoder:方法。

我们仍然需要调用超类的初始化方法:

在rectangle类:

// Initializer from NSCoding
- (id)initWithCoder:(NSCoder*)decoder {

     // Call through to super's designated initializer
         if ((self = [super init])) {
            _width = [decoder decodeFloatForKey:@"width"];
            _height = [decoder decodeFloatForKey:@"height"];
        }
     return self;
}

在square类:

// Initializer from NSCoding
- (id)initWithCoder:(NSCoder*)decoder {

 // Call through to super's designated initializer
      if ((self = [super initWithCoder:decoder])) {
     // EOCSquare's specific initializer
    }
     return self;
}

每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。在调用了超类的初始化方法后,再执行与本类相关的方法。

initWithCoder方法在什么情况下 会执行?

在iOS开发中,initWithCoder方法是NSCoding协议的一部分,用于从归档数据(archive)中初始化对象。当你将一个对象归档到文件系统或通过网络传输时,可以使用NSKeyedArchiver类将对象转换为NSData格式,并将其写入文件或传输。然后,可以使用NSKeyedUnarchiver类从NSData中读取对象并还原为原始对象。在这个过程中,initWithCoder方法会被调用来初始化对象。

具体来说,当你使用NSKeyedUnarchiver的unarchiveObject(with:)方法解码归档数据时,系统会查找对象的initWithCoder方法来完成对象的初始化。这个方法在以下情况下会被执行:

从文件中解档对象:当你使用NSKeyedUnarchiver解码从文件读取的归档数据时,会调用initWithCoder方法来初始化对象。 通过网络传输解档对象:当你通过网络传输归档数据,并使用NSKeyedUnarchiver解码接收到的数据时,会调用initWithCoder方法来初始化对象。 总之,initWithCoder方法在使用NSKeyedUnarchiver解码归档数据时被调用,用于初始化对象。

第17条:实现description方法

在打印我们自己定义的类的实例对象时,在控制台输出的结果往往是这样的:

object = <EOCPerson: 0x7fd9a1600600>

这里只包含了类名和内存地址,它的信息显然是不具体的,远达不到调试的要求。

**但是!**如果在我们自己定义的类覆写description方法,我们就可以在打印这个类的实例时输出我们想要的信息。

例如:

- (NSString*)description {
     return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}

在这里,显示了内存地址,还有该类的所有属性。

而且,如果我们将这些属性值放在字典里打印,则更具有可读性:

- (NSString*)description {
     return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
   
    @{    @"title":_title,
       @"latitude":@(_latitude),
      @"longitude":@(_longitude)}
    ];
}

输出结果:

location = <EOCLocation: 0x7f98f2e01d20, {

    latitude = "51.506";
   longitude = 0;
       title = London;
}>

我们可以看到,通过重写description方法可以让我们更加了解对象的情况,便于后期的调试,节省开发时间。

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

匿名对象(Annonymous object),可以理解为“没有名字的对象”。有时我们用协议来提供匿名对象,目的在于说明它仅仅表示“遵从某个协议的对象”,而不是“属于某个类的对象”。

它的表示方法为:id<protocol>。 通过协议提供匿名对象的主要使用场景有两个:

  • 作为属性
  • 作为方法参数

1. 匿名对象作为属性

在设定某个类为自己的代理属性时,可以不声明代理的类,而是用id,因为成为代理的终点并不是某个类的实例,而是遵循了某个协议

举个 :

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

在这里使用匿名对象的原因有两个:

  1. 将来可能会有很多不同类的实例对象作为该类的代理。
  2. 我们不想指明具体要使用哪个类来作为这个类的代理。

也就是说,能作为该类的代理的条件只有一个:它遵从了 协议。

2. 匿名对象作为方法参数

有时,我们不会在意方法里某个参数的具体类型,而是遵循了某种协议,这个时候就可以使用匿名对象来作为方法参数。

举个 :

- (void)setObject:(id)object forKey:(id<NSCopying>)key;

这个方法是NSDictionary的设值方法,它的参数只要遵从了协议,就可以作为参数传进去,作为NSDictionary的键。

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

在发生异常时的内存管理需要仔细考虑内存管理的问题:

在try块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么除非在catch块中能处理此问题,否则对象所占内存就将泄漏。

在MRC环境下:

@try {
     EOCSomeClass *object = [[EOCSomeClass alloc] init];
      [object doSomethingThatMayThrow];
      [object release];
}

@catch (...) {
         NSLog(@"Whoops, there was an error. Oh well...");
}

这里,我们用release方法释放了try中的对象,但是这样做仍然有问题:如果在doSomthingThatMayThrow方法中抛出了异常了呢?

这样就无法执行release方法了。

解决办法是使用@finnaly块,无论是否抛出异常,其中的代码都能运行:

EOCSomeClass *object;
@try {
    object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
}

@catch (...) {
     NSLog(@"Whoops, there was an error. Oh well...");
}

@finally {
    [object release];
}

在ARC环境下呢?

@try {
     EOCSomeClass *object = [[EOCSomeClass alloc] init];
     [object doSomethingThatMayThrow];
}

@catch (...) {
 NSLog(@"Whoops, there was an error. Oh well...");
}

这时,我们无法手动使用release方法了,解决办法是使用:-fobjc-arc-exceptions 标志来加入清理代码,不过会导致应用程序变大,而且会降低运行效率。

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

对象之间都用强指针引用对方的话会造成保留环。

两个对象的保留环:

两个对象都有一个对方的实例来作为自己的属性:

@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end


@interface EOCClassB : NSObject
@property (nonatomic, strong) EOCClassA *other;
@end

两个对象的保留环

两个对象都有指向对方的强指针,这样会导致这两个属性里的对象无法被释放掉。

多个对象的保留环:

如果保留环连接了多个对象,而这里其中一个对象被外界引用,那么当这个引用被移除后,整个保留环就泄漏了。

多个对象的保留环:孤岛

解决方案是使用弱引用:

//EOCClassB.m
//第一种弱引用:unsafe_unretained
@property (nonatomic, unsafe_unretained) EOCClassA *other;


//第二种弱引用:weak
@property (nonatomic, weak) EOCClassA *other;

这两种弱引用有什么区别呢?

unsafe_unretained:当指向EOCClassA实例的引用移除后,unsafe_unretained属性仍然指向那个已经回收的实例,

而weak指向nil:

unsafe_unretained 和 weak的区别

显然,用weak字段应该是更安全的,因为不再使用的对象按理说应该设置为nil,而不应该产生依赖。

第34条:以“自动释放池快”降低内存峰值


释放对象的两种方式:

  • 调用release:保留计数递减
  • 调用autorelease将其加入自动释放池中。在将来清空自动释放池时,系统会向其中的对象发送release消息。

内存峰值(high-memory waterline)是指应用程序在某个限定时段内的最大内存用量(highest memory footprint)。新增的自动释放池块可以减少这个峰值:

不用自动释放池减少峰值:

for (int i = 0; i < 100000; i++) {
      [self doSomethingWithInt:i];
}

在这里,doSomethingWithInt:方法可能会创建临时对象。随着循环次数的增加,临时对象的数量也会飙升,而只有在整个for循环结束后,这些临时对象才会得意释放。

这种情况是不理想的,尤其在我们无法控制循环长度的情况下,我们会不断占用内存并突然释放掉它们。

因此,我们需要用自动释放池来降低这种突兀的变化:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
     @autoreleasepool {
             EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
            [people addObject:person];
      }
}

这样一来,每次循环结束,我们都会将临时对象放在这个池里面,而不是线程的主池里面。

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

某个对象被回收后,再向它发送消息是不安全的,这并不一定会引起程序崩溃。

如果程序没有崩溃,可能是因为:

  • 该内存的部分原数据没有被覆写。
  • 该内存恰好被另一个对象占据,而这个对象可以应答这个方法。

如果被回收的对象占用的原内存被新的对象占据,那么收到消息的对象就不会是我们预想的那个对象。在这样的情况下,如果这个对象无法响应那个方法的话,程序依旧会崩溃。

因此,我们希望可以通过一种方法捕捉到对象被释放后收到消息的情况

这种方法就是利用僵尸对象!

Cocoa提供了“僵尸对象”的功能。如果开启了这个功能,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”(通过修改isa指针,令其指向特殊的僵尸类),而不会真正回收它们,而且它们所占据的核心内存将无法被重用,这样也就避免了覆写的情况。

在僵尸对象收到消息后,会抛出异常,它会说明发送过来的消息,也会描述回收之前的那个对象。

第38条:为常用的块类型创建typedef

如果我们需要重复创建某种块(相同参数,返回值)的变量,我们就可以通过typedef来给某一种块定义属于它自己的新类型

例如:

int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value){
     // Implementation
     return someInt;
}

这个块有一个bool参数和一个int参数,并返回int类型。我们可以给它定义类型:

typedef int(^EOCSomeBlock)(BOOL flag, int value);

再次定义的时候,就可以通过简单的赋值来实现:

EOCSomeBlock block = ^(BOOL flag, int value){
     // Implementation
};

定义作为参数的块:

- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;

这里的块有一个NSData参数,一个NSError参数并没有返回值

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;”

通过typedef定义块签名的好处是:如果要某种块增加参数,那么只修改定义签名的那行代码即可。

第39条:用handler块降低代码分散程度

下载网络数据时,如果使用代理方法,会使得代码分布不紧凑,而且如果有多个下载任务的话,还要在回调的代理中判断当前请求的类型。但是如果使用block的话,就可以让网络下载的代码和回调处理的代码写在一起,这样就可以同时解决上面的两个问题:

用代理下载:

- (void)fetchFooData {

     NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
    _fooFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    _fooFetcher.delegate = self;
    [_fooFetcher start];

}

- (void)fetchBarData {

     NSURL *url = [[NSURL alloc] initWithString: @"http://www.example.com/bar.dat"];
    _barFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
    _barFetcher.delegate = self;
    [_barFetcher start];

}

- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher didFinishWithData:(NSData*)data
{   //判断下载器类型
     if (networkFetcher == _fooFetcher) {
        _fetchedFooData = data;
        _fooFetcher = nil;

    } else if (networkFetcher == _barFetcher) {
        _fetchedBarData = data;
        _barFetcher = nil;
    }
}

用block下载:

- (void)fetchFooData {

     NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
     EOCNetworkFetcher *fetcher =
     [[EOCNetworkFetcher alloc] initWithURL:url];
     [fetcher startWithCompletionHandler:^(NSData *data){
            _fetchedFooData = data;
   }];

}

- (void)fetchBarData {

     NSURL *url = [[NSURL alloc] initWithString: @"http://www.example.com/bar.dat"];
     EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
    [fetcher startWithCompletionHandler:^(NSData *data){
            _fetchedBarData = data;
    }];
} 

还可以将处理成功的代码放在一个块里,处理失败的代码放在另一个块中:

“#import <Foundation/Foundation.h>

@class EOCNetworkFetcher;
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void(^EOCNetworkFetcherErrorHandler)(NSError *error);


@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler: (EOCNetworkFetcherCompletionHandler)completion failureHandler: (EOCNetworkFetcherErrorHandler)failure;

@end



EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data){
     // Handle success
}

 failureHandler:^(NSError *error){
 // Handle failure
}];

这样写的好处是,我们可以将处理成功和失败的代码分开来写,看上去更加清晰。

我们还可以将 成功和失败的代码都放在同一个块里:

“#import <Foundation/Foundation.h>


@class EOCNetworkFetcher;
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:

(EOCNetworkFetcherCompletionHandler)completion;

@end



EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];

[fetcher startWithCompletionHander:

^(NSData *data, NSError *error){

if (error) {

     // Handle failure

} else {

     // Handle success

}
}];

这样做的好处是,如果及时下载失败或中断了,我们仍然可以取到当前所下载的data。而且,如果在需求上指出:下载成功后得到的数据很少,也视为失败,那么单一块的写法就很适用,因为它可以取得数据后(成功)再判断其是否是下载成功的。

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

如果块捕获的对象直接或间接地保留了块本身,那么就需要小心保留环问题:

@implementation EOCClass {

     EOCNetworkFetcher *_networkFetcher;
     NSData *_fetchedData;

}


- (void)downloadData {

     NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
    _networkFetcher =[[EOCNetworkFetcher alloc] initWithURL:url];

    [_networkFetcher startWithCompletionHandler:^(NSData *data){

             NSLog(@"Request URL %@ finished", _networkFetcher.url);
            _fetchedData = data;

    }];

}

在这里出现了保留环:块要设置_fetchedData变量,就需要捕获self变量。而self(EOCClass实例)通过实例变量保留了获取器_networkFetcher,而_networkFetcher又保留了块。

解决方案是:在块中取得了data后,将_networkFetcher设为nil。

- (void)downloadData {

     NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
    _networkFetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data){

             NSLog(@"Request URL %@ finished", _networkFetcher.url);
            _fetchedData = data;
            _networkFetcher = nil;

    }];

}

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

多个线程执行同一份代码时,很可能会造成数据不同步。作者建议使用GCD来为代码加锁的方式解决这个问题。

方案一:使用串行同步队列来将读写操作都安排到同一个队列里:

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

//读取字符串
- (NSString*)someString {

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

}

//设置字符串
- (void)setSomeString:(NSString*)someString {

     dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}

这样一来,读写操作都在串行队列进行,就不容易出错。

但是,还有一种方法可以让性能更高:

方案二:将写操作放入栅栏快中,让他们单独执行;将读取操作并发执行。

_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;
    });

}

显然,数据的正确性主要取决于写入操作,那么只要保证写入时,线程是安全的,那么即便读取操作是并发的,也可以保证数据是同步的。

这里的dispatch_barrier_async方法使得操作放在了同步队列里“有序进行”,保证了写入操作的任务是在串行队列里。

第42条:多用GCD,少用performSelector系列方法

在iOS开发中,有时会使用performSelector来执行某个方法,但是performSelector系列的方法能处理的选择子很局限:

  • 它无法处理带有多个参数的选择子。
  • 返回值只能是void或者对象类型。

但是如果将方法放在块中,通过GCD来操作就能很好地解决这些问题。尤其是我们如果想要让一个任务在另一个线程上执行,最好应该将任务放到块里,交给GCD来实现,而不是通过performSelector方法。

举几个 来比较这两种方案:

1. 延后执行某个任务的方法:

// 使用 performSelector:withObject:afterDelay:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];


// 使用 dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
    [self doSomething];
});

2. 将任务放在主线程执行:

// 使用 performSelectorOnMainThread:withObject:waitUntilDone:
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];


// 使用 dispatch_async
// (or if waitUntilDone is YES, then dispatch_sync)
dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
});

注意: 如果waitUntilDone的参数是Yes,那么就对应GCD的dispatch_sync方法。 我们可以看到,使用GCD的方式可以将线程操作代码和方法调用代码写在同一处,一目了然;而且完全不受调用方法的选择子和方法参数个数的限制。

第43条:掌握GCD及操作队列的使用时机

除了GCD,操作队列(NSOperationQueue)也是解决多线程任务管理问题的一个方案。对于不同的环境,我们要采取不同的策略来解决问题:有时候使用GCD好些,有时则是使用操作队列更加合理。

使用NSOperation和NSOperationQueue的优点:

  1. 可以取消操作:在运行任务前,可以在NSOperation对象调用cancel方法,标明此任务不需要执行。但是GCD队列是无法取消的,因为它遵循“安排好之后就不管了(fire and forget)”的原则。
  2. 可以指定操作间的依赖关系:例如从服务器下载并处理文件的动作可以用操作来表示。而在处理其他文件之前必须先下载“清单文件”。而后续的下载工作,都要依赖于先下载的清单文件这一操作。
  3. 监控NSOperation对象的属性:可以通过KVO来监听NSOperation的属性:可以通过isCancelled属性来判断任务是否已取消;通过isFinished属性来判断任务是否已经完成。
  4. 可以指定操作的优先级:操作的优先级表示此操作与队列中其他操作之间的优先关系,我们可以指定它。

第44条:通过Dispath Group机制,根据系统资源状况来执行任务

有时需要等待多个并行任务结束的那一刻执行某个任务,这个时候就可以使用dispath group函数来实现这个需求:

通过dispath group函数,可以把并发执行的多个任务合为一组,于是调用者就可以知道这些任务何时才能全部执行完毕。

//一个优先级低的并发队列
dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

//一个优先级高的并发队列
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

//创建dispatch_group
dispatch_group_t dispatchGroup = dispatch_group_create();

//将优先级低的队列放入dispatch_group
for (id object in lowPriorityObjects) {
 dispatch_group_async(dispatchGroup,lowPriorityQueue,^{ [object performTask]; });
}

//将优先级高的队列放入dispatch_group
for (id object in highPriorityObjects) {
 dispatch_group_async(dispatchGroup,highPriorityQueue,^{ [object performTask]; });
}

//dispatch_group里的任务都结束后调用块中的代码
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup,notifyQueue,^{
     // Continue processing after completing tasks
});

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

有时我们可能只需要将某段代码执行一次,这时可以通过dispatch_once函数来解决。

dispatch_once函数比较重要的使用例子是单例模式: 我们在创建单例模式的实例时,可以使用dispatch_once函数来令初始化代码只执行一次,并且内部是线程安全的。

而且,对于执行一次的block来说,每次调用函数时传入的标记都必须完全相同,通常标记变量声明在static或global作用域里。

+ (id)sharedInstance {

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

我们可以这么理解:在dispatch_once块中的代码在程序启动到终止的过程里,只要运行了一次后,就给自己加上了注释符号,不再存在了。

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

通过无缝桥接技术,可以再Foundation框架中的OC对象和CoreFoundation框架中的C语言数据结构之间来回转换。

创建CoreFoundation中的collection时,可以指定如何处理其中的元素。然后利用无缝桥接技术,可以将其转换为OCcollection。

简单的无缝桥接演示:

NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));

这里,__bridge表示ARC仍然具备这个OC对象的所有权。CFArrayGetCount用来获取数组的长高度。

为什么要使用无缝桥接技术呢?因为有些OC对象的特性是其对应的CF数据结构不具备的,反之亦然。因此我们需要通过无缝桥接技术来让这两者进行功能上的“互补”。

⚠️ **GitHub.com Fallback** ⚠️