刘毅的技术博客

记录自己的学习生活点滴,也希望和大家交流分享!

Effective Objective-C读书笔记7

这是本书的最后一章了,集中讲解了有关Cocoa自带的system framework,这是开发中必须要使用的基本库,没有这些封装,很多基本的功能都无法实现,没有集合,也没有基类NSObject,可谓寸步难行,一些新的Api有时会节省我们很多工作量,同时这些库中的很多设计也是我们自己的代码需要学习的。

Item47 Familiarize Yourself with the System Frameworks

  1. 一个framework是将代码打包成一个动态库,会有头文件来描述接口,有时候也会有一些第三方的静态库(即.a文件),这些不能作为真正意义上的框架,但是被常常当做框架来用,所有的系统框架都使用了动态库。
  2. Cocoa或Cocoa Touch是框架集,其中的基本框架就是Foundation框架,Foundation Framework不但提供了基本类型和基本集合,而且还有很多复杂功能,比如字符串处理。
  3. 除了另一个基础库是CoreFoundation,几乎就是Foundation的镜像库,只不过内部都是C接口和结构体,OC提供了一个名为toll-free bridging的转换特性,可以使OC对象和CF对象自由转化,toll-free bridging自身比较复杂,所以不建议自己去实现这一转化功能。除了上述两个基础框架,还有以下一些常用框架:
  4. CFNetwork:基于C的网络请求基本框架,基于BSD socket提供了很多易用的请求工具,Foundation通过对它的部分封装,提供了OC类型的网络接口。
  5. CoreAudio:提供了基于C的音频设备访问接口,本身是很复杂的,但OC的抽象将其变得易用不少。
  6. AVFoundation:提供了用于播放和录制音视频的OC对象,例如播放视频的UI类。
  7. CoreData:提供了用于数据持久化的OC对象,CoreData处理数据的存取,并能在Mac OS X与iOS之间通用。
  8. CoreText:提供了基于C的文字高效的类型设置和渲染的接口。
  9. 使用一些C类型的框架,有时是必要的,因为通过绕过runtime,速度会更快,但是需要更加关注内存管理。
  10. AppKit和UIKit分别是Mac OS X 和iOS的UI框架,提供了基于Foundation和CoreFoundation的OC类型,在它的下面是CoreAnimation和CoreGraphics在支持。
  11. CoreAnimation基于OC类型,提供了渲染图像和展示动画的工具,它不是一个独立的框架,还是QuartzCore框架的一部分,但是很多情况CoreAnimation还是被优先使用。
  12. CoreGraphics是基于C类型的,提供了用于2D渲染的必不可少的结构体和函数,CGPoint,CGSize,CGRect都是在这儿定义的。
  13. UIKit的上层还有很多更高级的框架,例如:MapKit,Social framework。

Item48 Prefer Block Enumeration to for Loops

1.遍历一个集合类型是常见需求,,而OC也有很多方式,从标准的C的for循环,到OC 1.0的NSEnumerator,到OC 2.0的快速遍历,block加入OC后,又出现了遍历直接传入block进行对象处理的新方法。
2.For Loops:沿用最原始的C语言的循环,定义一个int型index,然后按照index去遍历每个对象,对于NSArray来说影响还不大,但是对于NSDictionary,NSSet来说,因为都是无序的,所以必须额外生成中间数组,这是额外的内存消耗,但倒序遍历只需要改变index为递减即可,还算方便。
3.OC 1.0 NSEnumerator:NSEnumerator是一个基类,需要重写-(NSArray*)allObjects,–(id)nextObject两个方法,而Foundation框架的集合类型都支持了NSEnumerator,可以通过不断执行nextObject()来完成遍历,它的优势是所有的集合类型的遍历方式都是类似的,而且也支持不同的enumerator来实现不同顺序来遍历,缺点是还是需要额外的enumerator,而且不能得知当前的index。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//NSArray enumerator
NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [anArray objectEnumerator];
id object;
while((object = [enumerator nextObject])!=nil){
  //Do something with 'object'
}
//NSDictionary enumerator
NSDictionary *aDictionary = /*...*/;
id key;
while((key = [enumerator nextObject])!=nil){
  id value = aDictionary[key];
  //Do something with 'key' and 'value'
}
//NSArray reverse enumerator
NSArray *anArray = /*...*/;
NSEnumerator *enumerator = [anArray reverseObjectEnumerator];
id object;
while((object = [enumerator nextObject])!=nil){
  //Do something with 'object'
}

4.Fast Enumeration:OC2.0引入了快速遍历,快速遍历详单与结合了for-loop和enumerator的双重特点,同时极大的简化了语法。实现这一技术是采用了NSFastEnumeration这一协议(只有一个方法),集合类型通过遵循这一协议,从而支持了快速遍历,实现协议中的方法使得类可以同时返回多个对象:

1
2
//NSFastEnumeration
-(NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState*)state objects:(id*)stackbuffer count:(NSUInteger)length;
1
2
3
4
5
6
7
8
9
10
11
//NSArray
NSArray *anArray = /*...*/;
for(id object in anArray){
  //Do something with 'object'
}
//NSDictionary
NSDictionary *aDictionary = /*...*/;
for(id key in aDictionary){
  id value = aDictionary[key];
  //Do something with 'key' and value
}

如果需要反向遍历,可以通过下列方法,因为NSEnumerator也实现了NSFastEnumeration:

1
2
3
4
NSArray *anArray = /*...*/;
for(id object in [anArray reverseObjectEnumerator]){
  //Do something with 'object'
}

快速遍历的优点是效率很高,而且代码简洁,但是依然有两个缺陷,NSDictionary如果同时需要key和value的话,还是需要两步;同时index也是无法直接获取。

5.Block-Based Enumeration:是在OC引入block后的遍历集合的最新方法,下面是NSArray的基本block遍历方法,前两个参数很明显,第三个参数是用来停止遍历的:

1
2
3
4
5
6
7
8
9
10
11
12
//NSArray
-(void)enumerateObjectsUsingBlock:(void(^)(id object,NSUInteger idx,BOOL *stop))block;
//NSDictionary
-(void)enumerateLeysAndObjectsUsingBlock:(void(^)(id key,id object,BOOL *stop))block;
//example
NSArray *aArray = /*...*/;
[aArray enumerateObjectsUsingBlock:^(id object,NSUInteger idx,BOOL *stop){
  //Do something with 'object'
  if(shouldStop){
      *stop = YES;
  }
}];

block遍历虽然看起来语法比快速遍历更复杂了,但是代码还是很整洁的,而且提供了方便的停止遍历的方法,而在其他方式中需要自己添加break,另外你可以现在一次性获得所有的信息,包括:NSArray的index,NSDictionary的key和value。

如果NSDictionary的键值类型是确定的,那么你可以重写block中的id类型,也可以在类型出现异常时抛出警告,所以如果类型确定,还是推荐这么写的。

1
2
3
4
NSDictionary *aDictionary = /*...*/;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key,NSString *obj,BOOL *stop){
  //Do Something with 'key' and 'obj'
}];

除了这些,还有个最大的特点是,block遍历可以通过设置option(枚举类型)来实现各种各样的遍历方式,例如通过NSEnumerationConcurrent实现对集合中的对象并发执行方法(内部应该是利用了GCD的dispatch group),通过NSEnumerationReverse实现集合的逆向遍历,而且也可以通过位与操作,同时实现这两个option。

1
2
3
4
//NSArray
-(void)enumerateObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id object,NSUInteger idx,BOOL *stop))block;
//NSDictionary
-(void)enumerateLeysAndObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id key,id object,BOOL *stop))block;

综上,block遍历虽然语法不及快速遍历简洁,但是对于NSArray获得index,对于NSDictionary获得key和value这些信息,通过option获得并发执行遍历,都是优于其他遍历方法的。

Item49 Using Toll-Free Bridging for Collections with Custom Memory-Management Semantics

1.Toll-Free Bridging是OC用来在Foundation定义的OC对象和CoreFoundation定义的对应的C结构体之间相互转化,使用了bridge关键字,相当于ARC依然持有OC对象,如果使用了bridge_retained,那么ARC就要交出持有权,那么在我们使用完CF指针后要执行CFRelease(aCFArray)来释放内存,反之你需要将CF指针转化为OC对象,并需要转移持有权时,要加上__bridge_transfer关键字,这三个关键字非常重要。

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

2.为什么要去使用CF结构体指针?当然绝大部分情况我们是不会刻意去使用的,作者列举了一个特殊的场景:NSDictionary的key是copy类型,而value是retain类型,也就意味着,不支持copy协议的类是不能作为NSDictionary的key的,如果我们需要一个value和key都是retain类型的dictionary,那要怎么办呢?
3.作者列举了一种思路,重新构建一个CF的字典类型,因为在这一级,我们可以控制key和value的回调类型,然后再通过Toll-Free Bridging来转化为需要的NSMutableDictionary,从而达到目的。

Item50 Use NSCache Instead of NSDictionary for Caches

1.在网络请求中做缓存是常见的需求,因为一般会将data和url或者request对象做成键值对存储,所以一些年轻的程序员就会考虑用NSDictionary来做缓存,但是作者建议用NSCache来做这类缓存。
2.NSCache的优势之一是,当系统资源吃紧时,cache会被自动释放,且会优先释放最近未被使用的缓存,如果NSDictionary想实现这一点,是需要做很多额外工作的。
3.NSCache的优势之二是,NSDictionary的key是copy类型,而value是retain类型,而NSCache的key和value都是retain类型,这就意味着,NSCache可以将不实现copy协议的对象作为key,虽然根据Item49的做法,也可以把NSDictionary做成这种类型,但是实现起来很复杂。另外,NSCache是线程安全的,你可以在多个线程同时插值,在做缓存时,在主线程读取缓存,然后缓存不存在,在后台线程下载并赋值是常见的场景。
4.你可以手动控制缓存的容量,通过设置缓存数量和大小来控制,如果缓存数量或大小超过限制,也会开始自动释放,但释放的顺序是不可控的,所以想通过改变最大容量来让缓存按照顺序释放是不现实的。
5.需要注意的是,设置缓存大小容量,是基于加入缓存的对象的大小易于计算,如果计算对象大小成本过高的话,这就会影响效率,因为每次加入都会进行计算。例如去硬盘计算文件大小或去数据库查找大小就是耗时的操作,但如果是NSData作为缓存对象,那么获取它的大小代价就很小,只是读取一个property而已。
6.下面是一个使用NSCache的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@implementation EOCClass{
  NSCache *_cache;
}
-(id)init{
  if((self = [super init])){
      _cache = [NSCache new];
      _cache.countLimit = 100;
      _cache.totalCostLimit = 5*1024*1024;
  }
  return self;
}
-(void)downloadDataForURL:(NSURL*)url{
  NSData *cachedData = [_cache objectForKey:url];
  if(cacheData){
      //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

7.除了NSData,另一种可以和NSCache搭配的是NSPurgeableData,它是NSMutableData的子类,实现了NSDiscardableContent协议,NSPurgeableData的内存会在系统资源紧张时自动释放,isContentDiscard是协议中的一个方法,返回内存是否已释放。
8.NSPurgeableData在使用之前要用beginContentAccess确保目前内存不被释放,然后使用完后调用endContentAccess告知系统可以被释放,这一对操作可以嵌套,类似retain/release。
9.如果NSPurgeableData添加到NSCache,释放的对象会自动移出cache,这可以被evictsObjectsWithDiscardedContent这一property关闭或开启。
10.下面是一个NSPurgeableData的实例,注意NSPurgeableData被创建时就相当于+1purge reference count与alloc类似,所以不必再加beginContentAccess,但用完要加endContentAccess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-(void)downloadDataForURL:(NSURL*)url{
  NSPurgeableData *cacheData = [_cache objectForKey:url];
  if(cachedData){
      //Stop the data being purged
      [cacheData beginContentAccess];
      //Use the cached data
      [self useData:cachedData];
      //Mark that the data may be purged again
      [cacheData endContentAccess];
  } else {
      //Cache miss
      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
          [self useData:data];
          //Mark that the data may be purged now
          [purgeable endContentAccess];
      }];
  }
}

Item51 Keep initialize and load Implementations Lean

  1. 一个类总会有有自己的初始化方法,由于OC中类型大部分继承于NSObject,所有有很多继承于NSObject的初始化方法,第一个就是*(void)load方法。
  2. load方法在每个class和category中会且只会被调用一次,这个方法发生在包含该类的library在加载后,一般是应用加载中,而且只是iOS代码的独有的,Mac OS X有更灵活的dynamic loading,可以使library在应用加载后加载,而category的load在其原class加载后加载。
  3. 想重写load方法的问题是,它运行时runtime处于不稳定的状态,所有父类的load方法是先于其他类的load调用的,所依赖库中的所有load方法会先调用。但在一个库中,这些类的load方法的调用顺序就是不可控的了。
  4. 作者举例:EOCClassB中实现了load方法,它导入了Foundation.h和EOCClassA.h头文件,而EOCClassA和它同属一个库,EOCClassB在其load方法中使用了NSLog输出NSString,也实例化了EOCClassA并进行了操作。那么NSLog和NSString的使用是没问题的,因为Foundation.h中的class肯定先于EOCClassB的load方法,但使用EOCClassA就有问题了,你不能保证EOCClassA的load是否已经在EOCClassB的load方法之前调用了,因为有可能EOCClassA在load之前是不可用的。
  5. load方法并不遵循于一般的继承规则,一个类没有实现load的话,是不会调用该方法的,即使它的父类实现了;load方法可同时存在于类和它的category中,且category的load会在本类的load之后调用。
  6. 所以综上,load方法并不适合我们自己做初始化工作,因为我们不能确保所有的类型都已经加载,所以实际上它的用途最好仅停留在测试层面,因为如果在load中加载过多任务,也会影响应用的加载时间,是很影响用户体验的。
  7. 第二个初始化方法是+(void)initialize方法,它也是会且只会被调用一次,它是被runtime调用的,而不能被直接调用,它与load有相似的地方,也有很多不同,概括有三点。
  8. 区别一是initialize是懒加载,只有一个类被第一次用到之前才会调用,因此会出现一个类的initialize永远没被调用过的情况,这也意味着不像load会出现所有的load方法在同一时间加载,而且会阻塞应用加载。
  9. 区别二是initialize在执行时,runtime是稳定状态,调用其他类的方法是安全的,而且runtime保证了initialize的线程安全,意味着只有执行initialize的线程可以和class和其实例交互,其他线程将会被阻塞,知道initialize完成。
  10. 区别三是initialize与其他消息一样,如果类的initialize没实现,但是父类实现了,那么父类的initialize会被调用。
  11. 作者举例父类实现了initialize,但是子类没有实现,但在log中会看到该方法被调用了两次,原因是使用一个类时,其父类的initialize会先调用,然后到子类时,由于没有实现该方法,所以继续沿响应链得知父类实现了该方法,所以又执行一次,为了避免这一问题,在initialize中加上if(self == [EOCBaseClass class])的判断就好了。
  12. 虽然initializeload灵活一些了,但是作者依然不推荐在initialize中做很复杂的初始化工作,原因也有三。
  13. 原因一,一个类的initialize可能在任意线程,如果它发生在UI线程,而且initialize做了很多工作的话,可能导致主线程阻塞。预测哪个线程会先使用一个类是不可靠的,所以强制一个固定线程去触发类的initialize方法是不现实的。
  14. 原因二,你不能控制一个类什么时候initialize,它是确定会在一个类被第一次使用之前调用,但是假设它会在某个固定时间执行是不可靠的,runtime可能会有所更新,导致细微改变类的初始化方式,那么你对类已经初始化完毕的设想可能是错误的。
  15. 原因三,比较特殊,就是两个或多个类之间的初始化方法中出现了内部数据的相互调用,可能会出现,一个类需要另一个类初始化完毕,但是第二个类还依赖于第一个类的初始化完毕,造成了两个类之间的相互等待,和循环引用有一定程度的类似。
  16. 综上,initialize中并不适合做大量的工作,尤其是调用其他类或自己的方法,如果自己的方法必须依赖自己已经初始化完毕,那么也会出现上述问题,所以initialize的正确用法是去初始化那些,无法在编译期间赋值的全局静态变量和全局变量,比如static NSMutableArray *kSomeObjects,因为这些OC对象必须等到runtime激活后才能使用。
  17. 所以始终保持initializeload方法简洁是一个好习惯,能避免大量的奇葩问题。

Item52 Remember that NSTimer Retains Its Target

1.NSTimer是一个常用的类,用来定时执行一些方法,或循环执行一些方法,它是需要和一个run loop关联的,你既可以在当前run loop预设置,也可以自己创建NSTimer对象自行设置。
2.NSTimer预设置的方法需要传入target和selector,timer会retain target,而会在timer失效时release它,一个timer可以通过直接调用invalidate(一般是循环的)或者启动后(一般是一次性的)就会失效。
3.因为timer会retain target,所以在循环执行时特别容易出现循环引用,如下,startPolling后,便会出现EOCClass和timer相互引用的结果,目前想解决这一问题,只能通过要求调用方自行调用stopPolling,但如果这时一个对外使用的类的话,这是不可控的;而寄希望于dealloc去解开这一循环,是不现实的,因为对于循环引用的两个对象,是不会出现一方先释放的。而且如果这一引用存在,会一直循环去执行这个任务,带来的问题不光是内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 #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
}

4.那么要想解决这个问题,又不依赖外部,一个方案是做一个NSTimer的block分类,如下,block被设置为userInfo参数,timer也会对它retain,block要进行一次copy,从stack移到heap上,这在Item37说过,现在的target变成了NSTimer这个类本身,因为NSTimer作为一个类对象,是一个单例,所以不用担心释放问题,虽然也存在循环引用,但是没关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 #import <Foundation/Foundation.h>
@interface NSTimer(EOCBlockSupport)
+(NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
@implementation NSTimer(EOCBlockSupport)
+(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();
  }
}

5.回到新方法的使用,像下面这样直接调用的话,还是会导致循环引用,因为block会retain self,而timer会在userInfo处retain block,而timer本身被self引用,所以正确的做法是做一个self的weak变量,在block中再声明一个strong的临时变量,确保block retain一个weak对象,而在block内部又不会提前释放掉,这也是解决block retain cycle的常见策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//wrong
-(void)startPolling{
  _polTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{
      [self p_doPoll];
  }
  repeats:YES
  ];
}
//right
-(void)startPolling{
  __weak __typeof(self)weakSelf = self;
  _polTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{
      __strong __typeof(weakSelf)strongSelf = weakSelf;
      [strongSelf p_doPoll];
  }
  repeats:YES
  ];
}
6vvqnj09Z6