前言

接上篇:iOS 开发 -《Effective Objective-C 2.0:编写高质量 iOS 与 OS X 代码的 52 个有效方法》读书笔记(2)

第 6 章 块与大中枢派发

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

blcok和函数类似,它是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围的东西。用^来表示,后面接一对大括号,括号里是blcok的实现代码。

  • 格式: 返回类型 (^blockName)(参数){实现代码};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    // 无返回值无参数
    void(^testBlcok)(void) = ^{
        NSLog(@"testBlcok");
    };
    testBlcok(); // print:"testBlcok"

    // 无返回值有参数
    void(^testBlcok)(int a) = ^(int a) {
        NSLog(@"%d", a);
    };
    testBlcok(5); // print:"5"

    // 有返回值有参数
    int (^addBlock)(int a, int b) = ^(int a, int b) {
        return a + b;
    };
    NSLog(@"%d", addBlock(13, 6)); // print:"19"
  • block可以访问局部变量,但是不能修改,如果修改局部变量,需要加__block
1
2
3
4
5
6
7
8
    __block NSInteger count = 0;
    int additional = 5;
    int (^addBlock)(int a, int b) = ^(int a, int b) {
        count = count + 10;
        return a + b + additional;
    };
    NSLog(@"addBlock = %d", addBlock(13, 6)); // print:"24"
    NSLog(@"count = %ld", count); // print:"10"
  • 另外关于block的修饰符应该注意: 1.如果用copy修饰Block,该Block就会存储在堆空间。则会对Block的内部对象进行强引用,导致循环引用。内存无法释放。 解决方法:新建一个指针(__weak typeof(Target) weakTarget = Target )指向Block代码块里的对象,然后用weakTarget进行操作。就可以解决循环引用问题。 2.如果用weak修饰Block,该Block就会存放在栈空间。不会出现循环引用问题。

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

为常用的块类型创建 typedef,主要是为了代码的易读性,用的时候也较为方便。请看下面代码对比:

1
2
3
4
5
6
7
8
// 第一种写法
- (void)testWithBlockString:(NSString *)string withBlock:(void(^)(id dataSource))block;
/* 这种写法非常难记,也很难懂,用的时候不方便 */

// 第二种写法
typedef void (^testBlock)(id dataSource);
- (void)testWithBlockString:(NSString *)string withBlockName:(testBlock)block;
/* 用 typedef 关键字,为常用的块类型起个别名,方便易懂 */

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

iOS开发中,我们经常会异步处理一些任务,然后等任务执行结束后通知相关方法。实现此需求的方法有很多,比如可以选择代理委托,也可以选择blockblock 更轻型,使用更简单,能够直接访问上下文,这样类中不需要存储临时数据,使用block的代码通常会在同一个地方,这样使代码更连贯,可读性好。

1
2
3
typedef void (^testBlock)(id dataSource);
- (void)testWithBlockString:(NSString *)string withBlockName:(testBlock)block;

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

这条讲的比较基础,是iOSblock的循环引用问题。所谓循环引用,就是两个对象相互持有,这样就会造成循环引用。

  • 请看下面代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef void (^testBlock)(id dataSource);
@property (copy, nonatomic) testBlock block;
@property (nonatomic, copy) NSString *blockString;

- (void)testBlock {
    self.block = ^(id dataSource) {
        NSString *blockString = self.blockString;
        NSLog(@"blockString = %@", blockString);
    };
}
  • 代码截图:

block 循环引用

  • 解决方法:
1
2
3
4
5
6
7
- (void)testBlock {
    __weak typeof(self) weakSelf = self;
    self.block = ^(id dataSource) {
        NSString *blockString = weakSelf.blockString;
        NSLog(@"blockString = %@", blockString);
    };
}

但并非所有 block 都会造成循环引用,在开发中,一些同学只要有block的地方就会用__weak来修饰对象,其实没有必要,以下几种block是不会造成循环引用的:

  • 大部分GCD方法
1
2
3
4
    dispatch_async(dispatch_get_main_queue(), ^{
        NSString *blockString = self.blockString;
        NSLog(@"blockString = %@", blockString);
    });

代码解读:因为self并没有对GCD中的block进行持有,所以不会形成循环引用。

  • block 属于另外一个类
1
2
3
4
    [NNHomeViewController testWithBlockName:^(id dataSource) {
        NSString *blockString = self.blockString;
        NSLog(@"blockString = %@", blockString);
    }];

代码解读:同上,block不是被self所持有的。

  • block并不是属性值,而是临时变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- (void)viewDidLoad {
    [super viewDidLoad];
    [self testWithBlock:^{
        NSString *blockString = self.blockString;
        NSLog(@"blockString = %@", blockString);
    }];
}

- (void)testWithBlock:(void(^)(void))block {
    block();
}

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

在 OC 中,如果有多个线程要执行同一份代码,有时可能会出问题。这种情况下,通常要使用锁来实现某种同步机制。

  • GCD 出现之前通常使用两种方法:

第一种:内置的“同步块”

1
2
3
4
5
- (void)synchronizedMethod {
    @synchronized(self) {
        // safe
    }
}

这种写法会根据给定的对象,自动创建一个锁,并等待块中代码执行完毕。执行到这段代码结尾处,锁就释放了。但滥用@synchronized(self)会很大程度上降低代码效率,因此不推荐使用。

第二种:直接使用 NSLock 对象(也可以使用 NSRecursiveLock 递归锁)

1
2
3
4
5
6
7
_lock = [[NSLock alloc] init];

- (void)synchronizedMethod {
    [_lock lock];
    // safe
    [_lock unlock];
}

这种写法也有缺陷,在极端情况下,同步块会导致死锁,另外与 GCD 相比效率也很低。

  • 推荐:GCD 方式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
- (NSString *)testString {
    __block NSString *localTestString;
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        localTestString = self.testString;
    });
    return localTestString;
}

- (void)setTestString:(NSString *)testString {
    dispatch_barrier_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        self.testString = testString;
    });
}

使用同步队列及栅栏块,可以令同步行为更加高效。

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

GCD 出现之前,开发者延迟调用一些方法,或者指定运行方法的线程会用 performSelector,但是在 GCD 出来之后就不需要再使用performSelector了。

  • performSelector 系列的方法:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
  • performSelector 方法存在的缺点

    • 内存管理问题:在ARC下使用performSelector编译器经常出现警告,因为它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。
    • performSelector系列方法所能处理的选择子太过于局限,performSelector的返回值只能是void或对象类型;而且它无法处理带有多个参数的选择子,最多只能处理两个参数。
  • 用 GCD 代替 performSelector 系列方法

  1. 延迟调用方法:
1
2
3
4
5
6
7
// GCD
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];
});

// performSelector
[self performSelector:@selector(doSomething) withObject:nil  afterDelay:5.0];
  1. 指定运行方法的线程:
1
2
3
4
5
6
7
// GCD
dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
});

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

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

这条讲的是什么时候该用 GCD,什么时候不该用GCDGCD技术确实很棒,但GCD并不总是最佳解决方案。比如当我们想取消队列中的某个操作时,或者需要后台执行任务时,这时我们可以用NSOperationQueue,其实NSOperationQueueGCD有很多相像之处。NSOperationQueueGCD之前就已经有了,GCD就是在其某些原理上构建的。GCDC层次的API,而NSOperation是重量级的OC对象。

  • 使用NSOperationNSOperationQueue的好处如下:
    • 取消某个操作。
    • 指定操作间的依赖关系。
    • 通过键值观察机制监控NSOperation对象的属性。
    • 指定操作的优先级。
    • 重用NSOperation对象。

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

dispatch groupGCD的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知,开发者可以拿到结果然后继续下一步操作。

请看以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 创建一个队列组
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        // 添加操作...
        NSLog(@"1%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        // 添加操作...
        NSLog(@"2%@", [NSThread currentThread]);
    });
    dispatch_group_async(group, queue, ^{
        // 添加操作...
        NSLog(@"3%@", [NSThread currentThread]);
    });
    // 收到通知,回到主线程刷新UI
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"回到主线程刷新UI");
    });

多个任务可归入一个dispatch group之中。开发者可以在这组任务执行完毕时获得通知。

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

这条讲的单例模式,即常用的dispatch_once。使用 dispatch_once 可以简化代码并且彻底保证线程安全,我们根本无须担心加锁或同步,另外它没有使用重量级的同步机制,所以也更高效。

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

第 44 条:不要使用 dispatch_get_current_queue

  • dispatch_get_current_queue 函数的行为常常与开发者所预期的不同,此函数已经废弃,只应做调试之用。
  • 由于GCD是按层级来组织的,所以无法单用某个队列对象来描述"当前队列"这一概念。
  • dispatch_get_current_queue 函数用于解决由不可以重入的代码所引发的死锁,然后能用此函数解决的问题,通常也可以用"队列特定数据"来解决。

第 7 章 系统框架

第 45 条:熟悉系统框架

开发者会碰到的主要框架就是Fundation。另外还有个与Fundation相伴的框架,叫做CoreFoundation。除了FundationCoreFoundation,还有很多系统库,其中包括但不限于下面列出的这些:

  • CFNetwork:此框架提供了 C 语言级别的网络通信能力,它将 BSD 套接字(BSD socket)抽象成易于使用的网络接口。而 Foundation 则将该框架里的部分内容封装为OC语言的接口,以便进行网络通信。

  • CoreAudio:此框架所提供的C语言API可以用来操作设备上的音频硬件。

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

  • CoreData:此框架所提供的OC接口可以将对象放入数据库,将数持久保存。

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

  • 要点:

    • 许多系统框架都可以直接使用。其中最重要的是FundationCoreFoundation,这两个框架提供了构架应用程序所需的许多核心功能。
    • 很多常见任务都能用框架来做。例如音频与视频处理、网络通信、数据管理等。
    • 用纯C写成的框架与用OC写成的一样重要,若想成为优秀的OC开发者,应该掌握C语言的核心概念。

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

遍历collection有四种方式。最基本的办法是for循环,其次是NSEnumerator遍历法快速遍历法,最新最先进的方式则是“块枚举法”

  • 四种遍历方法:
 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
    NSArray *testArray = @[@1, @2, @3, @4, @5];
    // for 循环遍历
    for (int i = 0; i < testArray.count; i ++) {
        NSLog(@"testArray[%d] = %@", i, testArray[i]);
    }
    
    // NSEnumerator遍历法
    NSEnumerator *enumerator = [testArray objectEnumerator];
    id object;
    while ((object = [enumerator nextObject]) != nil) {
        NSLog(@"object = %@", object);
    }
    
    // 快速遍历
    for (NSObject *obj in testArray) {
        NSLog(@"obj = %@", obj);
    }
    
    // 块枚举遍历数组
    [testArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"idx = %zd, obj = %@", idx, obj);
    }];

    // 块枚举遍历字典
    NSDictionary *testDic = @{@"name":@"liu zhong ning",@"age":@"25"};
    [testDic enumerateKeysAndObjectsUsingBlock:^(NSString * key,id object,BOOL * stop){
        NSLog(@"testDic[%@] = %@", key, object);
    }];

块枚举法拥有其他遍历方式都具备的优势,而且还能带来更多好处,在遍历字典的时候,还可以同时提供键和值,而且还有选项可以开启并发迭代功能,所以多写点代码还是值的。

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

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

  • 简单的无缝桥接示例:
1
2
3
4
    NSArray *testNSArray = @[@1, @2, @3, @4, @5];
    CFArrayRef testCFArray = (__bridge CFArrayRef)testNSArray;
    NSLog(@"Size of array = %li", CFArrayGetCount(testCFArray));
    // Output:Size of array = 5
  • __bridge:ARC仍然具备这个Objective对象的所有权。
  • __bridge_retained:ARC将交出对象的所有权。
  • __bridge_transfer:C转化为OC

想深入了解无缝桥接技术的童鞋可以点击这里:iOS无缝桥接官方文档

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

  • 实现缓存时应选用NSCache而非NSDictionary对象。因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键。
  • 可以给NSCache对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”,他们仅对NSCache起指导作用。
  • NSPurgeableDataNSCache搭配使用,可实现自动清除数据的功能,也就是说,当NSPurgeableData对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
  • 如果缓存使用得当,那么应用程序的相应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

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

  • 当程序启动的时候,类和分类,必定会调动且仅调用一次load方法。先调用类的load方法,再调用分类的load方法。先调用超类的load方法,再调用子类的load方法。load方法需要实现得精简一些,因为整个应用程序会在执行load方法时都会阻塞。
  • initialize方法会在程序首次用该类之前调用,且只调用一次。initialize是“懒加载”的,如果某个类一直都没有使用,就不会执行该类的initialize方法。initialize方法可以安全使用并调用任意类中的任意方法。initialize方法只应该用来设置内部数据,不应该在其中调用其他方法。
  • 在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与覆写机制。
  • 首次使用某个类之前,系统会向其发送initialize消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类。
  • loadinitialize 方法都应该实现的精简一点,这样有助于保持应用程序的响应能力,也可以减少引入依赖环的几率。
  • 无法在编译器设定的全局变量,可以放在initialize方法里初始化。

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

开发中经常会用到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
37
38
39
40
#import <Foundation/Foundation.h>

@interface NNTimer: NSObject

- (void)startPolling;
- (void)stopPolling;

@end

@implementation NNTimer {
    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

上面这段代码是存在问题的。如果创建了本类的实例,并调用其startPolling方法,那么会如何呢?创建计时器的时候,由于目标对象是self,所以要保留此实例。然而,因为计时器是用实例变量存放的,所以实例也保留了计时器,于是就产生了保留环。

单从计时器本身入手,你会发现很难解决这个问题,那么如何解决这个问题呢?我们可以通过“块”来解决。虽然计时器当前不直接支持块,但是可以用下面这段代码为其添加此功能: .h文件:

1
2
3
4
5
@interface NSTimer (NNBlocksSupport)
+ (NSTimer *)nn_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                          block:(void(^)(void))block
                                       repeats:(BOOL)repeats;
@end

.m文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "NSTimer+NNBlocksSupport.h"

@implementation NSTimer (NNBlocksSupport)

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

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

@end

结束语:由于个人能力有限,这三篇读书笔记难免有错误或不足之处,还望各位道友能不吝赐教,谢谢。

最后安利一下这本书:PDF版