欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-02-29 23:13:40
...

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内还是无法直接修改截获的值,你只是操作已经被修饰符包装一层的对象或者本身就是对象而已,虽然你代码是这么写,看上去直接修改了截获的值,可那只是假象而已

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

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

iOS 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,是静态数据存储区域的,可以再外部继续访问