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的。所以它們構成了循環引用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容