继上一篇《Effective Objective-C 》干货三部曲(一):概念篇以后,本篇便是三部曲的第二篇:规范篇。 没看过三部曲第一篇的小伙伴可能不知道我在说神马,在这里仍是先啰嗦一下三部曲是咋回事:笔者将《Effective Objective-C 》这本书的52个知识点分为三大类进行了归类整理:git
而后用思惟导图整理了一下: 程序员
做为三部曲的第二篇,本篇总结抽取了《Effective Objective-C 》这本书中讲解规范性知识的部分:这些知识点都是为了不在开发过程当中出现问题或给开发提供便利的规范性知识点。掌握这些知识有助于造成科学地写OC代码的习惯,使得代码更加容易维护和扩展,学习这类知识是iOS初学者进阶的必经之路。github
有时,类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的所有内容,这样就减小了编译时间。
可是个别的时候,必须在头文件中引入其余类的头文件:数组
主要有两种状况:缓存
在声明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"};
复制代码
NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下标操做也应该尽可能使用字面量语法。bash
NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];
复制代码
使用字面量语法的优势:网络
注意: 字面量语法建立出来的字符串,数组,字典对象都是不可变的。
在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。
咱们常常须要给类定义几个状态,这些状态码能够用枚举来管理。下面是关于网络链接状态的状态码枚举:
typedef NS_ENUM(NSUInteger, EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
复制代码
须要注意的一点是: 在枚举类型的switch语句中不要实现default分支。它的好处是,当咱们给枚举增长成员时,编译器就会提示开发者:switch语句并未处理全部的枚举。对此,笔者有个教训,又一次在switch语句中将“默认分支”设置为枚举中的第一项,自觉得这样写可让程序更健壮,结果后来致使了严重的崩溃。
关于实例变量的访问,能够直接访问,也能够经过属性的方式(点语法)来访问。书中做者建议在读取实例变量时采用直接访问的形式,而在设置实例变量的时候经过属性来作。
所以,有个关于折中的方案:
设置属性:经过属性 读取属性:直接访问
不过有两个特例:
Apple宣称其保留使用全部"两字母前缀"的权利,因此咱们选用的前缀应该是三个字母的。 并且,若是本身开发的程序使用到了第三方库,也应该加上前缀。
书中做者建议尽可能把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体作法是:
在头文件中,设置对象属性为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数据的同时要执行隐秘在实现文件里的其余工做,那么若是在外部随意更改这个属性的话,显然是达不到这种需求的。
所以,咱们须要提供给外界咱们定制的增删的方法,并不让外部”自行“增删。
在给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];
复制代码
这个方法名就很好的诠释了该方法的意图:这个类的初始化是须要宽度和高度的。并且,哪一个参数是高度,哪一个参数是宽度,看得人一清二楚。永远要记得:代码是给人看的。
笔者本身总结的方法命名规则:
每一个冒号左边的方法部分最好与右边的参数名一致。
对于返回值是布尔值的方法,咱们也要注意命名的规范:
- isEqualToString:
复制代码
获取“是否有”的布尔值,应该增长“has”前缀:
- hasPrefix:
复制代码
建议在实现文件里将非公开的方法都加上前缀,便于调试,并且这样一来也很容易区分哪些是公共方法,哪些是私有方法。由于每每公共方法是不便于任意修改的。
在这里,做者举了个例子:
#import <Foundation/Foundation.h>
@interface EOCObject : NSObject
- (void)publicMethod;
@end
@implementation EOCObject
- (void)publicMethod {
/* ... */
}
- (void)p_privateMethod {
/* ... */
}
@end
复制代码
注意: 不要用下划线来区分私有方法和公共方法,由于会和苹果公司的API重复。
若是给委托对象发送消息,那么必须提早判断该委托对象是否实现了该消息:
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)告诉它“你有这些数据”。仔细回味一下,这两个信息的传递方向是相反的。
一般一个类会有不少方法,而这些方法每每能够用某种特有的逻辑来分组。咱们能够利用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)”的办法。
分类机制虽然强大,可是若是分类里的方法与原来的方法名称一致,那么分类的方法就会覆盖掉原来的方法,并且老是以最后一次被覆盖为基准。
所以,咱们应该以命名空间来区别各个分类的名称与其中定义的方法。在OC里的作法就是给这些方法加上某个共用的前缀。例如:
@interface NSString (ABC_HTTP)
// Encode a string with URL encoding
- (NSString*)abc_urlEncodedString;
// Decode a URL encoded string
- (NSString*)abc_urlDecodedString;
@end
复制代码
所以,若是咱们想给第三方库或者iOS框架里的类添加分类时,最好将分类名和方法名加上前缀。
除了实现文件里的class-continuation分类中能够声明属性外,其余分类没法向类中新增实例变量。
所以,类所封装的所有数据都应该定义在主接口中,这里是惟一可以定义实例变量的地方。
关于分类,须要强调一点:
分类机制,目标在于扩展类的功能,而不是封装数据。
一般,咱们须要减小在公共接口中向外暴露的部分(包括属性和方法),而所以带给咱们的局限性能够利用class-continuation分类的特性来补偿:
永远不要本身调用dealloc方法,运行期系统会在适当的时候调用它。根据性能需求咱们有时须要在dealloc方法中作一些操做。那么咱们能够在dealloc方法里作什么呢?
举个简单的🌰 :
- (void)dealloc {
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
复制代码
尤为注意:在dealloc方法中不该该调用其余的方法,由于若是这些方法是异步的,而且回调中还要使用当前对象,那么颇有可能当前对象已经被释放了,会致使崩溃。
而且在dealloc方法中也不能调用属性的存取方法,由于颇有可能在这些方法里还有其余操做。并且这个属性还有可能处于键值观察状态,该属性的观察者可能会在属性改变时保留或者使用这个即将回收的对象。
在非ARC得环境下使用retainCount能够返回当前对象的引用计数,可是在ARC环境下调用会报错,由于该方法已经被废弃了 。
它被废弃的缘由是由于它所返回的引用计数只能反映对象某一时刻的引用计数,而没法“预知”对象未来引用计数的变化(好比对象当前处于自动释放池中,那么未来就会自动递减引用计数)。
咱们没法用某个队列来描述“当前队列”这一属性,由于派发队列是按照层级来组织的。
那么什么是队列的层级呢?
安排在某条队列中的快,会在其上层队列中执行,而层级地位最高的那个队列老是全局并发队列。
在这里,B,C中的块会在A里执行。可是D中的块,可能与A里的块并行,由于A和D的目标队列是并发队列。
正由于有了这种层级关系,因此检查当前队列是并发的仍是非并发的就不会老是很准确。
当遍历集合元素时,建议使用块枚举,由于相对于传统的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'
}
复制代码
这种快速遍历的方法要比传统的遍历方法更加简洁易懂,可是缺点是没法方便获取元素的下标。
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'
}];
复制代码
若是咱们能够知道集合里的元素类型,就能够修改签名。这样作的好处是:可让编译期检查该元素是否能够实现咱们想调用的方法,若是不能实现,就作另外的处理。这样一来,程序就能变得更加安全。
若是咱们缓存使用得当,那么应用程序的响应速度就会提升。只有那种“从新计算起来很费事的数据,才值得放入缓存”,好比那些须要从网络获取或从磁盘读取的数据。
在构建缓存的时候不少人习惯用NSDictionary或者NSMutableDictionary,可是做者建议你们使用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是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;
+(void)load;
复制代码
每一个类和分类在加入运行期系统时,都会调用load
方法,并且仅仅调用一次,可能有些小伙伴习惯在这里调用一些方法,可是做者建议尽可能不要在这个方法里调用其余方法,尤为是使用其余的类。缘由是每一个类载入程序库的时机是不一样的,若是该类调用了还未载入程序库的类,就会很危险。
+(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。
在使用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增长一个分类就能够解决:
#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日更新 ----------------------------
注意注意!!!
笔者在近期开通了我的公众号,主要分享编程,读书笔记,思考类的文章。
由于公众号天天发布的消息数有限制,因此到目前为止尚未将全部过去的精选文章都发布在公众号上,后续会逐步发布的。
并且由于各大博客平台的各类限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~
扫下方的公众号二维码并点击关注,期待与您的共同成长~