iOS开发iOS DeveloperiOS

【瞎搞iOS开发09】聊一聊Block和[Block copy]

2017-01-19  本文已影响555人  溪枫狼


一种取消GCD延时任务的解决方案

在项目中经常用到延时操作,但是dispatch_after有个缺点,不能像perform~方法那样可以随时取消延时,有时会导致控制器被Block强引用而延时释放,可能导致崩溃,安全性不高。所以想实现一个可以取消的GCD延时任务的功能,但是折腾了很久没跳出这个坑,最后在Github上找到了一种解决方案。【Dispatch-Cancel】,代码虽少,但是效果极好,下面是源码。

typedef void(^SMDelayedBlockHandle)(BOOL cancel);

static SMDelayedBlockHandle perform_block_after_delay(CGFloat seconds, dispatch_block_t block) {
    if (nil == block) {
        return nil;
    }

    __block dispatch_block_t blockToExecute = [block copy];
    __block SMDelayedBlockHandle delayHandleCopy = nil;
    
    SMDelayedBlockHandle delayHandle = ^(BOOL cancel){      
        if (NO == cancel && nil != blockToExecute) {
            dispatch_async(dispatch_get_main_queue(), blockToExecute);
        }
    
#if !__has_feature(objc_arc)
        [blockToExecute release];
        [delayHandleCopy release];
#endif

        blockToExecute = nil;
        delayHandleCopy = nil;
    };
    delayHandleCopy = [delayHandle copy];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        if (nil != delayHandleCopy) {
            delayHandleCopy(NO);
        }
    });

    return delayHandleCopy;
};

static void cancel_delayed_block(SMDelayedBlockHandle delayedHandle) {
    if (nil == delayedHandle) {
        return;
    }
    delayedHandle(YES);
}

这是我们常规写法

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC), dispatch_get_main_queue(), {
    /// do something ,并且强引用外部对象,执行完才release
});

对比常规写法,源码的采用__block修饰的blockToExecute代替传进来的block,并且加了一个__block修饰的桥梁(中介) delayHandleCopy实现解耦,dispatch_after不会直接强引用外部对象,大致的引用关系如下,这样释放2个__block修饰的桥梁block,就可以解除了dispatch_after对外部对象的强引用。对NSTimer、KVO啥的都可以借助这种思想,利用桥梁对象实现解耦。

dispatch_after --> (__block) delayHandleCopy -> delayHandle --> (__block) blockToExecute -> block -> 外部对象

之前看源码的时候发现有2处用到了[block copy],比如下面的代码,以前没这么用过,就看了一些博客学习,然后自己捯饬。

__block dispatch_block_t blockToExecute = [block copy];

源码兼容了MRC,所以我把MRC相关的代码去掉,并且把2个[block copy]也去掉,一捯饬,没毛病! 但还是搞不懂为啥MRC要加[block copy],而ARC可以不用copy,为啥声明Block属性要用copy?,所以继续瞎捯饬了两天,在这做个粗略总结。


三种Block类

这里只做简单介绍,不会介绍底层的那些结构体源码,看后面的代码也能大概了解block,如果想深入了解,简书也有很多相关的文章。
首先Block分3种Class,分别是__NSGlobalBlock____NSStackBlock____NSMallocBlock__,主要在于内存区域的不同,其次是__NSGlobalBlock__不会引用任务外部变量,__NSStackBlock__会引用外部变量,__NSMallocBlock__[__NSStackBlock__ copy]而来,所以也会引用外部变量,这里说的引用外部变量指的是在block里面直接引用Block外的变量对象,而不是引用传入block里面的参数对象。至于三种Block在内存中的储存位置,我找了一张图,来自【iOS Block源码分析系列】。如果对内存分区好奇,看看这篇文章【iOS内存分配和分区】

3种Block的储存位置

ARC模式打印引用计数retianCount

先介绍一种在ARC模式下打印引用计数的方法,但是使用CFGetRetainCount获取引用计数会比实际引用计数大1,所以下面做了-1操作(对一个对象只会+1一次,不会叠加),只是不适合打印字符串对象的引用计数(数值特别大),此文不会扯MRC基础原理,当然有疑问也可以讨论。

static inline void JKLogRetainCount(NSString * des ,id obj) {
    if (nil != obj) {
        /// 实际的RetainCount 比 CFGetRetainCount 小 1
        NSLog(@"%@  RetainCount = %zd", des,CFGetRetainCount((__bridge CFTypeRef)obj) - 1);
    } else {
        NSLog(@"%@  RetainCount = 0, obj == nil",des);
    }
}

NSGlobalBlock测试

    UIView * testView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)];
    testView.backgroundColor = [UIColor orangeColor];
    JKLogRetainCount(@"alloc testView",testView);
    
    [self.view addSubview:testView];
    JKLogRetainCount(@"add testView",testView);
    
     dispatch_block_t globalBlock = ^(){};
     NSLog(@"1:%@",globalBlock);
     NSLog(@"2:%@",^(int a, int b){ a = a + b;});
    
     void (^globalBlock_Temp) (UIView * , int ) = ^(UIView * a, int b) {
         a.backgroundColor = [UIColor redColor];
         JKLogRetainCount(@"作为GlobalBlock内部的参数 testView",a);
     };
    
    JKLogRetainCount(@"GlobalBlock外部的testView",testView);
    globalBlock_Temp(testView, 3);
    JKLogRetainCount(@"已执行完GlobalBlock,外部testView",testView);
    NSLog(@"3:%@",globalBlock_Temp);
    
    /// 作为方法的参数传入
    [self globalBlockTest:^{
        NSLog(@"没有强引用外部变量");
    }];
    
    
    - (void)globalBlockTest:(void(^)(void))globalBlock {
    NSLog(@"globalBlock Func Log:%@",globalBlock);
}

打印结果:

     [控制台打印] alloc testView  RetainCount = 1
     [控制台打印] add testView  RetainCount = 2
     [控制台打印] 1:<__NSGlobalBlock__: 0x10fa44100>
     [控制台打印] 2:<__NSGlobalBlock__: 0x10fa44140>
     [控制台打印] GlobalBlock外部的testView  RetainCount = 2
     [控制台打印] 作为GlobalBlock内部的参数 testView  RetainCount = 3
     [控制台打印] 已执行完GlobalBlock,外部testView  RetainCount = 2
     [控制台打印] 3:<__NSGlobalBlock__: 0x10fa44180>
     [控制台打印] 作为GlobalBlock内部的参数 testView  RetainCount = 3
     [控制台打印] globalBlock Func Log:<__NSGlobalBlock__: 0x10fa441c0>

上面4种情况中的Block都没有引用外部变量,不管有没有赋值操作,都是__NSGlobalBlock__类型。另外作为参数传入Block的外部对象,只有在执行GlobalBlock时,GlobalBlock会强引用参数变量(RetainCount+1),GlobalBlock执行结束就会解除强引用(RetainCount-1)。
在Block对对象参数的引用上,__NSGlobalBlock_、__NSStackBlock__、__NSMallocBlock__都是一样的效果,即在执行Block的过程中,Block会强引用参数对象一次(RetainCount+1),执行完就会解除强引用(RetainCount-1)。如果要在Block里面调用Block外部的对象,传参是最安全的,但使用起来比较麻烦,代码不是很好理解。


NSStackBlock测试

    testView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 50, 50)];
    JKLogRetainCount(@"alloc testView",testView);
    
    [self.view addSubview:testView];
    JKLogRetainCount(@"add testView",testView);

    NSLog(@"1:%@",^(){
        testView.backgroundColor = [UIColor darkGrayColor];
        JKLogRetainCount(@"stackBlock内部强引用的 testView",testView);
    });
    JKLogRetainCount(@"外部的TestView已被stackBlock引用,但是未调用stackBlock", testView);

    __weak void (^weakStackBlock)(UIView *) = ^(UIView * view){
        testView.backgroundColor = [UIColor darkGrayColor];
        view.backgroundColor = [UIColor darkGrayColor];
        JKLogRetainCount(@"stackBlock内部强引用的 testView",testView);
    };
    NSLog(@"2:%@",weakStackBlock);
    weakStackBlock(testView);
    JKLogRetainCount(@"weakStackBlock已执行完,外部的testView",testView);
    NSLog(@"3:%@",weakStackBlock);
    [self stackBlockTest:weakStackBlock];

    void(^tempStackBlock)(UIView *) = weakStackBlock;
    NSLog(@"tempStackBlock = weakStackBlock   2: %@",tempStackBlock);


- (void)stackBlockTest:(void(^)(UIView *))stackBlock {
     NSLog(@"stackBlock Func Log:%@",stackBlock);
    
    /// tempStackBlock会是__NSMallocBlock__,应该执行了copy
    void(^tempMallocBlock)(UIView *) = stackBlock;
    NSLog(@"tempMallocBlock = stackBlock   1: %@",tempMallocBlock);
}
    /**<  
     [控制台打印] alloc testView  RetainCount = 1
     [控制台打印] add testView  RetainCount = 2
     [控制台打印] 1:<__NSStackBlock__: 0x7fff501be818>
     [控制台打印] 外部的TestView已被stackBlock引用,但是未调用stackBlock  RetainCount = 3
     [控制台打印] 2:<__NSStackBlock__: 0x7fff501be7e8>
     [控制台打印] stackBlock内部强引用的 testView  RetainCount = 5
     [控制台打印] weakStackBlock已执行完,外部的testView  RetainCount = 4
     [控制台打印] 3:<__NSStackBlock__: 0x7fff501be7e8>
     [控制台打印] stackBlock Func Log:<__NSStackBlock__: 0x7fff501be7e8>
     
     ** 重点 **
     [控制台打印] tempMallocBlock = stackBlock   1: <__NSMallocBlock__: 0x60000005b960>
     [控制台打印] tempStackBlock = weakStackBlock   2: <__NSStackBlock__: 0x7fff501be7e8>
     */

__NSStackBlock__的出栈入栈都由系统管理,在定义block时外部变量就会被stackBlock强引用一次,直到stackBlock销毁才会解除强引用。在上面的测试中,testView被2个stackBlock引用(一个未命名,一个是weakStackBlock),所以引用计数被+1了2次。另外testView作为参数传入atackBlock,在执行stackBlock过程中也被retain了一次,不过执行完又release了一次,这点跟GlobalBlock是一样的。值得注意的有3点:

  1. 将一个stackBlock赋值给一个__weak修饰的weakStackBlock,weakStackBlock还是__NSStackBlock__类型,后面的测试中是将普通的stackBlock赋值给一个__strong修饰或者普通的strongBlock,strongBlock会是__NSMallocBlock__类型,原因ARC环境下在block赋值时会自动调用copy。
  2. 将weakStackBlock赋值给一个普通的block,block还是__NSStackBlock__,并且block == weakStackBlock,ARC和MRC环境测试结果一样。
  3. 调用方法时将weakStackBlock作为参数传入方法,在方法内部将weakStackBlock赋值给一个普通的block,将会是__NSMallocBlock__类型,而且block != weakStackBlock,或许__weak作用域/可识别域是有限的,这方面待研究。

NSMallocBlock

    void (^mallocBlock_temp) (UIView *) = [stackBlock copy];
    NSLog(@"1:%@",mallocBlock_temp);

    testView = [[UIView alloc] initWithFrame:CGRectMake(100, 300, 50, 50)];
    testView.backgroundColor = [UIColor orangeColor];
    JKLogRetainCount(@"alloc testView",testView);
    
    [self.view addSubview:testView];
    JKLogRetainCount(@"add testView",testView);
    
    ** 如果把这行放到alloc View前面去,结果会怎么样? ** 
    //void (^mallocBlock_temp) (UIView *) = [stackBlock copy];
    //NSLog(@"1:%@",mallocBlock_temp);
    
    void (^mallocBlock) (UIView *) = ^(UIView * view) {
        testView.backgroundColor = [UIColor redColor];
        view.backgroundColor = [UIColor blueColor];
        JKLogRetainCount(@"mallocBlock内部的testView", testView);
    };
    
    NSLog(@"2:%@",mallocBlock);
    mallocBlock(testView);
    JKLogRetainCount(@"已调用mallocBlock,testView被强引用,也作为参数传入mallocBlock", testView);
    
    [self mallocBlockTest:mallocBlock];
    
    void (^mallocBlock_Copy)(UIView *) = [mallocBlock copy];
    NSLog(@"mallocBlock_Copy %@",mallocBlock_Copy);
    JKLogRetainCount(@"copy mallocBlock之后", testView);
    
    __strong void (^mallocBlock_Copy_Copy)(UIView *) = [mallocBlock_Copy copy];
    NSLog(@"mallocBlock_Copy_Copy %@",mallocBlock_Copy_Copy);
    JKLogRetainCount(@"copy mallocBlock_Copy_Copy之后", testView);
     [控制台打印] alloc testView  RetainCount = 1
     [控制台打印] add testView  RetainCount = 2
     [控制台打印] 1:<__NSMallocBlock__: 0x6000002409f0>
     [控制台打印] 2:<__NSMallocBlock__: 0x600000240b70>
     [控制台打印] mallocBlock内部的testView  RetainCount = 5
     [控制台打印] 已调用mallocBlock,testView被强引用,也作为参数传入mallocBlock  RetainCount = 4
     [控制台打印] mallocBlock Func Log:<__NSMallocBlock__: 0x600000240b70>
     [控制台打印] mallocBlock内部的testView  RetainCount = 4
     [控制台打印] mallocBlock_Copy <__NSMallocBlock__: 0x600000240b70>
     [控制台打印] copy mallocBlock之后  RetainCount = 4
     [控制台打印] mallocBlock_Copy_Copy <__NSMallocBlock__: 0x600000240b70>
     [控制台打印] copy mallocBlock_Copy_Copy之后  RetainCount = 4

在ARC模式下,将stackBlock赋值给一个不被__weak修饰的mallocBlock,系统会自动执行[stackBlockw copy],所以__NSMallocBlock__是我们经常见的block类型。并且我在MRC环境下测试,[stackBlock retain]还是__NSStackBlock__类型,只有[stackBlock copy]会产生一个__NSMallocBlock__类型的block,所以系统是自动执行了copy。所以前面我删了部分源码在ARC模式下也能运行,是系统帮我们调用了copy,而copy globalBlock和mallocBlock还是原类型,像浅拷贝,所以我们再调用copy也无碍。

外部变量obj开始会被stackBlock强引用一次,在[stackBlock copy]过程又会被强引用一次,所以外部变量会被强引用2次,有点像深拷贝的效果。另外3种类型的block调用copy有不同的结果:

  • stackBlock != [stackBlcok copy]
  • mallocBlock == [mallocBlock copy]
  • globalBlock == [globalBlock copy]

[stackBlcok copy]像深拷贝,而[mallocBlock copy]和[globalBlock copy]则像浅拷贝。PS:只能说像,并期待大神解答。


ARC模式声明block属性不要用weak

之所以不能用weak,是因为存在很大的安全隐患。比如在ViewDidLoad中将一个stackBlock赋值给self.weakBlock,在viewDidAppear中调用self.block(),会出现崩溃,原因是self.weakBlock在viewDidLoad方法结束后会被系统释放掉,self.weakBlock == nil。

@property (nonatomic, weak) void(^weakBlock)();

- (void)viewDidLoad {
    [super viewDidLoad];
      
    testView = [[UIView alloc] initWithFrame:CGRectMake(100, 400, 50, 50)];
    [self.view addSubview:testView];
    
    self.weakBlock = ^(){
        testView.backgroundColor = [UIColor redColor];
        JKLogRetainCount(@"self.weakBlock 内部testView", testView);
    };
}


- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    /// 会崩溃
    self.weakBlock();
}


ARC环境下声明block属性,copy和strong都可以

@property (nonatomic, copy) void(^copyBlock)();
@property (nonatomic, strong) void(^strongBlock)();
    testView = [[UIView alloc] initWithFrame:CGRectMake(100, 400, 50, 50)];
    testView.backgroundColor = [UIColor orangeColor];
    JKLogRetainCount(@"alloc testView",testView);
    
    [self.view addSubview:testView];
    JKLogRetainCount(@"add testView",testView);

    self.copyBlock = ^(){
        testView.backgroundColor = [UIColor blueColor];
        JKLogRetainCount(@"self.copyBlock 内部testView", testView);
    };
    NSLog(@"2:%@",self.copyBlock);
    self.copyBlock();
    JKLogRetainCount(@"self.copyBlock 外部testView", testView);
    
    
     [控制台打印] alloc testView  RetainCount = 1
     [控制台打印] add testView  RetainCount = 2
     [控制台打印] 2:<__NSMallocBlock__: 0x60800044d080>
     [控制台打印] self.copyBlock 内部testView  RetainCount = 4
     [控制台打印] self.copyBlock 外部testView  RetainCount = 4
    
    testView = [[UIView alloc] initWithFrame:CGRectMake(100, 400, 50, 50)];
    testView.backgroundColor = [UIColor orangeColor];
    JKLogRetainCount(@"alloc testView",testView);
    
    [self.view addSubview:testView];
    JKLogRetainCount(@"add testView",testView);
    
    self.strongBlock = ^(){
        testView.backgroundColor = [UIColor blueColor];
        JKLogRetainCount(@"self.strongBlock 内部 testView", testView);
    };
    NSLog(@"3:%@",self.strongBlock);
    self.strongBlock();
    JKLogRetainCount(@"self.strongBlock 外部 testView", testView);


     [控制台打印] alloc testView  RetainCount = 1
     [控制台打印] add testView  RetainCount = 2
     [控制台打印] 3:<__NSMallocBlock__: 0x60800044d050>
     [控制台打印] self.strongBlock 内部 testView  RetainCount = 4
     [控制台打印] self.strongBlock 外部 testView  RetainCount = 4
- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];

        self.copyBlock();
        self.strongBlock();
    
        NSLog(@"self.copyBlock   :%@",self.copyBlock);
        NSLog(@"self.strongBlock :%@",self.strongBlock);

    
    /**<  
     [控制台打印] self.copyBlock 内部testView  RetainCount = 3
     [控制台打印] self.strongBlock 内部 testView  RetainCount = 3
     [控制台打印] self.copyBlock   :<__NSMallocBlock__: 0x60800044d080>
     [控制台打印] self.strongBlock :<__NSMallocBlock__: 0x60800044d050>
     */
}

从打印结果来看,外部变量testView的引用计数在Self.copyBlock和self.strongBlock强引用后都+2,也就是testView被强引用了2次效果一致。stackBlock赋值给self.copyBlock时,系统会执行[stackBlock copy],另外在setter方法里面也会执行copy。赋值给self.strongBlock的结果和赋值给self.copyBlock是一样的,上面提到过,在MRC模式下,[stackBlock retain]不会产生__NSMallocBlock__类型的block对象,所以我想苹果是对Block的赋值做了特殊处理。总之在ARC模式下,声明copy和strong类型的block属性效果是一样的,不会影响使用。

Block的原始类型 newBlock = [Block copy]
newBlock的类型
__NSGlobalBlock__ __NSGlobalBlock__
__NSStackBlock__ __NSMallocBlock__
__NSMallocBlock__ __NSMallocBlock__
  • stackBlock != [stackBlcok copy]
  • mallocBlock == [mallocBlock copy]
  • globalBlock == [globalBlock copy]

另外发现一个现象,声明Block、NSString、NSDictionary、NSArray属性(self.property)都适合用copy,它们的共同点是 self.property == [self.property copy],而且[self.property copy]这个过程都像or是浅拷贝。

所以在此提出一个猜测:"除blcok对象以外,可以用 obj == [obj copy]来判断对象obj是否可变",欢迎讨论。

猜完了再看看这篇文章

下一篇文章设计copy和mutableCopy对(NSString、NSDictionary、NSArray对象)的影响,并涉及深拷贝、浅拷贝。


参考门

上一篇 下一篇

猜你喜欢

热点阅读