iOS iOS基础iOS开发 Objective-C

iOS-Block 浅谈

2018-05-17  本文已影响50人  梦蕊dream

前言:Block 是开发过程中常用便捷的回调方式,本文简单介绍 Block

一、Block 简介

Block 对象是 C 级别的语法和运行时特性,和标准的 C 函数类似,除了可执行代码外,还可能包含变量自动绑定(栈)和内存托管(堆)。一个 Block 维护一个状态集。
闭包 = 一个函数「或指向函数的指针」+ 该函数执行的外部的上下文变量「也就是自由变量」;Block 是 Objective-C 对于闭包的实现。

使用 clang 将 OC 代码转换为 C++ 文件查看 block 的方法:

1.1 定义和使用

1.无参数无返回值

void (^ MyBlockOne)(void) = ^(void){
    NSLog(@"无参数,无返回值");  
};  
MyBlockOne();//block的调用

2.有参数无返回值

void(^MyblockTwo)(int a) = ^(int a){
    NSLog(@"@ = %d我就是block,有参数,无返回值",a);
};  
MyblockTwo(100);

3.有参数有返回值

int(^MyBlockThree)(int,int) = ^(int a,int b){    
    NSLog(@"%d我就是block,有参数,有返回值",a + b);returna + b; 
};  
MyBlockThree(12,56);

4.无参数有返回值

int(^MyblockFour)(void) = ^{NSLog(@"无参数,有返回值");
        return45;
  };
MyblockFour();

5.定义声明

声明

typedef void (^Block)();
typedef int (^MyBlock)(int , int);
typedef void(^ConfirmBlock)(BOOL isOK);
typedef void(^AlertBlock)(NSInteger alertTag);

定义属性

@property (nonatomic,copy) MyBlock myBlockOne;

使用

self.myBlockOne = ^int (int ,int){
    //TODO
}

1.2 Block与外界变量

1、截获自动变量(局部变量)值

(1)默认情况
对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();

输出结果:
age = 10

(2) __block 修饰的外部变量

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。block可以修改__block 修饰的外部变量的值。

__block int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();

输出为:
age = 18

2、__block 修饰的外部变量的值就可以被block修改

我们使用 clang 将 OC 代码转换为 C++ 文件:

clang -rewrite-objc 源代码文件名
__block int val = 10;
转换成
__Block_byref_val_0 val = {
    0,
    &val,
    0,
    sizeof(__Block_byref_val_0),
    10
};

会发现一个局部变量加上__block修饰符后竟然跟block一样变成了一个__Block_byref_val_0结构体类型的自动变量实例。
此时我们在block内部访问val变量则需要通过一个叫__forwarding的成员变量来间接访问val变量。

1.3 Block 存储

1、Block的存储域及copy操作

由C/C++/OBJC编译的程序占用内存分布的结构:


内存分布结构

block有三种类型:

三种block各自的存储域:

block存储域

简而言之,存储在栈中的Block就是栈块、存储在堆中的就是堆块、既不在栈中也不在堆中的块就是全局块。

1.4 判断Block的存储位置

(1)Block不访问外界变量(包括栈中和堆中的变量)
Block 既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。
(2)Block访问外界变量
MRC 环境下:访问外界变量的 Block 默认存储中。
ARC 环境下:访问外界变量的 Block 默认存储在中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。

ARC下,访问外界变量的 Block为什么要自动从栈区拷贝到堆区呢?
栈上的Block,如果其所属的变量作用域结束,该Block就被废弃,如同一般的自动变量。当然,Block中的__block变量也同时被废弃。
为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把Block复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆,如果有,自动生成将Block从栈上复制到堆上的代码。Block的复制操作执行的是copy实例方法。Block只要调用了copy方法,栈块就会变成堆块。

栈块copy堆块

Block的复制操作执行的是copy实例方法。不同类型的Block使用copy方法的效果如下表:

Block的复制操作

根据表得知,Block在堆中copy会造成引用计数增加,这与其他Objective-C对象是一样的。虽然Block在栈中也是以对象的身份存在,但是栈块没有引用计数,因为不需要,我们都知道栈区的内存由编译器自动分配释放。
不管Block存储域在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。在ARC有效时,多次调用copy方法完全没有问题:

blk = [[[[blk copy] copy] copy] copy];
// 经过多次复制,变量blk仍然持有Block的强引用,该Block不会被废弃。
1.5 __block变量与__forwarding

在copy操作之后,既然__block变量也被copy到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?__forwarding 终于要闪亮登场了。通过__forwarding, 无论是在block中还是 block外访问__block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。

__block与__forwarding

1.6 Block 循环引用

Block 循环引用的情况:
某个类将 block 作为自己的属性变量,然后该类在 block 的方法体里面又使用了该类本身。

self.someBlock = ^(Type var){
    [self dosomething];
};

解决办法:
(1)ARC 下:使用 __weak

__weak typeof(self) weakSelf = self;
self.someBlock = ^(Type var){
   [weakSelf dosomething];
};

(2)MRC 下:使用 __block

__block typeof(self) blockSelf = self;
self.someBlock = ^(Type var){
   [blockSelf dosomething];
};

解决办法:

//1.使用__weak ClassName
    __block XXViewController* weakSelf = self;
    self.blk = ^{
        NSLog(@"In Block : %@",weakSelf);
    };
//2.使用__weak typeof(self)
    __weak typeof(self) weakSelf = self;
    self.blk = ^{
        NSLog(@"In Block : %@",weakSelf);
    };
//3.Reactive Cocoa中的@weakify和@strongify
    @weakify(self);
    self.blk = ^{
        @strongify(self);
        NSLog(@"In Block : %@",self);
    };

二、Block 应用

2.1 Block 应用

1、Block作为变量(Xcode快捷键:inlineBlock)

int (^sum) (int, int); // 定义一个 Block 变量 sum
// 给 Block 变量赋值
// 一般 返回值省略:sum = ^(int a,int b)…
sum = ^int (int a,int b){  
    return a+b;
}; // 赋值语句最后有 分号
int a = sum(10,20); // 调用 Block 变量

2、Block作为属性(Xcode 快捷键:typedefBlock)

// 1. 给  Calculate 类型 sum变量 赋值「下定义」
typedef int (^Calculate)(int, int); // calculate就是类型名
Calculate sum = ^(int a,int b){ 
    return a+b;
};
int a = sum(10,20); // 调用 sum变量

// 2. 作为对象的属性声明,copy 后 block 会转移到堆中和对象一起
@property (nonatomic, copy) Calculate sum;    // 使用   typedef
@property (nonatomic, copy) int (^sum)(int, int); // 不使用 typedef

// 声明,类外
self.sum = ^(int a,int b){
    return a+b;
};
// 调用,类内
int a = self.sum(10,20);

3、作为 OC 中的方法参数

// ---- 无参数传递的 Block ---------------------------
// 实现
- (CGFloat)testTimeConsume:(void(^)())middleBlock {
    // 执行前记录下当前的时间
    CFTimeInterval startTime = CACurrentMediaTime();
    middleBlock();
    // 执行后记录下当前的时间
    CFTimeInterval endTime = CACurrentMediaTime();
    return endTime - startTime;

}

// 调用
[self testTimeConsume:^{
       // 放入 block 中的代码 

}];

// ---- 有参数传递的 Block ---------------------------
// 实现
- (CGFloat)testTimeConsume:(void(^)(NSString * name))middleBlock {
    // 执行前记录下当前的时间
    CFTimeInterval startTime = CACurrentMediaTime();
    NSString *name = @"有参数";
    middleBlock(name);
    // 执行后记录下当前的时间
    CFTimeInterval endTime = CACurrentMediaTime();
    return endTime - startTime;
}

// 调用
[self testTimeConsume:^(NSString *name) {
   // 放入 block 中的代码,可以使用参数 name
   // 参数 name 是实现代码中传入的,在调用时只能使用,不能传值    

}];

4、Block回调
Block回调是关于Block最常用的内容,比如网络下载,我们可以用Block实现下载成功与失败的反馈。block使用简单,逻辑清晰,灵活。

2.2 Block 几种类型演算

    {
        NSLog(@"\n--------------------block调用 基本数据类型---------------------\n");
        int a = 10;
        NSLog(@"block定义前a地址=%p", &a);
        void (^aBlock)() = ^(){
            NSLog(@"block定义内部a地址=%p", &a);
        };
        NSLog(@"block定义后a地址=%p", &a);
        aBlock();
    }
     
    /*
     结果:
     block定义前a地址=0x7fff5bdcea8c
     block定义后a地址=0x7fff5bdcea8c
     block定义内部a地址=0x7fa87150b850
     */
     
    /*
     流程:
     1. block定义前:a在栈区
     2. block定义内部:里面的a是根据外面的a拷贝到堆中的,不是一个a
     3. block定义后:a在栈区
     */
     
    {
        NSLog(@"\n--------------------block调用 __block修饰的基本数据类型---------------------\n");
         
        __block int b = 10;
        NSLog(@"block定义前b地址=%p", &b);
        void (^bBlock)() = ^(){
            b = 20;
            NSLog(@"block定义内部b地址=%p", &b);
        };
        NSLog(@"block定义后b地址=%p", &b);
        NSLog(@"调用block前 b=%d", b);
        bBlock();
        NSLog(@"调用block后 b=%d", b);
    }
     
    /*
     结果:
     block定义前b地址=0x7fff5bdcea50
     block定义后b地址=0x7fa873b016d8
     调用block前 b=10
     block定义内部b地址=0x7fa873b016d8
     调用block后 b=20
     */
     
    /*
     流程:
     1. 声明 b 为 __block (__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。)
     2. block定义前:b在栈中。
     3. block定义内部: 将外面的b拷贝到堆中,并且使外面的b和里面的b是一个。
     4. block定义后:外面的b和里面的b是一个。
     5. block调用前:b的值还未被修改。
     6. block调用后:b的值在block内部被修改。
     */
     
    {
        NSLog(@"\n--------------------block调用 指针---------------------\n");
         
        NSString *c = @"ccc";
        NSLog(@"block定义前:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
        void (^cBlock)() = ^{
            NSLog(@"block定义内部:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
        };
        NSLog(@"block定义后:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
        cBlock();
        NSLog(@"block调用后:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
    }
     
    /* 输出结果
      block定义前:c=ccc, c指向的地址=0x1068aac68, c本身的地址=0x7ffee93a7ab8
      block定义后:c=ccc, c指向的地址=0x1068aac68, c本身的地址=0x7ffee93a7ab8
      block定义内部:c=ccc, c指向的地址=0x1068aac68, c本身的地址=0x6000002542a0
      block调用后:c=ccc, c指向的地址=0x1068aac68, c本身的地址=0x7ffee93a7ab8
     c指针本身在block定义中和外面不是一个,但是c指向的地址一直保持不变。
     1. block定义前:c指向的地址在堆中, c指针本身的地址在栈中。
     2. block定义内部:c指向的地址在堆中, c指针本身的地址在堆中(c指针本身和外面的不是一个,但是指向的地址和外面指向的地址是一样的)。
     3. block定义后:c不变,c指向的地址在堆中, c指针本身的地址在栈中。
     4. block调用后:c不变,c指向的地址在堆中, c指针本身的地址在栈中。
     */
    {
        NSLog(@"\n--------------------block调用 指针并修改值---------------------\n");
         
        NSMutableString *d = [NSMutableString stringWithFormat:@"ddd"];
        NSLog(@"block定义前:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
        void (^dBlock)() = ^{
            NSLog(@"block定义内部:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
            d.string = @"dddddd";
        };
        NSLog(@"block定义后:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
        dBlock();
        NSLog(@"block调用后:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
    }
     
    /*输出结果
     block定义前:d=ddd, d指向的地址=0x600000440300, d本身的地址=0x7ffee9b2dab8
     block定义后:d=ddd, d指向的地址=0x600000440300, d本身的地址=0x7ffee9b2dab8
     block定义内部:d=ddd, d指向的地址=0x600000440300, d本身的地址=0x604000253940
     block调用后:d=dddddd, d指向的地址=0x600000440300, d本身的地址=0x7ffee9b2dab8

     d指针本身在block定义中和外面不是一个,但是d指向的地址一直保持不变。
     在block调用后,d指向的堆中存储的值发生了变化。
     */
     
    {
        NSLog(@"\n--------------------block调用 __block修饰的指针---------------------\n");
         
        __block NSMutableString *e = [NSMutableString stringWithFormat:@"eee"];
        NSLog(@"block定义前:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
        void (^eBlock)() = ^{
            NSLog(@"block定义内部:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
            e = [NSMutableString stringWithFormat:@"new-eeeeee"];
        };
        NSLog(@"block定义后:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
        eBlock();
        NSLog(@"block调用后:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
    }
     
    /*
     从block定义内部使用__block修饰的e指针开始,e指针本身的地址由栈中改变到堆中,即使出了block,也在堆中。
     在block调用后,e在block内部重新指向一个新对象,e指向的堆中的地址发生了变化。
     */
     
    {
        NSLog(@"\n--------------------block调用 retain cycle---------------------\n");
         
        View *v = [[View alloc] init];
        v.tag = 1;
        v.frame = CGRectMake(100, 100, 100, 100);
        [self.view addSubview:v];      //self->view->v
        void (^block)() = ^{
            v.backgroundColor = [UIColor orangeColor]; //定义内部:block->v
        };
        v.block = block;    //v->block
        block();   
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //预计3秒后释放v对象。
            [v removeFromSuperview];
        });
    }
     
    /*
     结果:
     不会输出 dealloc.
     */
     
    /*
     流程:
     1. self->view->v
     2. block定义内部:block->v 因为block定义里面调用了v
     3. v->block
      
     结论:
     引起循环引用的是block->v->block,切断其中一个线即可解决循环引用,跟self->view->v这根线无关
     */
     
    {
        NSLog(@"\n--------------------block调用self---------------------\n");
         
        View *v = [[View alloc] init];
        v.tag = 2;
        v.frame = CGRectMake(100, 220, 100, 100);
        [self.view addSubview:v];      //self->view->v
        void (^block)() = ^{
            self.view.backgroundColor = [UIColor redColor]; //定义内部:block->self
            _count ++;   //调用self的实例变量,也会让block强引用self。
             
        };
        v.block = block;    //v->block
        block();
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //预计3秒后释放self这个对象。
            AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
            appDelegate.window.rootViewController = nil;
        });
    }
    /*
     结果:
     不会输出 dealloc.
     */
     
    /*
     流程:
     1. self->view->v
     2. v->block
     3. block->self 因为block定义里面调用了self
      
     结论:
     在block内引用实例变量,该实例变量会被block强引用。
     引起循环引用的是self->view->v->block->self,切断一个线即可解决循环引用。
     */

2.3 Block 存储域

1. 在全局数据区的Block对象

NSGlobalBlock 静态block,释放有两种不同的时机:

    {
        NSLog(@"\n--------------------block的存储域 全局块---------------------\n");
         
        void (^blk)(void) = ^{
            NSLog(@"Global Block");
        };
        blk();
        NSLog(@"%@", [blk class]);
    }
    /*
     结果:输出 __NSGlobalBlock__
     */
     
    /*
     结论:
     全局块:这种块不会捕捉任何状态(外部的变量),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期就已经确定。
     全局块一般声明在全局作用域中。但注意有种特殊情况,在函数栈上创建的block,如果没有捕捉外部变量,block的实例还是会被设置在程序的全局数据区,而非栈上。
     */
2.在堆上创建的Block对象

NSMallocBlock 堆区block
堆区是内存的常驻区域,也叫永久存储区,block一般在函数中定义,最多是个栈block。在MRC时代你需要使用Block_copy()方法,才可以将blcok复制到堆中。

复制到堆中有何用处呢?

    {
        NSLog(@"\n--------------------block的存储域 堆块---------------------\n");
         
        int i = 1;
        void (^blk)(void) = ^{
            NSLog(@"Malloc Block, %d", i);
        };
        blk();
        NSLog(@"%@", [blk class]);
    }
    /*
     结果:输出 __NSMallocBlock__
     */
     
    /*
     结论:
     堆块:解决块在栈上会被覆写的问题,可以给块对象发送copy消息将它拷贝到堆上。复制到堆上后,块就成了带引用计数的对象了。
      
     在ARC中,以下几种情况栈上的Block会自动复制到堆上:
     - 调用Block的copy方法
     - 将Block作为函数返回值时(MRC时此条无效,需手动调用copy)
     - 将Block赋值给__strong修饰的变量时(MRC时此条无效)
     - 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
      
     上述代码就是在ARC中,block赋值给__strong修饰的变量,并且捕获了外部变量,block就会自动复制到堆上。
     */
3.在栈上创建的Block对象

NSStackBlock 栈区block

    {
        NSLog(@"\n--------------------block的存储域 栈块---------------------\n");
        int i = 1;
        __weak void (^blk)(void) = ^{
            NSLog(@"Stack Block, %d", i);
        };
        blk();
        NSLog(@"%@", [blk class]);
    }
    /*
     结果:输出 __NSStackBlock__
     */
     
    /*
     结论:
     栈块:块所占内存区域分配在栈中,编译器有可能把分配给块的内存覆写掉。
     在ARC中,除了上面四种情况,并且不在global上,block是在栈中。
     */

三、Block 原理

Block 优缺点

优点:

缺点:

Block 总结

Q:什么是Block?
A:Block是将函数及其执行上下文封装起来的对象

struct __block_impl{
  void *isa;//Block 是对象的标志
  int Flags;
  int Reserved;
  void *FuncPtr;//函数指针
};

Q:什么是Block调用?
A:Block调用是函数调用

Q:Block 如何截获变量?
A:1.基本数据类型的局部变量截获其值
2.对象类型的局部变量连同所有权修饰符一起截获
3.局部静态变量指针形式截获
4.不截获全局变量、静态全局变量

Q:什么情况使用__block修饰符?
A:一般情况下,对被截获变量进行赋值操作需要添加__block修饰符
赋值 != 使用
赋值:
赋值操作需要使用 __block修饰

__block NSMutableArray *arrM = nil;
    void (^testBlock)(void) = ^{
        arrM = [NSMutableArray array];
    };
    testBlock();

使用:如下代码不需要使用__block,因为是对数组的操作而不是数组的赋值。

NSMutableArray *arrM = [NSMutableArray array];
    void (^testBlock)(void) = ^{
        [arrM addObject:@"addObj"];
    };
    testBlock();
需要修饰符
不需要修饰符

Q:__block做了什么
A:__block修饰变量变成了对象

__block修饰符

Q:Block的Copy操作效果
A:如图

Block的Copy操作效果

Q:栈上Block的销毁
A:如图

栈上Block的销毁

Q:栈上Block的Copy操作
A:如图
Q:栈上Block的Copy,MRC是否会引起内存泄漏
A:会的

i栈上Block的Copy

Q:栈上__block变量的Copy操作
A:如图,修改__block变量值,修改的都是堆上的值

栈上__block变量的Copy操作

Q:__forwarding存在的意义
A:无论在栈还是堆上,__forwarding都可以顺利访问到同一个__block变量

Q:Block外部定义__weak修饰变量可以解决循环引用?
A:Block截获对象连同所有权修饰符一起截获的,如在外部对变量进行__weak修饰,结构体里持有的对象类型也是weak

Q:Block的引用循环,如图代码出现什么问题?

A:MRC下,不会产生循环引用;ARC下回产生循环引用,引起内存泄漏
ARC下的引用循环

ARC下的引用循环
ARC下的引用循环解决方案
解决方案

上述代码:ARC下解决方案
弊端:如果该Block长时间不被引用,该断环处一直存在,循环引用无法解除

解决方案

Question1:__weak修饰对象,当外部对象释放了之后,block 内部也访问不到这个对象,怎么办?

Answer:通过在 block 内部声明一个 __strong 的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题。

__weak typeof(self) weakSelf = self;
self.block = ^{
      __strong typeof(weakSelf) strongSelf = weakSelf;
      [strongSelf print];
};

我们以 AFNetworking 中 AFNetworkReachabilityManager.m 的一段代码举例:

__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
    __strong __typeof(weakSelf)strongSelf = weakSelf;

    strongSelf.networkReachabilityStatus = status;
    if (strongSelf.networkReachabilityStatusBlock) {
        strongSelf.networkReachabilityStatusBlock(status);
    }
};

Question2:__strong修饰对象,会不会引起循环引用?

Answer:不会!

详解:

__weak修饰的对象被Block引用,不会影响对象的释放,而__strong在Block内部修饰的对象,会保证,在使用这个对象在scope内,这个对象都不会被释放,出了scope,引用计数就会-1。
self是一个指向实例对象的指针,它的生命周期至少是伴随着当前的实例对象的,一旦它和对象之间有循环引用是无法被自动打破的;strongSelf是block内部的一个局部变量,变量的作用域仅限于局部代码,而程序一旦跳出作用域,strongSelf就会被释放,这个临时产生的“循环引用”就会被自动打破,代码的执行事实上也是这样子的。

推荐阅读 https://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/

深入篇:iOS-Block本质

上一篇下一篇

猜你喜欢

热点阅读