0%

第七章:系统框架(2)

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

优点

1、当系统资源耗尽时,NSCache 可以自动删减缓存,而且还会优先删除最久没有使用的缓存。
2、NSCache 并不会“拷贝”键,而是“保留”它。不拷贝键的原因是:很多时候,键都是由不支持拷贝操作的对象充当的。
3、NSCache 是线程安全的。
4、可以操控缓存删减其内容的时机,有两个与系统资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的“总开销”(overroll cost)。

下面代码演示缓存的用法:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#import <Foundation/Foundation.h>

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

@interface EOCNetworkFetcher : NSObject

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

@end

@implementation EOCClass {
NSCache *_cache;
}

- (id)init {
self = [super init];
if (self) {
_cache = [NSCache new];
// 最多缓存 100 条数据
_cache.countLimit = 100;
// 最大缓存空间 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:cachedData];
}];
}
}
@end

NSPurgeableData

NSPurgeableData 和 NSCache 搭配起来用,效果很好。此类是 NSMutableData 的子类,而且实现了 NSDiscardableContent 协议。如果某个对象所占有的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。当系统资源紧张时可以把保存 NSPurgeableData 对象的那块内存释放掉。NSDiscardableContent 协议定义了名为 isContentDiscarded 的方法,用来查询相关内存是否已释放。
如果需要访问某个 NSPurgeableData 对象,可以调用 beginContentAccess 方法,告诉它现在还不应该丢弃自己所占据的内存。用完之后,调用 endContentAccess 方法,告诉它在必要时可以丢弃自己所占据的内存了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)downloadDataForURLTwo:(NSURL *)url {
NSPurgeableData *cachedData = [_cache objectForKey:url];
if (cachedData) {
[cachedData beginContentAccess];
[self useData:cacheData];
[cachedData 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];
[self useData:purgeableData];
[purgeableData endContentAccess];
}];
}
}

创建好 NSPurgeableData 后,其 “purge 引用计数”会多1,所以无需再调用 beginContentAccess 了,但使用完后必须调用 endContentAccess 方法,将多出来的 “1” 抵消掉。

总结

合理的使用 NSCache 可以提高程序的响应速度。

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

有时候,类必须先执行某些初始化操作才能正常使用。在 Objective-C 中,绝大多数的类都继承自 NSObject 这个根类,该类有两个方法,可用来实现这种初始化操作。

load

对于加入运行期系统中的每个类(class)及分类(category)来说,必定会调用此方法,而且仅调用一次。如果分类和其所属的类都定义了 load 方法,则先调用类里的,再调用分类的。

执行 load 方法时,运行期系统处于“脆弱状态”(fragile state)。在执行子类的 load 方法之前,必定会先执行所有父类的 load 方法,而如果代码还依赖其他程序,那么程序库里相关类的 load 方法也必定会先执行。然而,根据某个给定的程序库,却无法判断出其中各个类的载入顺序。因此,在 load 方法中使用其他类是不安全的。

load 方法不像普通方法那样,它不遵从那套继承规则。如果某个类本身没实现 load 方法,那么不管其各级父类是否实现此方法,系统都不会调用。此外,分类的其所属的类里,都可能出现 load 方法。此时两种实现代码都会调用,类的实现要比分类的实现先执行。

load 方法务必实现得精简一些,也就是要尽量减少其所执行操作,因为整个程序在执行 load 方法的时候都会阻塞。如果 load 方法中包含繁杂的代码,那么应用程序在执行期行就会变得无响应。也不要写等待锁,也不要调用可能会加锁的方法。

initialize

只有在第一次给该类发送消息之前会调用 initialize 方法。

与 load 方法不同,运行系统在执行 initialize 方法时,是处于正常状态的。因此,从运行期系统完整角度上来讲,此时也可以安全使用并调用任意类中的任意方法。而且,运行期系统也能确保 initialize 方法在“线程安全的环境”中执行。这就是说,只有执行 initialize 的那个线程可以操作类或类实例。其他线程都要先阻塞,等着 initialize 执行完。

跟其他方法一样,如果某个类未实现 initialize 方法,而父类实现了,那么就会运行父类的代码。initialize 遵循通常的继承规则。所以应该在 initialize 方法中判断是否是当前类,代码如下:

1
2
3
4
5
+ (void)initialize {
if(self == [EOCBaseClass class]) {
// doSomething
}
}

最后,initialize 和 load 一样,都应该实现的精简一些。可以用来初始化一些全局变量,

参考

之前写的文章 iOS开发之理解load和initialize

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

计时器要和“运行循环”(runloop)相关联,运行循环到时候会触发任务。创建 NSTimer 时,可以将其“预先安排”在当前的运行循环中,也可以先创建好,然后由开发者来调度。无论采用哪种方式,只有把计时器放在运行循环里,它才能正常触发任务。

使用 NSTimer 很容易会造成引用循环。看看下面的例子

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
27
28
29
30
31
32
33
34
35
36
#import <Foundation/Foundation.h>

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

// --

#import "EOCClass.h"

@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

上面代码中 self 强引用了 _pollTimer ,而 _pollTimer 也强引用了 self 。所以就造成了引用循环。除非手动调用 stopPolling 这个方法,否则就会出现内存泄漏。但我们无法保证开发者一定会调用这个方法。

解决方法:

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
27
28
29
30
31
32
33
34
35
#import <Foundation/Foundation.h> 
@interface NSTimer (EOCBlocksSupport)

+ (NSTimer *)eoc_timerScheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;

@end

// --

#import "NSTimer+EOCBlocksSupport.h"

@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer *)eoc_timerScheduledTimerWithTimeInterval:(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;
block ? block() : nil;
}

- (void)startPolling {
__weak EOCClass *weakSelf = self;
_pollTimer = [NSTimer eoc_timerScheduledTimerWithTimeInterval:5.0 block:^{
EOCClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
} repeats:YES];
}

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

@end

使用这种方法捕获到 weakSelf ,这样 self 就可以正常释放了,self 释放后, weakSelf 也就变为 nil 。从而打破了引用循环。

补充

在项目中我使用另一种方法也可以用来解决这个问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <Foundation/Foundation.h>

typedef void (^VCHTimerHandler)(id userInfo);

@interface VCHWeakTimer : NSObject

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(VCHTimerHandler)block
userInfo:(id)userInfo
repeats:(BOOL)repeats;

@end

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#import "VCHWeakTimer.h"

@interface VCHWeakTimer()

@property(nonatomic,weak) id target;
@property(nonatomic,assign) SEL selector;

@end

@implementation VCHWeakTimer

- (void)fire:(id)obj {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:obj];
#pragma clang diagnostic pop
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
VCHWeakTimer *weakTimer = [[VCHWeakTimer alloc] init];
weakTimer.target = aTarget;
weakTimer.selector = aSelector;
return [NSTimer scheduledTimerWithTimeInterval:interval
target:weakTimer
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(VCHTimerHandler)block
userInfo:(id)userInfo
repeats:(BOOL)repeats {
NSMutableArray *userInfoArray = [NSMutableArray arrayWithObject:[block copy]];
if (userInfo != nil) {
[userInfoArray addObject:userInfo];
}
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(_timerBlockInvoke:)
userInfo:userInfoArray
repeats:repeats];
}

- (void)_timerBlockInvoke:(NSArray *)userInfo {
VCHTimerHandler block = userInfo[0];
id info = nil;
if (userInfo.count == 2) {
info = userInfo[1];
}
block ? block(info) : nil;
}

@end

总结

直接使用 NSTimer 可能会发生内存泄漏,一定要想办法处理掉这个问题。

全书 · 完

第七章:系统框架(1)

第47条:熟悉系统框架

Foundation

Foundation 框架中的类,使用 NS 这个前缀,此前缀是在 Objective-C 语言用作 NeXTSTEP 操作系统的编程语言时首度确定的。Foundation 框架是 Objective-C 应用程序的基础。Foundation 框架不仅提供了 collection 等基础核心功能,而且还提供了字符串处理这样的复杂功能。

CoreFoundation

CoreFoundation 框架不是 Objective-C 框架,但它却是 Objective-C 应用程序时所应熟悉的重要框架,Foundation 框架中的许多功能,都可以在此框架中找到对应的 C 语言 API。CoreFoundation 与 Foundation 名字相似、联系紧密。能做到“无缝桥接”,可以把 CoreFoundation 框架中的 C 语言数据结构平滑转换为 Foundation 中的 Objective-C 对象,也可以反向转换。比如:NSString 与 CFString 可以互转。

CFNetWork

此框架提供了 C 语言级别的网络通信能力,它将”BSD套接字”(BSD socket)抽象成易于使用的网络接口。而 Foundation 则将该框架里的部分内容封装为 Objective-C 语言的接口,以便于进行网络通信,例如可以用 NSURLConnection 从 URL 中下载数据。

CoreAudio

该框架所提供的 C 语言 API 可用来操作设备上的音频硬件。这个框架属于比较难用的那种,因为音频处理本身就很复杂。所幸由这套 API 可以抽象出另外一套 Objective-C 式的 API,用后者来处理音频问题会更简单些。

AVFoundation

此框架所提供的 Objective-C 对象可用来回放并录制音频及视频,比如能够在 UI 视图类里播放视频。

CoreData

此框架提供的 Objective-C 接口可以将对象放入数据库,便于持久保存。CoreData 会处理数据的获取及存储事宜,而且可以跨越 Mac OS X 及 iOS 平台。

CoreText

此框架提供的 C 语言接口可以高效执行文字排版及渲染操作。

UIKit

我们可能会编写使用 UI 框架的 Mac OS X 或 iOS 应用程序。这两个平台的核心 UI 框架分别叫做 Appkit 及 UIKit,它们都提供了构建在Foundation 与 CoreFoundation 之上的 Objective-C 类。框架里含有 UI 元素,也含有粘合机制,令开发者可将所有相关内容组装为应用程序。

CoreAnimation

CoreAnimation 是用 Objective-C 语言写成的,它提供了一些工具,而 UI 框架则用这些工具来渲染图形并播放动画。开发者编程时可能从来不会深入到这种级别,不过知道该该框架总是好的。CoreAnimation 本身并不是框架,它是 QuartzCore 框架的一部分。然而在框架的国度里,CoreAnimation 仍应算作“一等公民”(first-class citizen)。

CoreGraphics

CoreGraphics 框架以 C 语言写成,其中提供了 2D 渲染所必备的数据结构与函数。例如,其中定义了 CGPoint、CGSize、CGRect 等数据结构,而 UIKit 框架中 UIView 类在确定视图控件之间的相对位置时,这些数据结构都要用到。

总结

系统框架给我们提供了构建应用程序所需的核心功能。
Objective-C 编程经常需要使用底层的 C 语言级 API。好处是可以绕过 Objective-C 运行期系统,从而提供执行速度。
由于 ARC 只负责 Objective-C 对象,所以使用 C 语言级别的 API 时尤其要注意内存管理问题。

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

在编程中经常需要列举 collection 中的元素,当前的 Objective-C 语言有很多种办法实现此功能,比较常用的有,标准 C 语言循环, Objective-C 2.0 的快速遍历,以及“块”循环。

for 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Dictionary
NSArray *anArray = /*...*/;
for (int i = 0; i < anArray.count; i++) {
id object = anArray[i];
// Do something with 'object'
}

// NSDictionary
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'
}

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

for 循环的缺点就是有时需要创建额外的对象才能完成遍历。

在这里,字典与 set 都是”无序的”( unordered ),所以无法根据特定的整数下标来直接访问其中的值。于是,就需要先获取字典里的所有键或是 set 里的所有对象,这两种情况下,都可以在获取到的有序数组上遍历,以便借此访问原字典及原 set 中得值。创建这个附加数组会有额外的开销,而且还会多创建一个数组对象,它会保留 collection 中得所有元素对象。

快速遍历

Objective-C 2.0 引入了快速遍历这一功能。快速遍历语法更简洁,它为 for 循环开设了 in 关键字。这个关键字大幅简化了遍历 collection 所需的语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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'
}

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

这种遍历方式简单且效率高,然而如果在遍历字典时需要同时获取键与值,那么会多出来一步。而且,与传统 for 循环不同,这种遍历方式无法轻松获取当前遍历操作所针对的下标。

基于块的遍历方式

在当前的 Objective-C 语言中,最新引入的一种做法就是基于块来遍历。NSArray、NSDictionary、NSSet 中定义了下面这个方法,可以实现最基本的遍历功能:

1
2
3
4
5
6
// NSArray
- (void)enumerateObjectsUsingBlock:(void(^)(id object, NSUInteger idx, BOOL *stop))block;
// NSDictionary
- (void)enumerateKeysAndObjectsUsingBlock:(void(^)(id key, id object, BOOL *stop))block;
// NSSet
- (void)enumerateObjectsUsingBlock:(void(^)(id object, BOOL *stop))block;

NSArray 对应的块有三个参数,分别是当前迭代所针对的对象、所针对的下标,以及指向布尔值的指针。前两个参数的含义不言而喻。而通过第三个参数所提供的机制,开发者可以终止遍历操作。其他两个类似。
使用下面代码可以遍历数组

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
// NSArray
NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) {
// Do something with 'object'
if (shouldStop) {
*stop = YES;
}
}];

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

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

遍历时可以直接从 block 里获取更多信息。在遍历数组时,可以知道当前所针对的下标。遍历有序 NSSet(NSOrderedSet)时也一样。而在遍历字典时,无须额外编码,即可同时获取键与值,因而省去了根据给定键来获取对应值这一步。用这种方式遍历字典,可以同时得知键与值,这很可能比其他方式快很多,因为在字典内部的数据结构中,键与值本来就是存储在一起的。同时,使用这种方法能够修改 block 的方法名,以免进行类型转换的操作,从效果上讲,相当于把本来需要执行的类型转换操作交给block方法签名来做。

用此方式也可以执行反向遍历。数组、字典、set都实现了前述方法的另一个版本,使开发者可向其传入“选项掩码”(option mask):

1
2
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options usingBlock:(void(^)(id obj, NSUInteger idx, BOOL *stop))block; 
- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)options usingBlock: (void(^)(id key, id obj, BOOL *stop))block;

NSEnumerationOptions 类型是个 enum,其各种取值可用“按位或”(bitwise OR)连接,用以表明遍历方式。

总体来看,block 枚举法拥有其他遍历方式都具备的优势,而且还能带来更多好处。与快速遍历法相比,它要多用一些代码,可是却能提供遍历时所针对的下标,在遍历字典时也能同时提供键与值,而且还有选项可以开启并发迭代功能。

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

使用 “无缝桥接” 技术,可以在定义于 Foundation 框架中的 Objective-C 类和定义于 CoreFoundation 框架中 C 数据结构之间相互转换。

下面代码演示了简单的无缝桥接:

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

转换操作中的 __bridge 告诉 ARC 如何处理所涉及的 Objective-C 对象。__bridge 本身的意思是:ARC 仍然具备这个 Objective-C 对象的所有权。而 __bridge_retained 则与之相反,意味着 ARC 将交出对象的所有权。若是前面那段代码改用它来实现,那么用完数组之后就要加上CFRelease(aCFArray)以释放其内存。与之相似,反向转换可通过 __bridge_transfer 来实现。那么,为什么需要桥接呢?那是因为Foundation 框架中 Objective-C 类所具备的某些功能,是 CoreFoundation 框架中 C 数据结构所不具备的,反之亦然。

第六章:块与大中枢派发(3)

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

dispatch group 是 GCD 的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。通过这个功能可以把将要并发执行的多个任务合为一组,于是调用者就可以知道这些任务何时才能全部执行完毕。

创建 dispatch group

1
dispatch_group_t group = dispatch_group_create();

想把任务分组,有两种办法。

1
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
1
2
3
dispatch_group_enter(dispatch_group_t group);
// task
dispatch_group_leave(dispatch_group_t group);

判断任务完成也有两种方法
第一种方法是同步的,等到所有任务完成,才能继续往下执行。

1
void dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

第二种方法是异步的,当所有的任务执行完成,就会触发这个通知。

1
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

如果想令数组中的每个对象都执行某项任务,并且想等待所有任务执行完毕,那么就可以使用这个GCD特性来实现。同时还可以给任务加上优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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_t dispatchGroup = dispatch_group_create();

NSArray *lowPriorityObject;
NSArray *highPriorityObject;

for (id object in lowPriorityObject) {
dispatch_group_async(dispatchGroup, lowPriorityQueue, ^{
[object task];
});
}

for (id object in highPriorityObject) {
dispatch_group_async(dispatchGroup, highPriorityQueue, ^{
[object task];
});
}

dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{

});

除了像上面这样把任务提交到并发队列之外,也可以把任务提交至各个串行队列中,并用 dispatch group 跟踪其执行状况。如果所有任务都排在同一个串行队列里面,那么 dispatch group 就用处不大了。因为此时,任务总要逐个执行,所以只需在提交完全部任务之后再提交一个块即可,这样做与通过 notify 函数等待 dispatch group 执行完毕后再回调块是等效的。

dispatch_apply

dispatch_apply 也是并发,并且是阻塞的,所以有时候我们完全可以使用 dispatch_apply 来代替 dispatch group 来执行任务。

1
2
3
4
dispatch_queue_t queue = dispatch_queue_create("com.vhuichen.queue", NULL); 
dispatch_apply(count, queue, ^(size_t i) {
//Perform task
});

总结

当有一组任务需要执行时,可以将这一组任务加到 dispatch group 中,当所有任务执行完成后会收到一个通知。

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

单例模式(singleton)是我们常用的一种开发模式,常见的一种写法如下:

1
2
3
4
5
6
7
8
9
+ (instancetype)sharedInstance { 
static id sharedInstance = nil;
@synchronized (self) {
if (!sharedInstance) {
sharedInstance = [[self alloc] init];
}
}
return sharedInstance;
}

也可以通过 GCD 的 dispath_once 来实现,dispath_once 是线程安全的。

1
2
3
4
5
6
7
8
+ (instancetype)sharedInstance { 
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

使用 dispath_once 方式比 @synchronized 方式要快很多

第46条:不要使用 dispatch_get_current_queue

使用 GCD 时,经常需要判断当前代码正在哪个队列上执行,文档提供了这个函数:

1
dispatch_queue_t dispatch_get_current_queue();

iOS6.0 开始已经正式弃用此函数了。这个函数有个典型的错误用法,就是用它来检测当前队列是不是某个特定的队列,试图以此来避免执行同步派发时可能遇到的死锁问题。
下面两个存取方法,用串行队列保证实例变量的访问是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}

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

这种写法的问题在于,getter 方法可能会死锁(当 getter 方法恰好就是 _syncQueue 时)。
可以将上面的代码稍作修改,只需先判断当前队列是否为 _syncQueue 队列,如果是就不派发,直接执行。这样做就可以另其变得“可重入”

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_block_t accessorBlock = ^{
localSomeString = _someString;
};

if (dispatch_get_current_queue() == _syncQueue) {
accessorBlock();
} else {
dispatch_sync(_syncQueue, accessorBlock);
}
return localSomeString;
}

这样做好像是可以解决问题,但有些情况下还是会出现死锁问题,例如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_queue_t queueA = dispatch_queue_create("com.vhuichen.queueA", NULL);  
dispatch_queue_t queueB = dispatch_queue_create("com.vhuichen.queueB", NULL);

dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{ /* ... */ };
if (dispatch_get_current_queue() == queueA) {
block();
} else {
dispatch_sync(queueA, block);
}
});
});

上面的代码依然会出现死锁。也就是说想通过 dispatch_get_current_queue 来避免死锁问题是不可能的。

有的 API 可令开发者指定运行回调时所用的队列,但实际上却会把回调块安排在内部的串行同步队列上,而内部队列的目标队列又是开发者所提供的那个队列,那么就会出现死锁。使用 API 的开发者认为在回调块里调用 dispatch_get_current_queue 返回的“当前队列”,总是调用 API 时指定的那个,但实际返回的却是 API 内部的那个队列。

要解决这个问题,最好的办法是通过 GCD 所提供的功能来设定“队列特有数据”( queue_specific data ),此功能可以把任意数据以键值对的形式关联到队列里。假如根据指定的键值对获取不到关联数据,那么系统会沿着层级体系一直向上找,直到找到数据或者到达根队列为止。看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_t queueA = dispatch_queue_create("com.vhuichen.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.vhuichen.queueB", NULL);

static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);

dispatch_sync(queueB, ^{
dispatch_block_t block = ^{ NSLog(@"no deadlock"); };
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) {
block();
} else {
dispatch_sync(queueA, block);
}
});

使用 “队列特有数据”( queue_specific data )则可以避免由不可重入引发的死锁。

总结

dispatch_get_current_queue 函数无法解决由不可重入引发的死锁问题,但“队列特有数据”( queue_specific data )可以解决此问题。

第六章:块与大中枢派发(2)

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

如果有多个线程要执行同一份代码,那么有时可能会出问题。这种情况下,通常要使用锁来实现同步机制。在GCD出现之前,一般有两种方式可以实现同步

原始方法:synchronized & NSLock

1
2
3
4
5
- (void)synchronizedMethod {
@synchronized (self) {
// Safe
}
}
1
2
3
4
5
6
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod {
[_lock lock];
// Safe
[_lock unlock];
}

滥用 @synchronized(self) 会很危险,因为所有同步块都会彼此抢夺同一个锁。要是有很多个属性都这么写的话,那么每个属性的同步块都要等其他所有同步块执行完毕才能执行。两种方法的使用效率都不高,并且处理不当会造成死锁。

改进方法:串行同步队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_syncQueue = dispatch_queue_create("com.vhuichen.syncQueue", NULL);

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

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

这里有一种方案就是可以把 setter 方法改成异步执行,提升程序的执行速度。

1
2
3
4
5
- (void)setSomeString:(NSString *)someString { 
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}

这里需要考虑的是:执行异步派发时,需要拷贝块。若拷贝块所需的时间明显超过执行块所花的时间,那么这种做法将比原来的更慢。只有当拷贝块所花的时间远低于执行块所花的时间时,可以考虑这种异步方法。

最优方案:dispatch_barrier

事实上,获取值时可以多个同时进行,设置值和获取值不能同时进行。利用这个特点,我们可以对代码再次优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_syncQueue = dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL); 

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

- (void)setSomeString:(NSString *)someString {
// 这是使用 async 还是 sync 取决于 block 的业务逻辑复杂度,上面有解释
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}

上面的代码,我们创建的是一个并行队列。读取操作可以并行,但写入操作是单独执行的,因为给它加了栅栏,代码的执行逻辑如下图

总结

使用GCD实现同步方式,比使用 synchronized 或 NSLock 更高效。

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

performSelector 有几个缺点。

可能会引起内存泄漏

看下面一段代码

1
2
3
4
5
6
7
8
9
SEL selector;
if (/* ... */) {
selector = @selector(newObject);
} else if (/* ... */) {
selector = @selector(copy);
} else {
selector = @selector(someProperty);
}
id ret = [object performSelector:selector];

编译器会发出如下警示信息

1
warning:PerformSelector may cause a leak because its selector is unknown

原因在于,编译器并不知道将要调用的选择子的方法签名及返回值。由于编译器不知道方法名,所以就没办法运用 ARC 的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。

返回值只能是 void 或对象类型

如果想返回整数或浮点数等类型的值,那么就需要执行一些复杂的转换操作。如果返回的是结构体,则不能使用 performSelector 。

传入参数有限制

传入参数必须为对象类型,最多只有两个限制。

改进(GCD)

1
[self performSelectorOnMainThread:@selector(aSelector) withObject:nil waitUntilDone:NO];

上面的功能可以通过 GCD 来实现

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
[self aSelector];
});

其它 performSelector 的方法也一样可以用 GCD 的方法代替。

第43条:掌握 GCD 及 NSOperationQueue 的使用时机

使用 NSOperationQueue 优点

取消某个操作

使用 NSOperationQueue ,想要取消操作队列是很容易的。运行任务之前,可以在 NSOperation 对象上调用 cancel 方法,该方法会设置对象内的标志位,用以表明此任务不需执行,不过,已经启动的任务无法取消。GCD 则无法直接取消。

指定操作间的依赖关系

一个操作可以依赖其他多个操作。开发者能够制定操作之间的依赖体系,使特定的操作必须在另外一个操作顺利执行完毕后方可执行。

通过键值观测机制监控 NSOperation 对象的属性

NSOperation 对象有许多属性都适合通过键值观测机制(KVO)来监听。比如可以通过 isCancelled 属性来判断任务是否已取消,又比如可以通过 isFinished 属性来判断任务是否已完成。

指定操作的优先级

操作的优先级表示此操作与队列中其他操作之间的优先级关系。优先级高的操作先执行,优先级低的后执行。

重用 NSOperation 对象

系统内置了一些 NSOperation 的子类(比如 NSBlockOperation)以供开发者调用,要是不想用这些子类,可以自己创建。这些类就是普通的 Objective-C 对象,能够存放任何信息。对象在执行时可以充分利用存于其中的信息,而且还可以随意调用定义在类中的方法。NSOperation 类符合软件开发中的“不重复”(Don’t Repeat Yourself,DRY)原则。

总结

GCD 操作简单,NSOperation 则功能更多。熟练掌握两种方式,在各种各样的场景中运用自如。

什么是 URL Schemes

URL(Uniform Resoure Locator:统一资源定位器)一般就是我们所说的网址,通过 URL 可以访问我们想要访问的服务和资源。

URL的地址格式排列为:scheme://host:port/path,比如 URL 地址: https://vhuichen.github.io/tags/ ,这里的 https 就是 Schemes 。又比如支付宝的 URL Schemes 是alipay:// ,我们在 Safari 的地址栏输入就可以打开支付宝了。

在iOS系统中,我们可以通过 URL Schemes 开定位APP的位置,通过 URL Schemes 实现APP跳转。一般情况下每个 APP 的 URL Schemes 都是不一样的,如果出现相同的 URL Schemes ,那么最后安装的 APP 的 URL Schemes 有效。

实现

创建两个Project,ProjectA 和 ProjectB ,设置两个项目的 URL Schemes 和项目名相同(这里只做测试用)。具体怎么设置如下图(白名单后面会讲到),这里只放了一张截图。

两个 Project 的实现代码实现如下:

1
2
3
4
5
6
7
// ProjectA
- (IBAction)projectButtonClick:(id)sender {
NSURL *url = [NSURL URLWithString:@"ProjectB://"];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url];
}
}
1
2
3
4
5
6
7
// ProjectB
- (IBAction)buttonClick:(id)sender {
NSURL *url = [NSURL URLWithString:@"ProjectA://"];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url];
}
}

实现点击 ProjectA 的按钮跳转到 ProjectB ,点击 ProjectB 的按钮跳转到 ProjectA。就这样简单几句代码实现了应用之间的跳转。

iOS10 新接口

在iOS10中,废弃了 openURL 这个接口改用下面这个接口

1
- (void)openURL:(NSURL*)url options:(NSDictionary<NSString *, id> *)options completionHandler:(void (^ __nullable)(BOOL success))completion NS_AVAILABLE_IOS(10_0) NS_EXTENSION_UNAVAILABLE_IOS("");

字典参数 options 目前有一个关键字 UIApplicationOpenURLOptionUniversalLinksOnly 对应的值为 BOOL 类型。默认情况下 值为 NO。如果设置成 YES,那么只能通过 URL 对应的APP才能打开这个链接,如果没有安装对应的APP,那么 completionHandler 中就会返回 NO。默认情况下是通过 Safari 来打开这个 URL 的。

白名单

iOS9 开始新增了白名单,当没有添加白名单时 调用 canOpenURL: 会返回 NO 。并打印出log信息

-canOpenURL: failed for URL: “ProjectB://“ - error: “This app is not allowed to query for scheme projectb”

添加白名单只需要在 Info.plist 添加 LSApplicationQueriesSchemes 关键字,选择数组类型并设置 URL Schemes 的值。具体可以看上图。

传参

和我们平时用的 URL 一样,也可以在 URL Schemes 后面加参数,例如:

1
2
3
4
5
6
7
// ProjectA
- (IBAction)projectButtonClick:(id)sender {
NSURL *url = [NSURL URLWithString:@"ProjectB://title=mytitle&content=mycontent"];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url];
}
}

注意:参数之间不能有空格

URL Schemes 对应的APP可以在回调函数中处理传过来的参数。

回调函数

当应用是通过 URL Schemes 启动时,会调用相应的回调函数,系统提供了三个回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// iOS9及以上
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
NSLog(@"B0:%@",url);
return YES;
}

// iOS8
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
NSLog(@"B1:%@",url);
return YES;
}

// iOS8
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
NSLog(@"B2:%@",url);
return YES;
}

iOS9及以上会优先调用第一个方法,iOS9以下会优先调用第二个方法。
这里我们接收到的 url 中就包含了参数信息。

UIApplicationOpenURLOptionsKey

最新的回调接口提供了字典参数,这个字典有三个 key

1
2
3
UIKIT_EXTERN UIApplicationOpenURLOptionsKey const UIApplicationOpenURLOptionsSourceApplicationKey NS_AVAILABLE_IOS(9_0);   // value is an NSString containing the bundle ID of the originating application
UIKIT_EXTERN UIApplicationOpenURLOptionsKey const UIApplicationOpenURLOptionsAnnotationKey NS_AVAILABLE_IOS(9_0); // value is a property-list typed object corresponding to what the originating application passed in UIDocumentInteractionController's annotation property
UIKIT_EXTERN UIApplicationOpenURLOptionsKey const UIApplicationOpenURLOptionsOpenInPlaceKey NS_AVAILABLE_IOS(9_0); // value is a bool NSNumber, set to YES if the file needs to be copied before use

默认情况下,会传入两个参数

1
2
3
4
{
UIApplicationOpenURLOptionsOpenInPlaceKey = 0;
UIApplicationOpenURLOptionsSourceApplicationKey = "com.xxxx.ProjectB";
}

UIApplicationOpenURLOptionsSourceApplicationKey 传入的是原应用的Bundle ID
我们可以通过判断这个 Bundle ID 来决定执行哪项操作以及返回值。例如我们可以这样写

1
2
3
4
5
6
7
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if([options[UIApplicationOpenURLOptionsSourceApplicationKey] isEqualToString:@"com.xxxx.ProjectB"]) {
// do something
return YES;
}
return NO;
}

疑问:这里我测试发现,不管返回值是否为真,结果都一样。

新建一个 Workspace

先创建一个名为 MultiProject 的 .xcworkspace 文件 ,放到 MultiProject 这个文件夹下。

将 Project 添加到 Workspace 中

方法一:添加已创建好的 Project

先创建一个名为 ProjectA 的 Project。

创建好 Project 后。打开 MultiProject.xcworkspace 文件。

点击 File -> Add Files to “Workspace Name”,找到刚创建项目的 ProjectA.xcodeproj 文件,添加。此时 ProjectA 已经加到 MultiProject 这个工作空间下了。

方法二:创建 Project 时就添加到 Workspace 中

在创建 Project 整个过程的最后一步,会是这样的界面

在红色框中选择对应的 Workspace,点击 Create 后,刚创建的 Project 就添加到 Workspace 中了。

注意:一般会将创建好的 Project 放到 Workspace 目录下。

CocoaPods 安装

在 MultiProject.xcworkspace 文件的目录下创建 Podfile 文件。内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
platform :ios, '8.0'
workspace 'MultiProject.xcworkspace'

target 'ProjectA' do
project 'ProjectA/ProjectA.xcodeproj'

pod 'SDWebImage'
pod 'iVersion'

end

target 'ProjectB' do
project 'ProjectB/ProjectB.xcodeproj'

pod 'SDWebImage'
pod 'AFNetworking'
pod 'iVersion'

end

最后在命令行中进入该目录,执行 pod install 命令,OK 搞定。

最终的目录文件如下:

项目内结构如下:

第六章:块与大中枢派发(1)

第37条:理解“块”这一概念

块与函数类似,只不过是直接定义在另一个函数里,和定义他的那个函数共享一个范围内的东西。
块类型的语法结构如下:

1
return_type (^block_name)(parameters)

变量捕获

block 可以捕获外部变量,例如:

1
2
3
4
5
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b + additional;
};
int add = addBlock(2, 5);

block 捕获 additional 变量,仅仅是捕获 additional 那一刻的值,捕获了之后,如果外部 additional 的值改变了,此时并不会影响 block 内部 additional 的值,因为这个值是一个常量,分别存放在两个不同的内存中,是互不干扰的。如果尝试去修改此时 block 内部的additional 变量的值,编译器会报错。
事实上,在 ARC 环境下,block 外部的 additional 变量是存放在栈中的,而 block 内部的 additional 变量则是存放在堆中的。
那么,如果需要 block 内外共享一份内存呢?这时可以给变量加上 __block 关键字。

__block 关键字修饰变量

下面用 __block 关键字修饰 additional 变量,那么当外部的 additional 变量改变时,里面的 additional 值也会改变。因为这两个是同一个值。

1
2
3
4
5
6
__block int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
additional = 1;
return a + b + additional;
};
int add = addBlock(2, 5);

用 __block 修饰的变量存放在堆中,和 block 中的 additional 共享同一份内存,是同一个数据。

引用循环

如果在 block 中引用了某个对象,比如self,而这个对象正好直接或者间接引用了 block ,那么就会造成引用循环。
所以一般在 block 中引用的变量都会使用弱引用。

块的内部结构

块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,该指针叫做isa。其余内存里含有块对象正常运转所需的各种信息。下图描述了块对象的内存布局。

在内存布局中,最重要的就是invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void *型的参数,此参数代表块。

descriptor 变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了 copy 与 dispose 这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比方说,前者 copy 要保留捕获的对象,而后者 dispose 则将之释放。

block 会把它所捕获的所有变量都拷贝一份,拷贝的是指向这些对象的指针变量。invoke函数为何需要把块对象作为参数传进来呢?原因就在于,执行块时,要从内存中把这些捕获到的变量读出来。

全局块、栈块及堆块

定义块时,其所占的内存区域是分配在栈中的。这就是说,块只在定义他的那个范围内有效。例如,下面这段代码会有问题:

1
2
3
4
5
6
7
8
9
10
11
void (^block)();
if ( /* ... */ ) {
block = ^{
NSLog(@"Block A");
};
} else {
block = ^{
NSLog(@"Block B");
};
}
block();

上面两个 block 都是分配在栈中的,当离开了作用域后,就会将其释放掉,也就是两个 block 只在 if else 内有效。所以离开了 if slse 后在执行 block的话就可能会出问题。若编译器未覆写待执行的 block,则程序照常运行,若覆写,则程序崩溃。

其实这就是为什么 block 属性要使用 copy 修饰的原因。给 block 发送 copy 消息将其拷贝。这样就可以把 block 从栈复制到堆了。拷贝后的 block,可以在定义它的范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增对象的引用计数。

给上面的 block 发送 copy 消息就可以保证程序可以正确运行

1
2
3
4
5
6
7
8
9
10
11
void (^block)();
if ( /* ... */ ) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}
block();

此时的 block 是分配到堆的,这样在 if else 外也可以使用。

全局块

这种块不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到的时候于栈中创建。另外,全局块的拷贝操作是个空操作,因为全局块绝不可能为系统所回收。这种块实际上相当于单例。

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

此 block 所需的全部信息都能在编译期确定,所以可把它做成全局块。

要点

块可以分配在栈、堆或者全局上。分配在栈上的块可以拷贝到堆里,就和标准的 Objective-C 对象一样具备了引用计数。

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

一开始我们定义 block 是这样的

1
2
3
int (^variableName)(BOOL flag, int value) = ^(BOOL flag, int value) {
return someInt;
};

这样做会有两个不友好的问题

不易读

如果我们提供的接口中有好几个 block ,每个 block 中又有好几个参数,这样会感觉比较难读。
解决方法是给 block 类型定义一个别名

1
2
3
4
5
typedef int (^EOCSomeBlock)(BOOL flag, int value);

EOCSomeBlock block = ^(BOOL flag, int value) {
return someInt;
};

这样使用起来就会简介很多。

不易修改

当打算重构 block 的类型签名时,比方说,要给原来的 completion handler block 再加一个参数,如果没有使用别名的话,那么我们需要将所有使用了该 block 的地方都修改,这样显得过于繁杂。如果使用了别名的话,那么只需修改类型定义语句即可。

总结

当要在多个地方使用同种签名的 block 时,应该给该 block 定义一个别名,然后在需要的地方使用该别名定义 block 。

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

程序在执行任务时,通常需要 “异步执行” ,这样做的好处在于:处理用户界面的显示及触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。某些情况下,如果应用程序在一定时间内无响应,那么就会自动终止。“系统监控器”(system watchdog)在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。

通常有两种方式可以处理异步代码

delegate

使用 delegate 会使代码变得分散,当一个对象同时接收多个同种类型对象的委托时,还需要在委托方法中判断是哪个对象传来的委托。那么代码会变的更加复杂。delegate 一般用在一个委托对象有多个委托事件的情况下,比如:UITableView,其他情况可以使用 block 来实现。

block

用 block 处理起来代码会变的更加清晰。block 可以令这种API变得更紧凑,同时也令开发者调用起来更加方便。

1
2
3
4
5
6
7
8
9
- (void)vch_successWithComplete:(VCHAddNewDeviceComplete)complete failure:(VCHFailure)failure {
[self vch_startWithComplete:^(id object) {
// do something
complete();
} failure:^(NSString *error) {
// do something
failure(error);
}];
}

这里我的处理方式是将成功和失败分开处理,也可以用一个 block 来处理两个两种情况,两种方法均有优劣。具体可多看看官方的做法。

总结

在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。使代码变得更加紧凑。

第40条:用 block 引用其所属对象时不要出现引用循环

书中的例子比较长,我用项目中的一部分代码来替代,意思是一样的

1
2
3
4
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
[self queryFence];
}];
[self.tableView.mj_header beginRefreshing];

上面的代码会出现引用循环,self -> mj_header -> block -> self 。这个是初学时很容易犯的错误。这种情况下有两种比较常用的方法可以解决这个问题,一种就是用完 block 后,立即将其释放,另一种就是使用 __weak 关键字修饰某一环节。这里我使用第二种方法,代码如下

1
2
3
4
5
__weak typeof(self) weakSelf = self;
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
[weakSelf queryFence];
}];
[self.tableView.mj_header beginRefreshing];

此时 block 弱引用了 self ,这个循环也就被打破了。

总结

如果 block 所捕获的对象直接或间接的保留了 block 本身,那么就需要解除引用循环。

微信登录

注册 微信开放平台 账号

地址:微信开放平台
随后创建一个应用,提交给微信审核(微信审核比较慢,目前已经几天还在审核中)。

开发者资质认证

微信的原话是这样的

微信开放平台帐号的开发者资质认证提供更安全、更严格的真实性认证、也能够更好的保护企业及用户的合法权益
开发者资质认证通过后,微信开放平台帐号下的应用,将获得微信登录、智能接口、第三方平台开发等高级能力
认证有效期:一年,有效期最后三个月可申请年审即可续期
审核费用:中国大陆地区:300元,非中国大陆地区:99美元

这里要注意的是,文档里面却写着不需要收费

3.开放平台移动应用微信登陆目前是否收费?
答:“微信登录”和第三方网站共享微信庞大的用户价值,同时为微信用户提供更便捷服务和更优质内容,实现双向共赢,目前不收取任何费用。

总之,需要先交钱认证。认证的时候还要提供 N 多资料。审核有点繁琐,写错了就要重填,有3次免费修改的机会。

集成SDK

直接通过 CocoaPods 集成

1
pod "WechatOpenSDK"

配置工程文件

这里要设置白名单以及 URL Scheme 。微信 的 URL Scheme 就是 APPID

代码

这里我自定义了一个 WXLoginHelper 类,将所有的处理逻辑都放在里面。
我们需要先重写 application 的两个委托,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <WXApi.h>
#import "WXLoginHelper.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[WXLoginHelper registerApp:kWXAppID appSecret:kWXAppSecret];
return YES;
}

// ios9
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [WXLoginHelper handleOpenURL:url];
}

// ios8
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
return [WXLoginHelper handleOpenURL:url];
}

然后在 WXLoginHelper 中实现 WXApiDelegate 委托,有两个方法,目前只需要关注 onResp 方法即可

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
27
28
29
30
31
#import "WXLoginHelper.h"

@implementation WXLoginHelper

- (void)onResp:(BaseResp *)resp {
SendAuthResp *sendAuthResq = (SendAuthResp *)resp;
NSLog(@"resp.code = %@", sendAuthResq.code);

if (!sendAuthResq.errCode) {
NSString *urlString = [NSString stringWithFormat:@"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%@&secret=%@&code=%@&grant_type=authorization_code", self.appId, self.appSecret, sendAuthResq.code];
id json = [self getDataWithURLString:urlString];
NSLog(@"json : %@", json);

if (json) {
NSString *access_token = json[@"access_token"];
NSString *openid = json[@"openid"];

NSString *urlString = [NSString stringWithFormat:@"https://api.weixin.qq.com/sns/userinfo?access_token=%@&openid=%@", access_token, openid];
id json1 = [self getDataWithURLString:urlString];

NSLog(@"json1 : %@", json1);
if (json1) {
NSString *icon = json1[@"headimgurl"];
NSString *name = json1[@"nickname"];
self.callback ? self.callback(openid, icon, name) : nil;
}
}
}
self.callback = nil;
}
@end

这里的 callback block 会将 openid,icon,name,返回给调用者。

需要注意的是不要直接将从微信拿到的图片链接传给服务器,因为这个链接可能会失效。我的处理方式是,从这个链接拿到图片,然后将图片传给服务器。

最后在点击了微信登录按钮后给微信发送消息

1
2
3
4
5
6
7
8
+ (void)sendRepWithCallback:(void (^)(NSString *, NSString *, NSString *))callback {
[WXLoginHelper sharedInstance].callback = callback;

SendAuthReq *req = [[SendAuthReq alloc] init];
req.scope = @"snsapi_userinfo";
req.state = @"text";
[WXApi sendReq:req];
}

用户点击授权或者取消都会通过 onResp 方法告诉调用者授权状态。

QQ登录

注册 腾讯开发者平台 账号

地址:腾讯开发者平台

创建一个应用并提交审核(QQ审核很快,当天就通过了)。

和微信不一样,QQ登录目前不需要付费

集成SDK

下载 SDK

找了好久都没发现可以通过 CocoaPods 集成,所以只能手动。下载地址
下载好的SDK里面包含:环境搭建、API说明、Demo、framework 四个文件。

集成

将 TencentOpenAPI.framework 加到项目,然后添加依赖库 “SystemConfiguration.framework”。

配置工程文件

工程配置和微信一样,可以看上图。这里稍微有点区别的就是 QQ 的 URL Scheme = tencent + appid 。

代码

代码逻辑跟微信差不多。这里我创建了 QQLoginHelper 类用来处理相关的逻辑,遵守 TencentSessionDelegate 这个协议,主要看协议里面的这个两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)tencentDidLogin {
if (_tencentOAuth.accessToken) {
// 回调方法:- (void)getUserInfoResponse:(APIResponse *) response
[_tencentOAuth getUserInfo];
} else {
NSLog(@"accessToken 获取失败");
}
}

- (void)getUserInfoResponse:(APIResponse *)response {
NSLog(@"response :%@",response.jsonResponse);
if (response.jsonResponse && self.callback) {
self.callback(_tencentOAuth.openId, response.jsonResponse[@"figureurl_qq_2"], response.jsonResponse[@"nickname"]);
}
self.callback = nil;
}

登录成功会调用 tencentDidLogin 方法,然后在方法中调用 getUserInfo 方法获取用户信息。最后会通过 block 将获取到的信息返回。

点击QQ登录,通过下面的代码给QQ发送消息,调用方式如下

1
2
3
4
5
6
7
8
+ (void)sendRepWithCallback:(void (^)(NSString *, NSString *, NSString *))callback {
QQLoginHelper *helper = [QQLoginHelper sharedInstance];
helper.callback = callback;

helper->_tencentOAuth = [[TencentOAuth alloc] initWithAppId:helper.appId andDelegate:helper];
helper->_permissionArray = [NSMutableArray arrayWithObjects: kOPEN_PERMISSION_GET_SIMPLE_USER_INFO,kOPEN_PERMISSION_GET_INFO,kOPEN_PERMISSION_GET_USER_INFO,nil];
[helper->_tencentOAuth authorize:helper->_permissionArray inSafari:NO];
}

注意

Demo 中我用 block 回调获取到的信息,而我是使用单例处理的,所以 block 中的对象不能被强引用,虽然我调用完 block 后将其释放掉,但有时候回调函数可能并没有执行,那么这时就可能会出现内存泄漏。所以最好的方法是对 block 中的对象使用弱引用。代码如下:

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
- (IBAction)wechatClick:(id)sender {
if ([WXLoginHelper isWXAppInstalled]) {
// 使用 __weak 可以处理特殊情况下内存泄漏的问题,比如跳转到微信后没有再跳转回来
__weak typeof(self) weakSelf = self;
[WXLoginHelper sendRepWithCallback:^(NSString *openid, NSString *icon, NSString *name) {
__strong typeof(self) strongSelf = weakSelf;
[strongSelf thirdPartLoginOpenId:openid userIcon:icon userName:name type:1];
}];
} else {
NSLog(@"没有安装微信客户端");
}
}

- (IBAction)qqClick:(id)sender {
if ([QQLoginHelper isQQInstalled]) {
// 使用 __weak 可以处理特殊情况下内存泄漏的问题,比如跳转到QQ后没有再跳转回来
__weak typeof(self) weakSelf = self;
[QQLoginHelper sendRepWithCallback:^(NSString *openid, NSString *icon, NSString *name) {
__strong typeof(self) strongSelf = weakSelf;
[strongSelf thirdPartLoginOpenId:openid userIcon:icon userName:name type:2];
}];
} else {
NSLog(@"没有安装QQ客户端");
}
}

问题

1、同一个QQ账号,iOS、Android 手机登录获取到的 OpenID 不相同。
原因:创建的应用必须拥有相同的 APPID。

Demo

Demo 有在不断的优化,部分代码可能会和上面的不一样。