iOS Block深层次总结和一些经典的面试题

版权声明:本文为博主原创文章,未经博主允许不得转载。转载请Email我....... https://blog.csdn.net/Deft_MKJing/article/details/78582933

2分钟明白Block究竟是什么?

局部变量的截获以及__block的作用和理解

隐藏的三种Block本体以及为什么要使用copy修饰符

__block和Block的循环引用

上面几个是之前看书记录的知识点,可以回顾下,下面用人话概括下自己的理解,方便以后参考,先记住一个概念,Block就是一个对象

OC Block—> C++转换

1.最普通的转换

int a = 100;  
int b = 200;  
const charchar *ch = "b = %d\n";  
void (^block)(void) = ^{  
printf(ch,b);  
};  

struct __main_block_impl_0 {  
  struct __block_impl impl;  
  struct __main_block_desc_0* Desc; 
  // 上面也别管了
  // 这就是最简单的值捕获,外部类型是什么就是什么
  // 可以理解为copy了一个副本进入这个block,外部怎么变都和里面无关
  const charchar *ch;  
  int b;  
  // 下面先别看了
  __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, const charchar *_ch, int _b, int flags=0) : ch(_ch), b(_b) {  
    impl.isa = &_NSConcreteStackBlock;  
    impl.Flags = flags;  
    impl.FuncPtr = fp;  
    Desc = desc;  
  }  
}; 



2.__block修饰的转换

__block int a = 0;
void (^block)(void) = ^{a = 1};

struct __main_block_impl_0 {  
  struct __block_impl impl;  
  struct __main_block_desc_0* Desc;
  // 忽略上面  
  // 这就是重点,当你加了__block 内部转换就变成了结构体指针
  // 你可以理解为加了修饰符,就变成了结构体,那这里就是拿到了引用
  // 外部和内部引用的是同一个,外部修改就能改变内部
  __Block_byref_a_0 *a; // by ref  
  // 忽略下面
  __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {  
    impl.isa = &_NSConcreteStackBlock;  
    impl.Flags = flags;  
    impl.FuncPtr = fp;  
    Desc = desc;  
  }  
}; 

介绍下几个例子

这里详细介绍可以参考最顶部的几个链接,下面分析下几个典型的例子

基本案例1

    __block int a = 10;
    void (^test)(void) = ^{
        printf("%d",a); // 20
    };
    a = 20;
    test();



    int b = 100;
    void (^block1)(void) = ^{
        printf("%d",b); // 100
    };
    b = 200;
    block1();
    return 0;

根据这个结果和上面给出的转换后的结构体,咱们可以理解为
1.没有加__block修饰符,Block截获的时候只是把外部这个int值赋值到Block内部的一个int变量,那么这种copy的方法,外部无论怎么改变都不会影响内部值,因此,打印100
2.加了__block,可以根据上面转换后的代码,我个人把它理解为,加了修饰符,相当于用对象(结构体)包裹起来,而这个变量就是该结构体的某一个属性,那么Block截获的就是这个包裹的结构体,之后你再操作传进去结构体的地址,就是通过包裹的结构体间接操作包裹结构体内部的变量,因此外部的改变,其实就是通过包裹的结构体在改变变量的值
3.其实刚开始接触理解起来怪怪的,总之,Block就是值的Copy,那么直接截获到的值是无法通过外部的改变而影响内部copy出来的值的,因此,系统通过修饰符帮我们再用结构体包了一层,只是copy的是这个重新创建结构体的指针,你依然无法给这个指针在Block里面重新复制,思路和没有加修饰符一样,况且你能赋值,你也拿不到啊,所以Block内还是无法直接修改截获的值,你只是操作已经被修饰符包装一层的对象或者本身就是对象而已,虽然你代码是这么写,看上去直接修改了截获的值,可那只是假象而已

这里写图片描述

惊不惊喜,意不意外???
想要知道如何变成C++代码,可以看顶部第一篇文章介绍,其实Block就只是copy了一个你的外部变量而已,只是有修饰符,你的变量已经被一个结构体所包含,而没有修饰符,你的变量被copy了一份进入结构体,如果基础类型,那么就是简单的值复制,如果是指针,那么就是copy了一个新的指针,可以理解为指向的对象引用计数+1,这里涉及到循环引用,可以参考头部的文章分析,你外部指针变量改变指针地址,不会影响block内部,很简单,你要在block里面修改变量的直接值,你必须加__block修饰符,通过新的结构体对象间接修改,访问的时候其实也就通过结构体访问最原始的或者已经修改的值

这里写图片描述

基本案例2


NSMutableString *mutable_string = [NSMutableString stringWithString:@"aaa"];
    void(^mutable_append)(void)=^{
        [mutable_string appendString:@"ccc"];
    };
    [mutable_string appendString:@"bbb"];
    mutable_append();
    NSLog(@"\\n %@",mutable_string);  //结果:aaabbbccc
    // 没有__block,但是也没有涉及到直接指针的修改,只是操作而已,因此aaabbbccc

    NSString *string = @"aaa";
    NSString*(^append)(void)=^{
        return [string stringByAppendingString:@"ccc"];
    };
    string = @"bbb";
    NSLog(@"\\n %@",append());  //结果:aaaccc
    // 没有__block,copy值,截获之后和外部都指向@"aaa",但是外部string修改了指向为@"bbb",内部指针还是指向@"aaa",所以aaaccc

    __block NSString *block_string = @"aaa";
    NSString*(^block_append)(void)=^{
        return [block_string stringByAppendingString:@"ccc"];
    };
    block_string = @"bbb";
    NSLog(@"\\n %@",block_append()); //结果: bbbccc
    // 有__block,自动转换成新的结构体,string变成其内部属性,block截获的是新结构体的地址,外部block_string重新赋值,也不是简单的赋值,内部转换成`a.__forwarding.a`的代码,可以理解为通过新的结构体改变指针所指向的值,通过__block所形成的新结构体作为载体,之后所有的操作都是操作同一个对象,理解为指针操作,因此,形成一致,打印bbbccc

    __block NSString *name = [NSString stringWithFormat:@"%@",@"mikejing"];

    NSString *(^addaa)(void) = ^{
        return [name stringByAppendingString:@"cjj"];
    };
    name = @"MKJ";
    NSLog(@"\\n %@",addaa()); // \n MKJcj
    // 同上

    char *ch = "b =\n";
    void (^block)(void) = ^{
        printf("%s",ch); // b =
    };

    ch = "value had changed.b =\n";
    block();
    // 无法修改,上面已经介绍

总结

1.Block为什么不能修改外部变量(这里如何全局和static变量,头部文章有介绍)?因为你Block是copy值的类型进入Struch结构体存储,如果外部变量修改指向,影响不了内部copy的值,好比两个指针都指向字符串@”a”,当一个指针指向了@”b”,但是另一个指针还是指向@”a”,除非存储@”a”的地址下的值发生了变化
2.如何在Block内部和外部变量统一,或者如何Block内部修改值?用__block,该修饰符的意思,包裹成新的对象,变量就成了这个对象的属性,Block捕获的就是这个新对象的地址,之后这个变量出现的上下文,都是通过这个新对象的地址间接访问,Block内也一样访问这个对象,这样就保持了一致性,都访问同一个,无论你在哪修改,都能让值产生变化
3.循环引用的产生,既然是copy,那指针copy就会导致retain count + 1,就必须用弱引用来消除,这里就不展开了

一道大厂的面试题

@autoreleasepool{
        NSString *test = @"test1111";
        TestBlock block = ^(void){
            dispatch_sync(dispatch_queue_create("jd.test", DISPATCH_QUEUE_SERIAL), ^{
                NSLog(@"%@",test);
            });
        };
        test = @"test2222";
        block();
    }
    // 输出什么,在哪个线程,为什么?
    // <NSThread: 0x60c00007cec0>{number = 1, name = main}
    // test1111

我感觉我功力不够,看不出这题目的玄机,感觉就考了一个block而已啊???!!!根据上面分析,没有__block,因此只是值copy,打印test1111,block里面搞了个同步线程,由于本身就在主线程,因此没有开新线程,还是主线程打印。
文章和题目都是个人的理解而已,如果有不同意见和简介,希望各位留言纠正,好记性不如烂笔头,多记录点知识点

我这算是比较通俗的写了点见解,顶部还有几个比较书面化的知识点分析,喜欢看C源码的可以看看内部,这里有一份写的很不错的文章分析
Block深入分析

2018年更新

更新源自于看到了论坛上的一篇文章 想要看原文章的可以点击 Block面试帖子

首先Block的文章顶部一定介绍了一些了,可以自己翻阅,这里我觉得最重要需要明白的一点就是
MRC下面 block在创建的时候,它的内存是分配在栈(stack)上,可能被随时回收,而不是在堆(heap)上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。通过copy可以把block拷贝(copy)到堆,保证block的声明域外使用。
ARC下面默认创建就在堆上面了,可以直接供外部调

一些比较老的书上会描述Block有三种类型,分别是

NSGlobalBlock:全局Block,程序被加载后被分配在进程数据段上,也就是常量,静态创建的Block。

NSMallocBlock:在进程堆上分配的Block,动态创建的Block。

NSStackBlock:进程栈上分配的Block,动态创建的Block。

 void(^blockA)(void) = ^{
        NSLog(@"just a block");
    };
    NSLog(@"%@", blockA);

    int value = 10;
    void(^blockB)(void) = ^{

        NSLog(@"just a block === %d", value);
    };
    NSLog(@"%@", blockB);

    void(^ __weak blockC)(void) = ^{
        NSLog(@"just a block === %d", value);
    };

    NSLog(@"%@", blockC);

    void(^ __weak blockD)(void) = ^{
        NSLog(@"just a block");
    };

    NSLog(@"%@", blockD);
2018-02-24 14:30:27.929070+0800 mianshi[1067:37772] <__NSGlobalBlock__: 0x102f680c8>
2018-02-24 14:30:27.929179+0800 mianshi[1067:37772] <__NSMallocBlock__: 0x60800025d940>
2018-02-24 14:30:27.929282+0800 mianshi[1067:37772] <__NSStackBlock__: 0x7ffeecc96b20>
2018-02-24 14:30:27.929360+0800 mianshi[1067:37772] <__NSGlobalBlock__: 0x102f68148>



注意看它们的地址,NSGlobalBlock的地址明显要短,因为它是在进程数据段上的。一般来讲StackBlock在ARC下基本不可见了,但是通过修饰符也可以出现
blockC则是强行用__weak声明让其分配在栈上,这里会看到一个黄色的警告(Assigning block literal to a weak variable; object will be released after assignment),大意就是指分配后就会被释放。就是说viewDidLoad这个方法return后这个block就会被释放。那么 weak修饰也分两种情况,一般Block没有捕获变量的情况下都是GlobalBlock类型的,捕获之后就是StackBlock类型了。

动态分配和静态分配的区分是在哪里?观察一下就发现NSGlobalBlock类型是没有捕获局部变量的,它只是打印一一个字符串。通过NSString literal创建的字符串是放在常量区的,也就是数据段上。全局的block里没有引用任何堆或栈上的数据。另外如果将上面的例子中的int value = 10;改为const int value = 10;那么blockB将变成NSGlobalBlock,这是因为const修饰下value里的值会存储在常量区即数据段上,也就是不违反原则,只要block literal里没有引用栈或堆上的数据,那么这个block会自动变为NSGlobalBlock类型,这是编译器的优化。

在属性声明上,我们一般会用copy修饰一个Block属性。原因是什么?
在MRC中,block默认是在栈上创建的。如果我们将它赋值给一个成员变量,如果成员变量没有被copy修饰或在赋值的时候没有进行copy,也就是局部变量离开作用域之后会被系统回收,那么在使用这个block成员变量的时候就会崩溃。

看一段代码

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

- (void)viewDidLoad {
[superviewDidLoad];

int value = 10;
void(^blockC)() = ^{
NSLog(@"just a block === %d", value);
     };

NSLog(@"%@", blockC);
     _block = blockC;

}

- (IBAction)action:(id)sender {
NSLog(@"%@", _block);
}

1.首先我们看到的属性修饰符是weak(注意不是Assign)
2.ARC下面能正常运行,是因为ARC下Block默认已经分配到heap上了 blockC创建的时候内部捕获了变量,而且没有weak修饰符,因此从globalBlock变成了MallocBlock类型,第一个打印的就是2018-02-24 14:55:30.870410+0800 mianshi[1467:64609] <__NSMallocBlock__: 0x6000000558d0> ,后面用weak修饰的属性进行赋值,weak修饰对象,不会增加引用计数,因此离开作用域之后,Block被释放,由于是weak修饰的,那么weak修饰的指针会在weak hash表中自动置为nil,因此下面事件中打印出来的就(null)
3.MRC下面就会崩溃,由于默认是生成在stack上面的,离开作用域之后被系统回收,再访问被释放的对象就会崩溃
4.在我看来,ARC下崩溃不崩溃是修饰符导致的,weak肯定不会崩溃,因为weak 引用的对象在释放的时候会把指针都置为nil,但是如果你用assign修饰,ARC下面是有很大概率崩溃的,为什么很大概率是因为这块用assgin修饰的block地址有没有被再次使用,你也可以理解为崩溃。因为assgin只是简单的赋值,不会再对象释放的时候自动置为nil,这也是weak和assign最大的区别

这里原文章留了一个小小的题目

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


- (void)viewDidLoad {
[superviewDidLoad];

void(^ __weak blockA)() = ^{
NSLog(@"just a block");
    };

    _block = blockA;

}

- (IBAction)action:(id)sender {
    _block();
}

这里的答案很显然了,由于根据上面的四种打印,这里打印出来的就是GlobalBlock,是静态数据存储区域的,可以再外部继续访问

猜你喜欢

转载自blog.csdn.net/Deft_MKJing/article/details/78582933
今日推荐