iOS中block相关面试题

说明:对于block底层不是太熟悉或者下面题目中有不太懂的地方,建议先去看看我另外一篇博客:OC中block的底层实现原理

1. 第一题

下面代码运行结果是什么?

int d = 1000; // 全局变量
static int e = 10000; // 静态全局变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int a = 10; // 局部变量
        static int b = 100; // 静态局部变量
        __block int c = 1000;
        void (^block)(void) = ^{
             NSLog(@"a = %d",a);
             NSLog(@"b = %d",b);
             NSLog(@"c = %d",c);
             NSLog(@"d = %d",d);
             NSLog(@"e = %d",e);
         };
         a = 20;
         b = 200;
         c = 2000;
         d = 20000;
         e = 200000;
         block();
    }
    return 0;
}

// ***************打印结果***************
2020-01-08 08:50:54.621532+0800 CommandLine[72269:7909757] a = 10
2020-01-08 08:50:54.621871+0800 CommandLine[72269:7909757] b = 200
2020-01-08 08:50:54.621912+0800 CommandLine[72269:7909757] c = 2000
2020-01-08 08:50:54.621969+0800 CommandLine[72269:7909757] d = 20000
2020-01-08 08:50:54.621994+0800 CommandLine[72269:7909757] e = 200000

解释:

  • block在捕获普通的局部变量时是捕获的a的值,后面无论怎么修改a的值都不会影响block之前捕获到的值,所以a的值不变。
  • block在捕获静态局部变量时是捕获的b的地址,block里面是通过地址找到b并获取它的值。所以b的值发生了改变。
  • __block是将外部变量包装成了一个对象并将c存在这个对象中,实际上block外面的c的地址也是指向这个对象中存储的c的,而block底层是有一个指针指向这个对象的,所以当外部更改c时,block里面通过指针找到这个对象进而找到c,然后获取到c的值,所以c发生了变化。
  • 全局变量在哪里都可以访问,block并不会捕获全局变量,所以无论哪里更改de,block里面获取到的都是最新的值。

2. 第二题

下面代码能正常编译吗?不能的话是那些代码不能通过编译呢?

int d = 1000; // 全局变量
static int e = 10000; // 静态全局变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int a = 10; // 局部变量
        static int b = 100; // 静态局部变量
        __block int c = 1000;
        NSMutableArray *array1 = nil;
        __block NSMutableArray *array2 = nil;
        void (^block)(void) = ^{
            a = 20;
            b = 200;
            c = 2000;
            d = 20000;
            e = 200000;
            array1 = [NSMutableArray array];
            [array1 addObject:@"111"];
            array2 = [NSMutableArray array];
            [array2 addObject:@"222"];
         };
         
         block();
    }
    return 0;
}

解答:

a = 20;无法通过编译,因为a是局部变量,其作用域和生命周期仅限于它所在的大括号内部,而block底层是将块中的代码封装到了一个函数中,在那个函数中修改a就相当于在一个函数中去修改另外一个函数中的局部变量,这样肯定是无法通过编译。

array1 = [NSMutableArray array];无法通过编译。原因和上面一样,array1是一个指针,这里是想在一个函数中去给另外一个函数中的变量重新赋值一个指针,所以无法通过编译。

其它的都可以通过编译。

全局变量在哪里都可以访问到,所以在block里面可以修改,实际上block并不会捕获全局变量存到block内部。

__block修饰的变量(以变量c为例)是被包装成了一个对象,c就存储在对象中,block外面 的c的地址实际上就是这个对象中存储的c的地址,而block里面也存储着一个指针指向这个对象进而能访问到这个对象中的c,所以是可以直接修改的。

[array1 addObject:@"111"];可以通过编译是因为block捕获了array1的值(也就是数组的地址)存储在block里面,这里是通过这个地址找到数组,然后对数组中的元素进行操作,所以是可以通过编译的。所以对于一个对象类型的变量,block内部只要不是想修改这个变量的值,都不需要用__block来修饰这个变量(比如增、删、修改集合类型对象里面的元素,或者修改一个实例对象的属性等都不需要用__block修饰)。

3. 第三题

下面代码运行结果是什么?

- (void)test{
  
    __block Person *person = [[Person alloc] init];
    person.age = 20;
    
    __weak Person *weakPerson = person;
    self.block = ^{
        NSLog(@"block-begin:%@ age = %d",[NSThread currentThread],weakPerson.age);
        [NSThread sleepForTimeInterval:1.0f];
        NSLog(@"block-eng:%@ age = %d",[NSThread currentThread],weakPerson.age);
    };
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        self.block();
    });
    
    [NSThread sleepForTimeInterval:0.2f];
    NSLog(@"test-end:%@",[NSThread currentThread]);
}

// ***************打印结果***************
2020-01-09 09:50:50.309564+0800 AppTest[76619:8356081] block-begin:<NSThread: 0x600001b1aa40>{number = 6, name = (null)} age = 20
2020-01-09 09:50:50.509657+0800 AppTest[76619:8356009] test-end:<NSThread: 0x600001b7ee40>{number = 1, name = main}
2020-01-09 09:50:51.314372+0800 AppTest[76619:8356081] block-eng:<NSThread: 0x600001b1aa40>{number = 6, name = (null)} age = 0

解释:

weakPerson是一个弱指针,所以self.blockperson是弱引用。然后在并发队列中通过异步函数添加一个任务来执行self.block();,所以是开启了一个子线程来执行这个任务,此时打印age值是20,然后子线程开始睡眠1秒钟。与此同时主线程也睡眠0.2秒,0.2秒后主线程执行完最后的打印操作,test函数就执行完了。而由于person是一个局部变量,而且self.block对它也是弱引用,所以在test函数执行完后person对象就被释放了。再过0.8秒钟,子线程结束睡眠,此时weakPerson所指向的对象已经变成了nil,所以打印的age是0。

此时如果将主线程的睡眠时间改的比子线程睡眠时间长的话结果又不一样,因为子线程睡眠结束时主线程还在睡眠睡眠,也就是test方法还没执行完,那person对象就还存在,所以子线程睡眠前后打印的age都是20。

// 主线程睡眠时间改为:[NSThread sleepForTimeInterval:1.2f];

// ***************打印结果***************
2020-01-09 10:01:26.951395+0800 AppTest[76646:8360116] block-begin:<NSThread: 0x6000027316c0>{number = 5, name = (null)} age = 20
2020-01-09 10:01:27.955117+0800 AppTest[76646:8360116] block-eng:<NSThread: 0x6000027316c0>{number = 5, name = (null)} age = 20
2020-01-09 10:01:28.152426+0800 AppTest[76646:8360048] test-end:<NSThread: 0x60000274dc80>{number = 1, name = main}

4. 第四题

下面代码运行结果是什么?

- (void)test{
  
    __block Person *person = [[Person alloc] init];
    person.age = 20;
    
    __weak Person *weakPerson = person;
    self.block = ^{
        __strong Person *strongPerson = weakPerson;
        NSLog(@"block-begin:%@ age = %d",[NSThread currentThread],strongPerson.age);
        [NSThread sleepForTimeInterval:1.0f];
        NSLog(@"block-eng:%@ age = %d",[NSThread currentThread],strongPerson.age);
    };
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        self.block();
    });
    
    [NSThread sleepForTimeInterval:0.2f];
    NSLog(@"test-end:%@",[NSThread currentThread]);
}

// ***************打印结果***************
2020-01-09 10:02:01.319436+0800 AppTest[76662:8360768] block-begin:<NSThread: 0x6000010b2a00>{number = 4, name = (null)} age = 20
2020-01-09 10:02:01.520491+0800 AppTest[76662:8360684] test-end:<NSThread: 0x6000010a8680>{number = 1, name = main}
2020-01-09 10:02:02.324449+0800 AppTest[76662:8360768] block-eng:<NSThread: 0x6000010b2a00>{number = 4, name = (null)} age = 20

解释:

和前一题相比,这里在block内部定义了一个__strong修饰的strongPerson。这里要说明一下,__strong的作用就是保证在block中的代码块在执行的过程中,它所修饰的对象不会被释放,即便block外面已经没有任何强指针指向这个对象了,这个对象也不会立马释放,而是等到block执行结束后再释放。所以在实际开发过程中__weak__strong最好是一起使用,避免出现block运行过程中其弱引用的对象被释放。

注意__strong只是保证在block运行过程中弱引用对象不被释放,为什么要再强调一遍这个问题,请看下面一道题。

5. 第五题

下面代码运行结果是什么?

- (void)test{
    __block Person *person = [[Person alloc] init];
    person.age = 20;
    
    __weak Person *weakPerson = person;
    self.block = ^{
        __strong Person *strongPerson = weakPerson;
        NSLog(@"block-begin:%@ age = %d",[NSThread currentThread],strongPerson.age);
        [NSThread sleepForTimeInterval:1.0f];
        NSLog(@"block-eng:%@ age = %d",[NSThread currentThread],strongPerson.age);
    };
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self test1];
    });
    
    NSLog(@"test - end");
}

- (void)test1{
    self.block();
}

// ***************打印结果***************
2020-01-09 10:17:45.993182+0800 AppTest[76745:8367628] test - end
2020-01-09 10:17:45.993225+0800 AppTest[76745:8367733] block-begin:<NSThread: 0x6000038b9bc0>{number = 5, name = (null)} age = 0
2020-01-09 10:17:46.997579+0800 AppTest[76745:8367733] block-eng:<NSThread: 0x6000038b9bc0>{number = 5, name = (null)} age = 0

解释:
在并发队列中通过异步函数添加任务执行test1,是开启一个新线程来执行,而新线程是先睡眠0.1秒再执行test1,所以会先执行异步函数后面的代码,所以等到开始执行test1时,test已经执行结束了,所以在执行block之前person就已经被释放了,这种情况下__strong修饰符是不起作用的。


如果我将并发队列换成串行队列会怎么样?

dispatch_async(dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL), ^{
        [NSThread sleepForTimeInterval:0.1f];
        [self test1];
    });
    
// ***************打印结果***************
2020-01-09 10:34:46.120004+0800 AppTest[76893:8377451] test - end
2020-01-09 10:34:46.222708+0800 AppTest[76893:8377542] block-begin:<NSThread: 0x6000022ce400>{number = 5, name = (null)} age = 0
2020-01-09 10:34:47.226216+0800 AppTest[76893:8377542] block-eng:<NSThread: 0x6000022ce400>{number = 5, name = (null)} age = 0

其实这里不管是改成主队列还是自定义的串行队列结果都一样,只要保证是异步函数就行,异步函数不会阻塞当前线程,所以执行test1test已经执行完了。


这种情况下如果我将异步函数换成同步函数,其他地方不变:

dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:0.1f];
        [self test1];
    });
    
// ***************打印结果***************
2020-01-09 10:26:02.978640+0800 AppTest[76834:8372825] block-begin:<NSThread: 0x600000cf5040>{number = 1, name = main} age = 20
2020-01-09 10:26:03.979483+0800 AppTest[76834:8372825] block-eng:<NSThread: 0x600000cf5040>{number = 1, name = main} age = 20
2020-01-09 10:26:03.979717+0800 AppTest[76834:8372825] test - end

因为同步函数会阻塞当前线程,所以是等test1执行结束后,test才会继续执行后面的代码,所以person是在block执行结束后才被释放的。


6. 第六题

下面代码运行结果是什么?

- (void)test{
    __block Person *person = [[Person alloc] init];
    person.age = 20;
    
    __weak Person *weakPerson = person;
    self.block = ^{
        __strong Person *strongPerson = weakPerson;
        NSLog(@"block-begin:%@ age = %d",[NSThread currentThread],strongPerson.age);
        [NSThread sleepForTimeInterval:1.0f];
        NSLog(@"block-eng:%@ age = %d",[NSThread currentThread],strongPerson.age);
    };
    
    [self performSelector:@selector(test1) withObject:nil afterDelay:.0f];
    
    NSLog(@"test - end");
}

- (void)test1{
    self.block();
}

// ***************打印结果***************
2020-01-09 10:38:48.341591+0800 AppTest[76937:8379870] test - end
2020-01-09 10:38:48.357055+0800 AppTest[76937:8379870] block-begin:<NSThread: 0x600002e1ad00>{number = 1, name = main} age = 0
2020-01-09 10:38:49.358265+0800 AppTest[76937:8379870] block-eng:<NSThread: 0x600002e1ad00>{number = 1, name = main} age = 0

解释:

performSelector:withObject:afterDelay这个方法底层实现实际上是将一个定时器添加到了runloop中,然后等时间到了后就执行test1方法。虽然这里最后一个参数传的是0,也就是等待0秒后执行test1,但它并不是立马执行,因为需要先唤醒runloop,这是要耗一定时间的,所以会先执行后面的方法。所以等到开始执行test1test已经执行结束了,person已经释放了。

7. 第七题

下面代码会造成什么上面后果(self的block是用copy修饰的)?

- (void)test{
    self.age = 20;
    self.block = ^{
      NSLog(@"%d",self.age);
    };
    
    self.block();
}

解答:

会因循环引用而导致内存泄露。因为self通过一个强指针指向了block,而block内部又捕获了self而且用强指针指向self,所以selfblock互相强引用对方而造成循环引用。

解决这个问题很简单,只需要定义一个弱指针指向self,然后block内部就是用一个若指针指向self,所以结果是self强引用block,block弱引用self,所以不会造成循环引用。

- (void)test{
    self.age = 20;
    __weak typeof(self) weakSelf = self;
    self.block = ^{
      NSLog(@"%d",weakSelf.age);
    };
    
    self.block();
}

这里说明一点,我们判断会不会造成循环引用关键看block有没有捕获并强引用self。我们只需要记住以下下几点:

  • 如果block内部使用到了某个变量,而且这个变量是局部变量,那么block会捕获这个变量并存储到block底层的结构体中。
  • 如果捕获的这个变量是用__weak__unsafe_unretained修饰的,那么block内部就是用弱指针指向这个变量(也就是block不持有这个对象),否则block内部就是用强指针指向这个对象(也就是block持有这个对象)。
  • self是一个局部变量。因为self是所有OC方法的一个隐藏参数,所以它是一个局部变量。
  • 如果self并不持有这个block,block内部怎么引用self都不会造成循环引用。

8. 第八题

下面代码是否会造成循环引用:


- (void)test{
    self.age = 20;
    self.block = ^{
      NSLog(@"%d",self.age);
    };
}

解答:

会造成循环引用,这里虽然没有调用block,但并不影响它们相互引用的结果。


- (void)test1{
    self.age = 20;
    
    self.block = ^{
      NSLog(@"%d",_age);
    };
    
    self.block();
}

解答:

会造成循环引用。block里面虽然看不到self,实际上_age这种写法只是省略了self,完整写法是self->_age,所以是会造成循环引用的。这一点开发中要格外注意。


- (void)test{
    self.block = ^{
        [self setAge:10];
    };
    
    self.block();
}

解答:

会造成循环引用。OC中调用方法就是给某个对象发送消息,所以调用方法时是需要用到self的,所以block会捕获self并强引用它。


dispatch_sync(dispatch_get_global_queue(0, 0), ^{
       NSLog(@"%d",self.age);
});
    
[UIView animateWithDuration:1.0f animations:^{
       NSLog(@"%d",self.age);
}];

解答:

不会造成循环引用。当block是某个函数的参数时,虽然block内部是对self强引用的,但self并不持有block,所以不会造成循环引用。


- (void)test1{

    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        self.block = ^{
            NSLog(@"%d",self.age);
        };
        self.block();
    });
}

解答:

会造成循环引用。这是一个嵌套的block,虽然外层block不会循环引用,但是里面的block会造成循环引用。


typedef void(^MyBlock)(void);

- (void)test{

    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        self.age = [self testWithBlock:^{
           NSLog(@"%d",self.age);
        }];
    });
}

- (int)testWithBlock:(MyBlock)myBlock{
    myBlock();
    return 10;
}

解答:

不会造成循环引用。这也是嵌套的block。外层block并没有被self持有,所以不会造成循环引用。我们主要看下里面那个block,其实里面的block就是一个函数的参数,self并不持有它,所以不会造成循环引用。


- (void)test1{

    self.block = [self blockWithBlock:^{
       NSLog(@"%d",self.age);
    }];
    self.block();
}

- (MyBlock)blockWithBlock:(MyBlock)myBlock{
    myBlock();
    
    return ^{
      NSLog(@"block作为返回值");
    };
}

解答:

不会造成循环引用。这道题很具有迷惑性。有人会觉得self是持有block的,而block内部又强引用了self,所以会造成循环引用。其实仔细观察会发现这里有2个不同的block,self持有的是一个block,而强引用self的又是另外一个block,所以它们并不会造成循环引用。


- (void)test1{

    self.block = [self blockWithBlock:^{
       NSLog(@"作为参数的block");
    }];
    self.block();
}

- (MyBlock)blockWithBlock:(MyBlock)myBlock{
    myBlock();
    
    return ^{
      NSLog(@"block作为返回值--%d",_age);
    };
}

解答:

会造成循环引用。这道题同样具有迷惑性。表面上看起来self持有的是一个block,作为参数的又是另外一个block,而且这个block里面也没有用到self。其实这里的关键在于第二个函数的返回值,它返回的就是一个block,这个block里面强引用了self,而且将这个block赋值给了self的block,所以self是强指针指向这个返回的block的。所以它们构成了循环引用。

推荐阅读更多精彩内容