Effective Objective-C 2.0 笔记(三)

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

block也是对象,也有引用计数,因此要避免循环引用,一个比较隐蔽的循环引用的例子是:

-(void)anInstanceMenthod{
    void (^someBlock)() = ^{
        _anInstanceVar = @"something";
        //...
    }
}

注意,通过下划线使用类的成员也会捕获self对象。其实这个在Xcode中会给出警告,所以只要注意每一个警告,还是很容易避开的。

书中有一个例子是错误的或者是过时的:

void (^block)();
if(/*some condition*/){
    block = ^{
        NSLog(@"block A");
    }
}else{
    block = ^{
        NSLog(@"block B");
    }
}
block();

书中说必须使用copy来将block 复制到堆中,实际上,在block=这句代码中,已经将block复制到了堆中。可以通过打印对象来查看。

可以通过以下几个问题理解block:

Block是继承自NSObject的么?

typedef void(^TestBlock)(void);

TestBlock b = ^{
    //
};

if([b isKindOfClass:[NSObject class]]){
    NSLog(@"TestBlock is subclass of NSObject.");
}else{
    NSLog(@"TestBlock is not subclass of NSObject.");
}

//...TestBlock is subclass of NSObject.

block中不仅有isa指针,并且是继承自NSObject的。

Block的捕获规则?

int globalA = 1;
static int globalB = 1;

{//in function
    int localA = 1;
    static int localB = 1;
    
    TestBlock b = ^{
        globalA ++;
        globalB ++;
//        localA ++;
        localB ++;
        
        NSLog(@"in block: globalA:%d, globalB:%d, localA:%d, localB:%d", globalA, globalB, localA, localB);
    };
    
    globalA ++;
    globalB ++;
    localA ++;
    localB ++;
    NSLog(@"out block: globalA:%d, globalB:%d, localA:%d, localB:%d", globalA, globalB, localA, localB);
    
    b();
}

对于全局变量和静态变量,block捕获的是指针,变量的任何修改都反映在block内外。

对于局部变量,如果没有使用__block参数修饰,block捕获的是值,block内不能修改,block外修改不反应在block内。也就是说捕获以后就是两个变量了。

{
    Person *p = [[Person alloc] init];
    p.name = @"1234";
    
    TestBlock b = ^{
        NSLog(@"in block before: block name:%@", p.name);
        p.name = @"4321";
        NSLog(@"in block after: block name:%@", p.name);
    };
    
    p.name = @"6789";
    NSLog(@"out block: block name:%@", p.name);

    b();
    
    NSLog(@"out block after block: block name:%@", p.name);
}

/* output
2018-08-27 12:21:37.755459+0800 test[5184:141812] out block: block name:6789
2018-08-27 12:21:37.755573+0800 test[5184:141812] in block before: block name:6789
2018-08-27 12:21:37.755643+0800 test[5184:141812] in block after: block name:4321
2018-08-27 12:21:37.755709+0800 test[5184:141812] out block after block: block name:4321
*/

对于对象,就直接是指针捕获了,block内外的修改都能互相影响。但是,p指向的对象内容可以修改,p指向的对象不能修改。这一点和值变量是逻辑一致的。

3种block有什么区别?

Block也是一个类簇,如果类簇听着别扭,可以叫一个类的继承体系。实际的block类型是以下3种:_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。

先看例子:

1)

int a = 1;
void(^block)(void) = ^{
    //expressions with captured val
    NSLog(@"%d", a);
};
NSLog(@"%@", block);

/* output
2018-08-27 14:08:18.042303+0800 test[6768:195779] <__NSMallocBlock__: 0x60000005c020>
*/

2)

static int globalA = 10;
{
    void(^block)(void) = ^{
        //expressions with captured val
        NSLog(@"%d", globalA);
    };
    NSLog(@"%@", block);
}
/* output
2018-08-27 14:09:43.681282+0800 test[6801:197501] <__NSGlobalBlock__: 0x1034fe0b8>
*/

第二个例子比较好理解,因为引用了Global的变量,所以生成了Global的block;第一个例子只是捕获了局部变量,为啥是Malloc的呢?说好的Stack呢?LLVM ARC文档上说,把一个stack类型的block赋值给一个变量相当于给一个strong类型的变量赋值,会发生将stack block copy到堆上过程。因此打印block就是Malloc类型的。如果想要看到Stack类型,需要使用弱引用:

static int globalA = 10;
{
    __weak void(^block)(void) = ^{
        //expressions with captured val
        NSLog(@"%d", a);
    };
    NSLog(@"%@", block);
}
/* output
2018-08-27 14:16:34.158457+0800 test[6930:202303] <__NSStackBlock__: 0x7ffee7086a68>
*/

所以规则就是捕获的变量声明周期如果是全局的,就是Global的,捕获的变量生命周期如果是局部的,那么创建初期是Stack的吗,一旦给被其他变量引用就编程Malloc的了。

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

不使用typedef,每次要这样写:

int (^variableName)(BOOL flag, int value) = ^(BOOL flag, int value){
    //...
}

使用typedef可以这样写:

typedef int(^EOCSomeBlock)(BOOL flag, int value);
EOCSomeBlock block = ^(BOOL flag, int value);

好读,好修改。

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

异步调用可以使用委托回调,也可以使用block回调。block的好处是可以捕获上下文,处理更方便,使用委托就得自己保存这些上下文,在委托回调的时候拿出来用。但是block需要注意循环引用问题。

对于错误的处理,书中建议将成功和失败放在同一个block中,有些第三方库将成功的回调和失败的回调分开,这时候如果在成功的block回调后续有错误发生,还需要写错误处理的代码,可能和失败block中的代码重复。

使用block的另外一个缺点是,可能会造成很多层嵌套,代码不好读,内存管理困难。

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

这个在第37条中已经有了。主要是要注意每一个编译警告。

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

常见的一种多线程同步方式是@sychronized(self),这种锁锁的是self,如果对象有多个方法需要上锁,那么对于没有多线程问题的两个方法,同时锁self会造成性能下降。这时候应该使用多个NSLock。还有一个NSRecursiveLock,能保证一个线程多次获取一个锁不造成死锁。

OC中提倡通过GCD来进行线程同步。如果想让多线程顺序的访问一个资源,通过一个同步串行队列就可以了。

如果涉及到读写锁,使用barrierAPI,使用dispatch_barrier_async函数添加的任务,只能单独执行,可以用于写,使用dispatch_async向队列添加的任务,可以用于读。

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

作者提倡使用GCD,主要还是从安全角度出发。performSelector提供的灵活性在一些框架级代码中还是常见的。

performSelector系列函数能够延迟方法的调用,根据行为动态执行方法,可以选择其他线程执行方法。

performSelector的缺陷是ARC无法插入适当的内存管理方法,参数和返回值有限制。

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

GCD和NSOperationQueue的区别:

NSOperationQueue中可以指定依赖关系,内建支持执行前取消,可以通过KVO获得任务状态,支持任务级别的优先级。

总之NSOperationQueue的特性比GCD多,但是GCD比较直观,简洁。因此很多时候还是使用GCD。

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

通过把多个异步任务放到一个Dispatch Group中,可以在group上等待这些线程执行完毕。比如一组相关的网络请求,希望等请求全部返回后渲染页面,就可以使用。

Dispatch Group也支持非阻塞等待,通过dispatch_group_notify可以在异步任务执行完成后得到回调。

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

这里给出一个单例的正确创建方式:

@interface MyClass : NSObject

+(instancetype)sharedInstance;
+(instancetype)init NS_UNAVAILABLE;
+(instancetype)copy NS_UNAVAILABLE;

@end


@implementation MyClass

+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static MyClass *sharedInstance;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[MyClass alloc] initPrivate];
    });
    
    return sharedInstance;
}

-(instancetype)initPrivate{
    self = [super init];
    if (self){
    }
    return self;
}

@end
第46条 不要使用dispatch_get_current_queue

这个函数从iOS6.0起已经弃用了。因为队列有层级关系,所以这个检查并不能避免死锁。

第47条 熟悉系统框架

到目前为止,iOS所有的第三方库都还是静态库,但是系统的框架是动态库。

Foundation,AVFoundation,CoreData,CoreAnimation是OC语言的,CFNetwork,CoreAudio,CoreText,CoreGraphics是C语言的。如果使用C语言的API,就失去了OC的runtime支持,需要自己管理内存。所以应该尽量使用高层的API。

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

对于一个数组的遍历,可以用下边这种方式:

for(int i = 0; i < [arr count]; i++){
    NSString *ele = arr[i];
    NSLog(@"%@", ele);
}

这种是最“老式”的遍历方式,通过index获取元素。但是对于NSSet这样无序的集合遍历,就有点麻烦,一般是先将Set转换成数组,再遍历。

“新式”的遍历方式可以解决这一问题:

for(NSString *ele in arr){
    NSLog(@"%@",ele);
}

使用for…in语句,可以直接拿到元素,这样对于无序的集合也可以直接遍历了。但是如果遍历的同时还想知道index怎么办?一般思路是这样的,如果一个有序的集合需要知道index,那么就使用“老式”的遍历方式,对于无序的集合,根本没有index,直接使用“新式“的遍历方式即可。

但是还有更两全其美的方法:

[arr enumerateObjectsUsingBlock:^(NSString*  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSLog(@"%lu, %@", (unsigned long)idx, obj);
}];

使用集合类型的enumerateObjectsUsingBlock方法,对于有序集合,同时返回index和index对应的元素,对于无序的集合,就没有index参数。

如果自己写的类想支持for…in遍历,只要遵从并实现NSFastEnumeration协议即可。

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

使用C语言的API创建的数据类型是需要手动内存管理的,幸运的是Foundation框架中的“无缝桥接”(toll-free bridge)技术能帮助我们在这两种内存管理之间进行转换。

把一个OC对象转换成一个对应的C数据结构,比如NSArray到CFArrayRef的转换,使用__bridge可以告诉ARC,ARC仍具有这个对象的管理权,转换出来的CFArrayRef仅仅是临时使用一下。如果使用__bridge_retained,则告诉ARC交出对象的管理权,此时对象这块内存的管理权归CFArrayRef所有,所以用完之后需要调用CFRelease释放内存。反过来,如果创建了一个CFArrayRef,想要把所有权给ARC,应该使用__bridge_transfer

书中还介绍了一种通过创建CFDictionary来改变NSDictionary行为的办法。默认的NSDictionary是对键复制,对值保留。而CFDictionary提供了更多的控制接口,创建一个对键保留,对值复制的CFDictionary然后再将其所有权交给一个NSDictionary,就改变了NSDictionary的行为。

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

第一,NSCache本身就是做缓存用的,在系统资源不够的时候,可以自动的删减缓存。如果使用NSDictionary需要自己处理。

第二,NSCache是线程安全的,NSDictionary不是。

有个类叫做NSPurgeableData,是NSMutableData的一个子类,实现了NSDiscardableContent协议。在使用的时候先调用beginContentAccess方法,在使用结束后调用endContentAccess方法,可以嵌套使用。当最后一个endContentAccess调用的时候,NSPurgeableData中的数据自动清空。这个数据结构可以用于大粒度的缓存控制。比如退出一个模块后,自动清除缓存,而不必等缓存自动淘汰。

最后作者提醒不好滥用缓存,只有获取数据的开销比较大的时候才使用缓存。

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

在iOS程序中,+load函数在应用启动的时候就会被执行。 如果+load还依赖于其他程序库,那么那个程序库中的+load也会被执行。在+load中调用方法是不安全的,因为模块加载的顺序不一定。并且+load不遵从继承规则,如果一个类没有实现+load,那么它基类的+load不会被自动调用。 所以+load中的代码应该尽量精简,只要能不放在这里就不放在这里。

然而,作者建议不要使用+load方法,而是使用+initialize方法,后者相当于一个惰性初始化。更重要的是,+initialize方法中运行环境是安全的,可以调用其他方法,另外运行时保证+initialize是线程安全的。+initialize是遵从继承规则的。看下边代码

@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会先初始化基类,会打印EOCBaseClass initialize,然后初始化EOCSubClass,因为EOCSubClass没有实现initialize,所以调用基类的initialize,但是这时候self是EOCSubClass,所以打印EOCSubClass initialize。

鉴于复杂性,initialize方法中应该只设置一些成员的初始值。其实在initialize方法中做的事情可以提供一个公开的接口,要求使用者在创建对象之后必须调用这个初始化接口。但是有时候希望设计出“开箱”即用的组件,这时候可能需要通过initialize在做一些初始化工作。

第52条 别忘了NSTimer会保留目标对象
@implementation ViewController{
    NSTimer *_pollTimer;
}

- (void)viewDidLoad {
    [super viewDidLoad];
}

-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    
    [self stopPolling];
}


-(void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
    [self stopPolling];
}


-(void)startPolling{
    _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(p_dopoll) userInfo:nil repeats:YES];
}

-(void)p_dopoll{
    NSLog(@"poll...");
}


-(void)stopPolling{
    [_pollTimer invalidate];
    _pollTimer = nil;
}

@end

上边的代码有问题么?

答案是没有问题,但是设计不好。timer会强引用target,这里target是self,然而self又强引用了timer。这里形成了一个保留环。但是并没有内存泄漏,因为在viewDidDisappear函数中会调用stopPolling打破这个保留环。而viewDidDisappear一定会被调用。

但是如果把stopPolling的调用放到dealloc中,就会有问题了。因为在环被打破前dealloc不会被调用。如果想完全避免内存泄漏,最合理的办法是self对timer的引用改成__weak。因为timer会被runloop保留,所以不用担心timer的释放问题。使用弱引用目的就是为了之后的取消操作。

作者给出了另外一个方案,感觉颇复杂。没有必要使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值