《Effective Objective-C》干货三部曲(二):规范篇

继上一篇《Effective Objective-C 》干货三部曲(一):概念篇以后,本篇便是三部曲的第二篇:规范篇。 没看过三部曲第一篇的小伙伴可能不知道我在说神马,在这里仍是先啰嗦一下三部曲是咋回事:笔者将《Effective Objective-C 》这本书的52个知识点分为三大类进行了归类整理:git

  • 概念类:讲解了一些概念性知识。
  • 规范类:讲解了一些为了不一些问题或者为后续开发提供便利所须要遵循的规范性知识。
  • 技巧类:讲解了一些为了解决某些特定问题而须要用到的技巧性知识。

而后用思惟导图整理了一下: 程序员

三部曲分布图

做为三部曲的第二篇,本篇总结抽取了《Effective Objective-C 》这本书中讲解规范性知识的部分:这些知识点都是为了不在开发过程当中出现问题或给开发提供便利的规范性知识点。掌握这些知识有助于造成科学地写OC代码的习惯,使得代码更加容易维护和扩展,学习这类知识是iOS初学者进阶的必经之路。github

第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 的取下标操做也应该尽可能使用字面量语法。bash

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一样会失效。

最后的话

总的来讲这一部分仍是比较容易理解的,更多的只是教咱们一些编写OC程序的规范,并无深刻讲解技术细节。

而三部曲的最后一篇:技巧篇则着重讲解了一些在编写OC代码的过程当中可使用的一些技巧。广义上来说,这些技巧也能够被称为“规范”,例如“提供全能初始化方法”这一节,可是这些知识点更像是一些“设计模式”目的更偏向于在于解决一些实际问题,所以将这些知识点归类为“技巧类”。

由于第三篇的内容稍微难一点,因此笔者打算再好好消化几天,将第三篇的初稿再三润饰以后呈献给你们~

本文已同步到我的博客:传送门

其余两篇的传送门:

《Effective Objective-C 》干货三部曲(一):概念篇

《Effective Objective-C 》干货三部曲(三):技巧篇

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

笔者在近期开通了我的公众号,主要分享编程,读书笔记,思考类的文章。

  • 编程类文章:包括笔者之前发布的精选技术文章,以及后续发布的技术文章(以原创为主),而且逐渐脱离 iOS 的内容,将侧重点会转移到提升编程能力的方向上。
  • 读书笔记类文章:分享编程类思考类心理类职场类书籍的读书笔记。
  • 思考类文章:分享笔者平时在技术上生活上的思考。

由于公众号天天发布的消息数有限制,因此到目前为止尚未将全部过去的精选文章都发布在公众号上,后续会逐步发布的。

并且由于各大博客平台的各类限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~

扫下方的公众号二维码并点击关注,期待与您的共同成长~

公众号:程序员维他命
相关文章
相关标签/搜索