cocoa中的消息机制-Block和KVO

3.block

Block是最近才加入Objective-C的,它首次出现在OSX10.6和iOS4平台上。Block通常可以完全替代delegation消息传递机制的角色。看一个例子:


[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
    NSLog(@“value for key %@ is %@”, key, value);
    if ([@“ENOUGH” isEqualToString:key]) {
*stop = YES; }
}];

我们可以将上面那个例子改为block试试

CustomView.h

@interface CustomView : UIView
@property (nonatomic, copy) void (^ButtonClickBlock)();
@end

CustomView.m

-(void)click:(UIButton *)sender{
    if (_ButtonClickBlock) {
        _ButtonClickBlock();
    }   
}
- (void)viewDidLoad {
    [super viewDidLoad];
    CustomView *customView = [[CustomView alloc]initWithFrame:CGRectMake(100, 100, 150, 50)];
    customView.ButtonClickBlock = ^{
        [self blockClick];
    };
    [self.view addSubview:customView];
}

-(void)blockClick{
    NSLog(@"block--click!");
}

是不是感觉block非常简单?Block是一个C语言的特性,它就是C语言的函数指针,在使用中最多的就是进行函数回调或者事件传递,比如发送数据到服务器,等待服务器反馈是成功还是失败,此时block就派上用场了,这个功能的实现也可用使用代理,只不过通过block代码更加简洁。

block是一种非常好用的语法特性,下面简单介绍一下block的语法。

1.作为局部变量:

回传值(^名字)(参数列)=^(传入参数列){行为主体} ;
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
//使用Block
NSString *str =  ^(NSInteger paramInteger){
    return [NSString stringWithFormat:@"%lu",(unsigned long)paramInteger];
}(10);

//使用Block指针:
NSString *(^intToString)(NSInteger) = ^(NSInteger paramInteger){
    return [NSString stringWithFormat:@"%lu",(unsigned long)paramInteger];
};
- (void)test {
    intToString(10);
}

2.作为property:

使用Block指针:回传值(^名字)(参数列)
@property (nonatomic, copy) returnType (^blockName)(parameterTypes);
@property (nonatomic, copy) NSString *(^intToString)(NSInteger) ;
@property (nonatomic, copy) void (^ButtonClickBlock)();

3.作为方法参数:

[someObject methodThatTakesABlock:^returnType (parameters) {...}];
-(void)test {
NSString *result2 = [self converIntToString:123 usingBlockObject:^(NSInteger paramInteger){
        return [NSString stringWithFormat:@"%lu",(unsigned long)paramInteger];
    }];
}

4.typedef

Block可以看作一种特殊语法的 “objects” 。通常我们typedef一个block来存储它:

//typedef这个block(类似c语言中的函数指针)
typedef NSString* (^intToStringConverter)(NSInteger paramInteger);
intToStringConverter inlineConverter = ^(NSInteger paramInteger){
        return [NSString stringWithFormat:@"%lu",(unsigned long)paramInteger];
    };
NSString *result2 = [self converIntToString:123 usingBlockObject:inlineConverter];

5.注意事项

1).block在实现时就会对它引用到的它所在方法中定义的栈变量进行一次只读拷贝,然后在block块内使用该只读拷贝。

- (void)testAccessVariable
{
    //如果没有前缀,只能读取outsideVariable的值,不能写入,加入__block前缀则可以
    __block NSUInteger outsideVariable = 10;
    NSMutableArray *array = [[NSMutableArray alloc]initWithObjects:@"obj1",@"obj2",nil];
    [array sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        NSUInteger insideVariable = 20;
        insideVariable = 30;//block内部的变量可读/写
        outsideVariable = 30;//block外部的变量可读,写的话必须先声明为__block
        self.stringProperty = @"Block Objects";//property变量可读/写
        NSLog(@"outside Variable = %ld",(unsigned long)outsideVariable);
        NSLog(@"inside Variable = %ld",(unsigned long)insideVariable);
        NSLog(@"self = %@",self);//因为testAccessVariable是instance method所以可以access self
        NSLog(@"string property = %@",self.stringProperty);
        return NSOrderedSame;
    }];
}

结果输出为:

outside Variable = 30
inside Variable = 30
self = 
string property = Block Objects

blockObject在实现时会对outside变量进行只读拷贝,在block块内使用该只读拷贝。所以,如果我们想要让blockObject修改或同步使用outside变量,就需要用__block来修饰outside变量。

__block NSInteger outsideVariable = 10;

2).非内联(inline)block不能直接访问self,只能通过将self当作参数传递到block中才能使用,并且此时的self 只能通过setter或getter方法访问其属性,不能使用句点式方法。但内联block(见上面的testAccessVariable)不受此限制。

错误的例子:

void(^independentBlockObject)(void) = ^(void){
    NSLog(@"self = %@",self);
   self.stringProperty = @"Block Objects"; 
   NSLog(@"string property = %@",self.stringProperty); 
};

正确的如下:

void (^independentBlockObject)(id)=^(id self){
    NSLog(@"self = %@",self);
    [self setStringProperty:@"Block Objects"];
    NSLog(@"string property = %@",self.stringProperty);
}

3).使用 weak–strong dance 技术来避免循环引用

通常block会存在导致retain环的风险。如果发送者需要retain block但又不能确保引用在什么时候被赋值为nil,那么所有在block内对self的引用就会发生潜在的retain环。

在第二条中,我提到内联block可以直接引用self,但是要非常小心地在block中引用self。因为在一些内联block引用self,可能会导致循环引用。如下例所示:

@interface ViewController (){
    id _observer;
}
@end

@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    _observer = [[NSNotificationCenter defaultCenter]
                 addObserverForName:@"TestNotificationKey"
                 object:nil queue:nil usingBlock:^(NSNotification *n) {
                     NSLog(@"%@", self);
                 }];
}
- (void)dealloc
{
    if (_observer) {
        [[NSNotificationCenter defaultCenter] removeObserver:_observer];
    }
}

在上面代码中,在消息通知block中引用到了self,而self对象被block保留.因此只要_observer对象还没有被解除注册,block就会一直被通知中心持有,从而self就不会被释放,其dealloc就不会被调用。弄明白这个逻辑了没?看下图:

cocoa中的消息机制-Block和KVO_第1张图片
流程图.png

这样就形成了循环引用。

block中的对象引用会保留在栈中,即block会对其中的对象指向强指针,再举另外一个例子

@property (nonatomic, strong) NSMutableArray *myBlocks; // array of blocks

[self.myBlocks addObject:^ {
    [self doSomething];
}];

在上面的例子中,self对myBlocks保留了强指针,而block中引用到了self,所以block会对self有强指针,引起Memory Cycles

在ARC中,在被拷贝的block中无论是直接引用self还是通过引用self的成员变量间接引用 self,该block都会retain self。可以用weak–strong dance技术来解决。如下:

 __weak ViewController * wself = self;
    _observer = [[NSNotificationCenter defaultCenter]
                 addObserverForName:@"TestNotificationKey"
                 object:nil queue:nil usingBlock:^(NSNotification *n) {
                     KSViewController * sself = wself;
                     if (sself) {
                         NSLog(@"%@", sself);
                     }
                     else {
                         NSLog(@" dealloc before we could run this code.");
                     }
                 }];

以及

__weak MyClass *weakSelf = self; 
[self.myBlocks addObject:^ {
    [weakSelf doSomething];
}];

4).block内存管理分析

block其实也是一个NSObject对象,并且在大多数情况下,block是分配在栈上面的,只有当block被定义为全局变量或block块中没有引用任何automatic变量时,block才分配在全局数据段上。

在ARC下,编译器会自动检测为我们处理了block的大部分内存管理,但当将block当作方法参数时候,编译器不会自动检测,需要我们手动拷贝该block对象。幸运的是,Cocoa库中的大部分名称中包含”usingBlock“的接口以及GCD接口在其接口内部已经进行了拷贝操作,不需要我们再手动处理了。但除此之外的情况,就需要我们手动干预了。

 (id) getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{ NSLog(@"  > block 0:%d", val); },    // block on the stack
            nil];
    
//    return [[NSArray alloc] initWithObjects:
//            [^{ NSLog(@"  > block 0:%d", val); } copy],    // block copy to heap
//            nil];
}

- (void)testManageBlockMemory
{
    id obj = [self getBlockArray];
    typedef void (^BlockType)(void);
    BlockType blockObject = (BlockType)[obj objectAtIndex:0];
    blockObject();
}

执行上面的代码中,在调用testManageBlockMemory时,程序会crash掉。因为从 getBlockArray返回的block是分配在stack上的,但超出了定义block所在的作用域,block就不在了。正确的做法(被屏蔽的那段代码)是在将block添加到NSArray中时先copy到heap上,这样就可以在之后的使用中正常访问。

在ARC下,对block变量进行copy始终是安全的,无论它是在栈上,还是全局数据段,还是已经拷贝到堆上。对栈上的block进行copy是将它拷贝到堆上;对全局数据段中的block进行copy不会有任何作用;对堆上的block进行copy只是增加它的引用记数。

如果栈上的block中引用了__block类型的变量,在将该block拷贝到堆上时也会将__block变量拷贝到堆上,如果该__block变量在堆上还没有对应的拷贝的话,否则就增加堆上对应的拷贝的引用记数。

上一篇文章我们讲了Delegate,Notification Center,Block,这篇我们将KVC和KVO。

4.KVC和KVO

1).键值编码KVC

最简单的KVC能让我们通过名字(键)访问属性,无需调用显示的存取方法,有些情况下,这会使我们非常灵活地简化代码。如下:

@property (nonatomic, copy) NSString *name;

取值:

NSString *n = [object valueForKey:@"name"]

设定:

[object setValue:@"Daniel" forKey:@"name"]

值得注意的是这个不仅可以访问作为对象属性,而且也能访问一些标量(例如int和CGFloat)和struct(例如CGRect).Foundation框架会为我们自动封装它们。举例来说,如果有以下属性

@property (nonatomic) CGFloat height;

我们可以这样设置它(我们不能把非对象传给setValue:forKey:,必须用NSValue或NSNumber封装标量)。

[object setValue:@(20) forKey:@"height"]

不过,把非对象属性设为nil是一种特殊情况。当标量和struct的值被传入nil的时候尤其需要注意。假设我们我们写了以下的方法:

[object setValue:nil forKey:@"height"]

这会抛出一个exception,要正确的处理nil,我们要像这样重载setNilValueForKey:

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"height"]) {
        [self setValue:@0 forKey:key];
    } else
        [super setNilValueForKey:key];
}

除了标量,我们甚至可以访问私有属性

@interface BookData : NSObject {  
    NSString * bookName;  
    float price;   
}  
BookData * book1 = [[BookData alloc] init];  
[book1 setValue:@"english" forKey:@"bookName"];  
[book1 setValue:@"20.0" forKey:@"price"];  
  
NSLog(@"value=%@",[book1 valueForKey:@"bookName"]);  
NSLog(@"price=%f",[[book1 valueForKey:@"price"] floatValue]);  

我们来看一个完整的例子,我们在storyBoard中添加两个UITextField,然后在ViewController.m加入如下代码。

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextField *nameField;
@property (weak, nonatomic) IBOutlet UITextField *cityField;
@end

新建我们的Model:Contact.h

@interface Contact : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@end

Contact.m

#import "Contact.h"
@implementation Contact
- (instancetype)init
{   self = [super init];
    if (self) {
        self.name = @"庄洁元";
        self.address = @"广州";
    }
    return self;
}
@end

这样在我们的viewDidLoad.m,可以通过KVC添加如下代码

- (void)viewDidLoad {
    [super viewDidLoad];
    _contact = [[Contact alloc]init];
    [self updateTextFields];
}
- (void)updateTextFields;
{
    _nameField.text = [_contact valueForKey:@"name"];
    [_contact setValue:@"潮州" forKey:@"address"];
    _cityField.text = [_contact valueForKey:@"address"];
}
键路径(Key Path)

KVC同样允许我们通过关系来访问对象。假设contact对象的address有属性city,我们可以这样通过address.city来访问city:

新建一个Model类:address.h

@interface Address : NSObject
@property (nonatomic, copy) NSString *country;
@property (nonatomic, copy) NSString *city;
@end

address.m

#import "Address.h"
@implementation Address
- (instancetype)init
{
    self = [super init];
    if (self) {
        self.country = @"中国";
        self.city = @"广州";
    }
    return self;
}
@end

Contact.h加入如下代码:

@interface Contact : NSObject
@property (nonatomic,strong) Address *address;
@end

不要忘了Address类的初始化
Contact.m

- (instancetype)init
{
    self = [super init];
    if (self) {
        _address = [[Address alloc]init];
        self.name = @"庄洁元";
        self.city = @"潮州";
    }
    return self;
}

这样我们就可以通过Key Path来访问对象了

ViewController.m:

- (void)updateTextFields;
{
    _nameField.text = [_contact valueForKey:@"name"];
    _cityField.text = [_contact valueForKeyPath:@"address.city"];
}
KVC和容器类

有一种更灵活的方式来管理容器类属性,我们从上面的例子举行说起,在Contact类实现以下方法:
Contact.h

@interface Contact : NSObject
- (NSUInteger)countOfNumbers;
- (id)objectInNumbersAtIndex:(NSUInteger)index;

Contact.m

- (NSUInteger)countOfNumbers {
    return 2;
}
- (id)objectInNumbersAtIndex:(NSUInteger)index {
    return @(index * 2);
}

ViewController.m

- (void)updateTextFields;
{   
    NSArray *items = [_contact valueForKey:@"numbers"];
    NSLog(@"numbers---%@",items);
}

打印结果如下:

numbers---(
    0,
    2
)

就算没有numbers这个属性,KVC系统也能创建一个行为和数组一样的代理对象。原因是contact实现了countOfNumbers和objectInNumbersAtIndex:方法,这些方法是特殊命名过的,当valueForKey:寻找对应项时,会搜索如下方法:

  • -getNumbers,numbers或isNumbers:系统会按顺序搜索这些方法,第一个找到的方法用来返回所请求的值.
  • -countOfNumbers,-objectInNumbersAtIndex:或-numbersAtIndexes:上例用到的组合会让KVC返回一个代理数组
  • -countOfNumbers,-enumeratorOfNumbers,-memberOfNumbers:这个组合会让KVC返回一个代理集合
  • 命名为_numbers,_isNumbers,或isNumbers的实例变量--KVC会直接访问ivar,一般最好避免这种行为。通过覆盖+accessInstanceVariablesDirectly并返回NO可以阻止这种行为

对于可变容器属性,有两种选择,可以用下面的方法:

-insertObject:inAtIndex:

-removeObjectFromAtIndex:

或者也可以通过调用mutableArrayValueForKey:或mutableSetValueForKey:返回一个特殊的代理对象。

KVC和字典

我们可以用valueForKeyPath来访问字典的任意一层,如下:

@property (nonatomic,copy)NSDictionary *dic;
self.dic = @{@"1":@"一",@"2":@"二"};

ViewController.m

- (void)updateTextFields;
{    
    NSString *dic = [_contact valueForKeyPath:@"dic.1"];
    NSLog(@"dic:%@",dic);    
}

这样我们可以吧键@"1"的值打印出来。

集合的高阶消息传递

我们举一个例子:

NSArray *array = @[@"foo",@"bar",@"hello"];
NSArray *capitals = [array valueForKey:@"capitalizedString"];
NSLog(@"---%@",capitals);

打印结果:

---(
    Foo,
    Bar,
    Hello
)

方法capitalizedString被传递给array中的每一项,并返回一个包含结果的新NSArray,这种被称为高阶消息传递,甚至可以用valueForKeyPath传递多个消息

NSArray *array = @[@"foo",@"bar",@"hello"];
NSArray *capitals = [array valueForKeyPath:@"capitalizedString.length"];
NSLog(@"---%@",capitals);

打印结果:

 ---(
    3, 3,5
)
集合的容器操作符

看下面的例子:

NSArray *array = @[@"foo",@"bar",@"hello"];    
NSInteger totalLength = [[array valueForKeyPath:@"@sum.length"]intValue];
NSLog(@"totalLength--%ld",(long)totalLength);

@sum是一个操作符,可以对指定的属性(length)求和。除了@sum,还有@max等其它很多操作符,这些操作符在处理Core Data时特别有用。

2).键值观察KVO

在Cocoa的MVC架构里,控制器负责让视图和模型同步。这一共有两步:当model 对象改变的时候,视图应该随之改变以反映模型的变化;当用户和控制器交互的时候,模型也应该做出相应的改变。

KVO的最大用处是能帮助我们让视图和模型保持同步。控制器可以观察视图依赖的属性变化。利用KVO,在我们关注的属性发生变化时都会得到一次回调。

KVO用addObserver:forKeyPath:options:context:开始观察,用removeObserver:forKeyPath:context:停止观察,回调总是observeValueForKeyPath:ofObject:change:context:。看一个例子:

在viewController中添加一个slider控件

@property (readwrite,strong)NSNumber *now;

- (IBAction)countSlider:(UISlider *)sender {

}

我们希望Model类Contact类可以监听viewController中slider控件的滑块值变化,所以我们需要在Contact.m中添加相应的监听代码:

Contact.h

@property (nonatomic, readwrite, strong) id object;
@property (nonatomic, readwrite, copy) NSString *property;

Contact.m

- (BOOL)isReady {
    return (self.object && [self.property length] > 0);
}

- (void)update {
    NSLog(@"KVO--%@",self.isReady ?
          [[self.object valueForKeyPath:self.property] description]
          : @"");
}

- (void)removeObservation {
    if (self.isReady) {
        [self.object removeObserver:self
                         forKeyPath:self.property];
    }
}

- (void)addObservation {
    if (self.isReady) {
        [self.object addObserver:self forKeyPath:self.property
                         options:0
                         context:(void*)self];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    if ((__bridge id)context == self) {
        // Our notification, not our superclass’s
        [self update];
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object
                               change:change context:context];
    }
}

- (void)setObject:(id)anObject {
    [self removeObservation];
    _object = anObject;
    [self addObservation];
    [self update];
}

- (void)setProperty:(NSString *)aProperty {
    [self removeObservation];
    _property = aProperty;
    [self addObservation];
    [self update];
}

- (void)dealloc {
    if (_object && [_property length] > 0) {
        [_object removeObserver:self
                     forKeyPath:_property
                        context:(void *)self];
    }
}

viewController.m

- (IBAction)countSlider:(UISlider *)sender {
    self.now = @(sender.value);
    [_contact setProperty:@"now"];
    [_contact setObject:self];
}

滑动滑块,打印出以下代码:

KVO--0.5042373
KVO--0.5127119
KVO--0.5466102
KVO--0.5720339
...省略

在viewController中,我们创建了一个属性now,并让Contact类观察此属性,滑块每次滑动,Contact都会得到通知。只要用存取方法来修改实例变量,所有的观察机制都会自动生效,不需要付出任何成本。

5.各种消息机制的权衡

上面讲了Delegate,Notification,Block和KVO,那么它们各自适合哪些情况?

对于Delegate来讲,它最大的优点是:

  • 协议在一个应用中的控制流程是可跟踪的并且是可识别的;
  • 没有第三方对象要求保持/监视通信过程。
  • 能够接收调用的协议方法的返回值。这意味着delegate能够提供反馈信息给controller

缺点:

  • 需要定义很多代码.
  • 在一个controller中有多个delegate对象,并且delegate是遵守同一个协议,但还是很难告诉多个对象同一个事件,不过有可能。

对于Block来讲,它的优点是

  • 写法更简练,不需要写protocol、函数等等
  • block注重结果的传输:比如对于一个事件,只想知道成功或者失败,并不需要知道进行了多少或者额外的一些信息

缺点是:

  • block需要注意防止循环引用。

对于Notification来讲,它最大的优点是:

  • 代码量少,实现比较简单;
  • 对于一个发出的通知,多个对象能够做出反应,即1对多的方式实现简单
  • controller能够传递context对象(dictionary),context对象携带了关于发送通知的自定义的信息

缺点:

  • 在编译期不会检查通知是否能够被观察者正确的处理;
  • 在调试的时候应用的工作以及控制过程难跟踪;
  • 通知发出后,controller不能从观察者获得任何的反馈信息。

KVO

KVO适合一个对象与另外一个对象保持同步的一种方法,即当另外一种对象的状态发生改变时,观察对象马上作出反应。它只能用来对属性作出反应,而不会用来对方法或者动作作出反应。

优点:

  • 能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步;;
  • 当需要大量观察的情况下,它的性能会比Notification好很多;
  • 用key paths来观察属性,因此也可以观察嵌套对象;

缺点:

  • 和Notification一样,我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查;
  • 对属性重构将导致我们的观察代码不再可用;
  • KVO会制造出人意料的代码执行路径,当我们调用postNotification时,我们知道还有另外一些代码会运行,只要在代码中搜索通知的名字,一般都可以找出所有可能发生的事情。而只是设置某个属性却会导致程序的其它部分执行,而且很难通过搜索代码找到这种交互;
  • 在存在复杂的相互依赖关系或复杂的类继承层次的地方避免使用KVO。

你可以在这里下载完整的代码。如果你觉得对你有帮助,希望你不吝啬你的star:)

你可能感兴趣的:(cocoa中的消息机制-Block和KVO)