歡迎關注
最酷最in的雲資訊

面試驅動电竞 – Block看我就夠了【干貨】


面試驅動电竞合集(初中級iOS開發),關注倉庫,及時獲取更新 Interview-series

photo-1536065998491-91b0ffda8909.jpgBlock 在 iOS 算比較常見常用且常考的了,現在面試中,要麼沒面試題,有面試題的,基本都會考到 block 的點。本文特別干!(但是初中級iOSer應該能有所收獲~)

先來個面試題熱熱身,題目: 手撕代碼 – 用Block實現兩個數的求和

(這題如果會的,block基礎知識可以跳過了,直接到 Block原理探究)

簡單介紹block入門級用法

Block結構比較復雜,一般用 typedef 定義,直接調用的感覺比較簡單、清晰易懂

//typedef block的時候有提示typedef void(^MNBlock)(int);@interface ViewController ()@property (nonatomic, copy) MNBlock block;@end@implementation ViewController- (void)viewDidLoad {    [super viewDidLoad];        //直接用self.block調用    self.block = ^(int a) {        //dosomething...    };}
  • 參數解釋:

typedef (^)();

image.png

題目: 手撕代碼 – 用Block實現兩個數的求和

日常開發中,block聲明一般寫的比較多,實現一般是靠Xcode自動補全提示出現的,手撕代碼的情況下,等號右側的block實現要怎麼寫?

聲明:

typedef int(^MNBlock)(int a, int b);@interface ViewController ()@property (nonatomic, copy) MNBlock sum;

Vip補全功能:

image.png

紙上按Enter沒用啊兄弟!看來還是需要了解一下Block右邊的東西~

先在 Xcode上按下 Enter,了解下再撕

image.png

^int(int a, int b) {    //Control reaches end of non-void block        因為返回值是int類型,所以這里需要返回}

image.png

int(^Sum)(int, int) = ^(int a, int b){    return a + b;};int result = Sum(5, 10);

image.png

Block的坑出現!新手可能會寫錯的地方

1.聲明出錯 – void ^(testBlock)

image.png

修正版︰

void (^testBlock)() = ^{    };

block的聲明,^ 和 blockName 都是在小括號里面。/p>

2.block各種實現的參數問題

聲明typedef int(^MNBlock)(int, int);

image.png

    self.sum = ^int(int a, int b) {        return a + b;    };

這里要注意,block聲明里面只有參數類型,沒有實際參數的話,Xcode提示也只有參數,這里涉及到形參和實參的問題

聲明是形參,可以不寫參數,但是使用的時候,必須有實際參數,才可以進行使用,所以這里需要實參,可以在 ^int(int , int) 中手動添加實參^int(int a, int b),就可以讓a 和 b 參與運算

小tips︰實際開發中,建議聲明的時候,如果需要帶參數,最好形參也聲明下,這樣使用Xcode提示的時候,會把參數帶進去,方便得多~(踩過坑的自然懂!)

3. 省略void導致看不懂block結構的 (正常是兩個void導致局面混亂)

//聲明typedef void(^MNBlock)(void);//實現self.sum = ^{    //dosomething...};

這種情況下,能知道怎麼省略的,聲明里兩個void,能知道怎麼對應的嗎?

這個其實比較簡單,block不管聲明 or 實現,最後一個小括號,里面都是參數,而參數是可以省略的!

而為了把聲明的兩個void區分開,返回值 or 參數區分開,其實就ok了

參數非void的例子

//聲明非void的參數typedef void(^MNBlock)(int a);//實現就必須帶參數,不可省略!self.sum = ^(int a) {    }

參數void的例子 ==> 參數可以省略

typedef int(^MNBlock)(void);self.sum = ^{    //聲明的返回值類型是int,所以一定要return;    return 5;};

其實-返回值是void的,也可以不省略

typedef void(^MNBlock)(void);//實現的返回值不省略self.sum = ^void () {    };

參數是void的省略:

typedef int(^MNBlock)();//實現里面,沒有參數,就可以不寫()self.sum = ^int{    return 5;}

注意。聲明里面的返回值void是不可以省略的。/strong>

image.png

4. 小箭頭^混亂的問題,到底放小括號內還是小括號外

聲明是 int(^MNBlock)(int a , int b)

實現是 ^int(int a, int b)

注意,這里箭頭之後的,不管是多寫() 還是少寫,都會出錯image.png

image.png

所以這里還不能死記,比如不管聲明還是實現,死記 (^ xxx) 是沒問題的 or 死記 ^…… xxx 不加括號是沒問題的,在這里都行不通,只能靠腦記了

這時候,就需要用到巧記了!

^ 和小括號組合的,一共有三種情況

  • 一種是聲明的,void(^MNBlock)

  • 一種是實現的,^int(int a,)

  • 還一種 ^(int a)

兄弟,看到這你還不亂嗎。/p>

image.png

怎麼記看這里,

  • 手寫分為兩個部分,block等號左邊 or 等號右邊的,左邊為聲明,右邊為實現區分開

  • 聲明記。後面跟blockName,他們需要包起來! (blockName),只有聲明會用到blockName,先記住一點,如果有blockName,要和一起,用小括號包起來

  • 實現又分為兩種︰

    • ^int:^後面跟的是返回值類型

      • ^ 直接跟類型,不用加”( )” ==> ^int

    • ^(int a):^後面直接跟參數 (返回值是void)。

      • 參數都是要用”( )”包起來的,如果^後面跟參數,就得用”( )” ==> ^(int a),

      • 實現里,肯定有實際參數,這時候,參數類型和實參,就得用( )包起來

^與小括號糾纏的總結

  • ^ 後面僅跟類型,不需要小括號,==> ^int

  • ^ 後面跟參數,參數需要小括號 ==> ^(int a)

  • ^ 後面跟block名稱,^和blockName需要小括號 ==> void (^MNBlock)

Block原理探究

void (^MNBlock)(void) = ^(void){    NSLog(@"this is a Block~ rua~");};MNBlock();

使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 轉成 C++ 代碼, 查看底層結構

//對應上面的 MNBlock聲明void (*MNBlock)(void) = (&__main_block_impl_0(__main_block_func_0,                                                      &__main_block_desc_0_DATA));        //對應上面的 MNblock() 調用MNBlock->FuncPtr(MNBlock);
//block聲明調用的 - __main_block_impl_0struct __main_block_impl_0 {  //結構體內的參數  struct __block_impl impl;  struct __main_block_desc_0* Desc;    //c++中的構造函數,類似于 OC 的 init 方法,返回一個結構體對象  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};struct __block_impl {  void *isa;  int Flags;  int Reserved;  void *FuncPtr;};static struct __main_block_desc_0 {  size_t reserved;  size_t Block_size;}

這里的block封裝的函數調用解釋MNBlock->FuncPtr(MNBlock);

MNBlock 其實內部結構是 __main_block_impl_0,

struct __main_block_impl_0 {  //函數調用地址在這個結構體內  struct __block_impl impl;    struct __main_block_desc_0* Desc;  }    struct __block_impl {  void *isa;  int Flags;  int Reserved;  //函數調用地址在這里  void *FuncPtr;};

內部只有兩個參數,一個impl,一個Desc,而函數的調用地址 – FuncPtr是再impl中的,為什麼這里能直接這樣寫呢?

因為,__main_block_impl_0 結構的地址和他的第一個成員一樣,第一個成員的地址是__block_impl,所以__main_block_impl_0 和 __block_impl 的地址其實是同一個,通過格式強制轉換,將 main_block_impl_0 轉成 block_impl 就可以直接拿到他內部的 FuncPtr 函數地址,然後進行調用!

image.png

  • 可見- block本質上是OC對象,內部有一個isa指針

  • block是封裝了函數調用以及函數調用的oc對象

Block面試題拋磚引玉~

開胃菜先來一下,以下結果輸出什麼

int a = 10;void (^MNBlock)(void) = ^{    NSLog(@"a = %d",a);};a += 20;MNBlock();

調用 MNBlock(); 之前,a 已經 + 20了,輸出30? 太天真了兄弟,這里涉及到capture的概念,即變量捕獲

Block捕獲變量(capture)

捕獲︰Block內部會新增一個成員,來存儲傳進來的變量

image.pngblock 內部直接捕獲了傳進去的這個變量a(10)

image.png

創建block的時候,已經將變量a=10 捕獲到 block內部,之後再怎麼修改,不會影響block 內部的  a

auto 和 static的區別:以下會輸出什麼~

static int b = 10;void (^MNBlock)(void) = ^{    NSLog(@"a = %d, b = %d",a,b);};a = 20;b = 20;MNBlock();

輸出

2019-03-07 21:49:49 Block-Demo a = 10, b = 20

why?

查看原因:

auto int a = 10;static int b = 10;void (*MNBlock)(void) = (&__main_block_impl_0(__main_block_func_0,                                              &__main_block_desc_0_DATA,                                              a,                                              &b));

發現︰兩種變量,都有捕獲到block內部。

a 是auto變量,走的是值傳遞,

b 是 static 變量,走的是地址傳遞,所以會影響(指針指向同一塊內存,修改的等于是同個對象)

總結

  • 只有局部變量才需要捕獲,

  • 全局變量不需要捕獲,因為在哪都可以訪問

  • 需不需要捕獲,其實主要是看作用域問題

  • auto局部變量 ==>值傳遞->因為會銷毀

  • static局部變量==>不會銷毀==>所以地址傳遞

看圖就行~

image.png

進階考題 – self 會被捕獲到 block 內部嗎

void (^MNBlock)(void) = ^{    NSLog(@"p = %p",self);};

模擬看官作答︰不會,因為整個類里,都能調用self,應該是全局的,全局變量不會捕獲到block中

哈哈哈哈!中計了!其實 self 是參數(局部變量)

struct __MNDemo__test_block_impl_0 {  struct __block_impl impl;  struct __MNDemo__test_block_desc_0* Desc;  MNDemo *self; ==> 捕捉到了兄弟  }

解釋原因︰

  • 每個OC函數,其實默認有兩個參數,一個self,一個_cmd,只是他們z兄弟默認是隱藏的

  • 而由于他們是參數,所以是局部變量,局部變量就要被 block 捕獲

  • – (void)test(self, SEL _cmd){XXX} 默認的OC方法里面其實有這兩個隱藏的參數!所以上題的答案,self是會被block捕獲的。 芴 粕。/p>

進進階考題 – 成員變量_name 會被捕獲到 block 內部嗎

void (^MNBlock)(void) = ^{    NSLog(@"==%@",_name);};

模擬看官作答︰呵呵,老子都中了這麼多次技了,這題學會了。因為_name是成員變量,全局的,也沒有self,所以不需要捕獲整個類就都可以隨便訪問它!

哎,兄弟,還是太年輕了。/p>

void (^MNBlock)(void) = ^{    NSLog(@"==%@",self->_name);};

看圖說話,不多bb, (能听懂掌聲。/p>

Block的類型

  • __NSGlobalBlock__

  • __NSStackBlock__

  • __NSMallocBlock__

MRC環境下

void (^global)() = ^{    NSLog(@"globalValue = %d",globalValue);};void (^autoBlock)() = ^{    NSLog(@"this is a Block~ rua~ = %d",a);};void (^copyAuto)() = [autoBlock copy];--------------------------------------------print class2019-03-08 17:40:43 Block-Demo global class = __NSGlobalBlock__  autoBlock class = __NSStackBlock__  copyAuto = __NSMallocBlock__

總結:

image.png

內存分配示意圖︰

image.png

棧上的內存系統會自動回收

  • 棧空間的block 不會對 對象進行強引用

  • 堆空間的block 可能會對對象產生強引用︰

    • 如果是weak指針,不會強引用

    • 如果是strong指針,會強引用

堆上的內存是由程序員控制,所以一般將block 拷貝到堆上,讓程序員控制他與內部變量的生命周期

題目︰以下輸出的順序是什麼(ARC環境下)

@implementation MNPerson- (void)dealloc{    NSLog(@"MNPerson - dealloc");}@end--------------------------------------MNPerson *person = [[MNPerson alloc]init];__weak MNPerson *weakPerson = person;dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{        NSLog(@"1-----%@",person);        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{        NSLog(@"2------%@",weakPerson);    });    });NSLog(@"touchesBegan");

輸出結果

2019-03-08 22:38:59.038452+0800 touchesBegan2019-03-08 22:39:00.056746+0800 1-----2019-03-08 22:39:00.057891+0800 MNPerson - dealloc2019-03-08 22:39:02.058011+0800 2-----(null)

解釋︰

  1. gcd的block會自動對auto變量進行copy操作

  2. block內部對 auto 變量的強弱引用,取決于指針類型

  3. 1 中的auto變量是 person,沒聲明默認對象是 strong 類型,所以 gcd1 會對 person進行 1s的強引用

  4. gcd2 中的變量是 weakPerson,看到是__wesk指針,所以block內部不會對其產生強引用

  5. 隨後,gcd1 對 person進行1s的強引用之後,gcd1 的block銷毀,person對象銷毀,打印MNPerson dealloc

  6. 最終,2s過後打印 2——weakPerson,因為person對象在gcd1 block結束之後,釋放掉了,所以此時person是空,因為是weak指針,對象是null不會crash,最終打印null

對象類型的auto變量

當 block 內部訪問了對象類型的auto變量時

  • 如果block在展示,不會對 auto 變量產生強引用

  • 如果 block 被 拷貝到堆上

    • 會調用 block 內部的 copy 函數

    • copy 函數內部會調用 _Block_object_assign 函數

    • _Block_object_assign 函數會根據auto變量的修飾符 ( strong、 weak、unsafe_unretained ) 做出對應的操作,看對內部auto變量進行強引用還是弱引用(類似于 retain)

  • 如果 block 從 堆上移除

    • 會調用 block 內部的 dispose 函數

    • dispose函數內部會調用_Block_object_dispose 函數

    • _Block_object_dispose 類似于 release,會對auto變量進行自動釋放(當引用計數器=0的時候 )

image.png

block中的copy

在ARC環境下,編譯器會根據情況,自動將棧上的block拷貝到堆上,比如以下幾種情況

  • block 作為函數返回值的時候

  • 將block復制給__strong指針的時候

  • block作為Cocoa API中方法名含有usingBlock的方法參數事

    • 比如︰[array enumerateObjectsUsingBlock:XXX]

__block 修飾符的使用

題目︰以下代碼的是否編譯通過,可以的話輸出結果是什麼

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

結果如下︰

image.png

思考︰無法編譯,為啥呢?編譯的時候,block應該是會把auto變量捕獲進去的,那block結構中應該有a才對啊

//main函數int main(int argc, const char * argv[]) {    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;                 int a = 10;        void (*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));    }    return 0;}//block執行地址  static void __main_block_func_0(struct __main_block_impl_0 *__cself) {  int a = __cself->a; // bound by copy  NSLog((NSString *)&__NSConstantStringImpl__var_folders_kh_0rp73c0s2mvfp5gjf25j5y6h0000gn_T_main_1a12fa_mi_0,a);}

block執行的時候,內部是 __main_block_func_0 函數,而a的聲明,是在main函數,兩個函數相互獨立,對于他們來說,a都是一個局部變量,而且兩個函數中都對a初始化,兩個函數的中a不是同一個,那怎麼可以在 執行函數中,修改main函數中的局部變量呢,所以編譯報錯!

如何改?

  • 方案一︰使用static

static int a = 10;void (^block)() = ^{    a = 20;    NSLog(@"a = %d",a);};

因為static修飾的auto變量,最終在block中進行的不是值傳遞,而是地址傳遞,措意執行函數中的a 和 main 函數中的a,是同一個地址 ==> 等于同一個a,所以可以修改,輸出20

但是使用static,就會變成靜態變量,永遠在內存中

  • 方案二︰ 使用__blcok

__block auto int a = 10;void (^block)() = ^{    a = 20;    NSLog(@"a = %d",a);};
struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  __Block_byref_a_0 *a; // by ref ==> auto的話,是int a,__block,變成對象了}
struct __Block_byref_a_0 {  void *__isa;__Block_byref_a_0 *__forwarding;==> 指向自己的結構體 int __flags; int __size; int a; ==> 10在這里};

a = 20;最終轉成 (a->__forwarding->a) = 20;

解釋下︰__forwarding  是指向結構體本身的指針,等價于a本身,其實就是通過a的結構體指針,拿到里面的成員a,再對他賦值

指針傳遞,所以可以修改 auto 變量,通過block,間接引用 auto 變量

image.png

__block的內存管理

  • 當 block 在棧上的時候,不會對內部的__block 變量產生強引用

  • 當 block 從棧上被 copy 到堆上的時候

    • 會調用block內部的copy函數

    • copy函數內部會調用_Block_object_assign 函數

    • _Block_object_assign 函數會對 __block 變量進行一次 retain操作,產生強引用

抄圖分析 :

image.png

image.png

  • 當block從堆中移除時

    • 會調用 block 內部的 dispose 函數

    • dispose內部會調用_Block_object_dispose函數

    • _Block_object_dispose函數會對__block變量進行一次release操作,如果retainCount為0,自動釋放該__block變量

image.png

image.png

總結︰

  • block在棧上的時候,不會對內部的變量產生強引用

  • 當block從棧上 copy 到堆上的時候,內部都會調用 __Block_object_assign

    • 如果是__block修飾的變量,會__block修飾的對象產生強引用

    • 如果是普通auto變量,看修飾的指針類型是strong 還是 weak(unsafe_unretained)

      • strong修飾的,block就會對內部的auto變量產生強引用

      • weak修飾的,block就不會對內部的auto變量產生強引用

    • 特別注意!上述條件僅在ARC環境下生效,如果是MRC環境下,block不會對內部auto變量產生強引用!(MRC下不會進行retain操作)

  • 當block從堆上移除的時候,內部會調用__Block_object_dispose函數,相當于對block內部所持有的對象進行移除release操作,如果retainCount為0,自動釋放該__block變量

__block中的 _ forwarding 指針

內存拷貝的時候,如果block從棧被copy到堆上,肯定也希望內部的變量一起存儲到堆上(讓變量的生命周期可控,才不會被回收)

加入變量a在棧上,在棧上的指針,指向堆上的 block,堆上的block的 forwarding指向他自己,就可以保證,修改&獲取的變量,都是堆上的變量

image.png

最終,__block指向的變量,是指向堆上的

__block 修飾的類型

@implementation MNObject- (void)dealloc{    NSLog(@"MNObject - dealloc");}@end--------------------------------------------typedef void (^MNBlock)();MNBlock block;{    MNObject *obj = [[MNObject alloc]init];    __block __weak MNObject *weakObj = obj;        block = ^{        NSLog(@"----------%p",weakObj);    };}block();

問,上述代碼的輸出順序是?

2019-03-09 21:57:56.673296+0800 Block-Demo[72692:8183596] MNObject - dealloc2019-03-09 21:57:56.673520+0800 Block-Demo[72692:8183596] ----------0x0

解釋︰ARC下

image.png

上述代碼,block 持有的是 weakObj,weak指針,所以block內部的__block結構體,對他內部持有的person不強引用!所以出了 小括號後,person沒有被強引用,生命gg,先dealloc,輸出dealloc,之後進行block調用,打印 ———

特別注意,上述邏輯進在ARC下,如果在MRC下,中間結構體對象,不會對person 進行retain操作! 即便 person 是強指針修飾,也不會對內部的person對象進行強引用!

MRC環境下

MNBlock block;{    MNObject *obj = [[MNObject alloc]init];    block = [^{        NSLog(@"----------%p",obj);    }copy];        [obj release];}block();[block release];--------------------輸出:2019-03-09 21:59:56.673296+0800 Block-Demo[72692:8183596] MNObject - dealloc2019-03-09 21:59:56.673520+0800 Block-Demo[72692:8183596] ----------0x0

上述代碼,obj 是 strong 修飾,但是並沒有被 block 強引用!可見MRC環境下,修飾的對象,生成的中間block對象不會對 auto變量產生強引用。

Block的循環應用問題

傳送門︰ 實際開發中-Block導致循環引用的問題(ARC環境下)

考題︰MRC 下,block的循環引用如何解決呢?

  • 方案1︰unsafe_unretained

MRC下,沒有__weak,所以只能用_unsafe_unretained指針,原理和 weak 一樣(ARC環境下不推薦使用,可能導致野指針,推薦使用weak)

__unsafe_unretained MNObject *weakSelf = self;self.block = [^{    NSLog(@"----------%p",weakSelf);}copy];

方案2︰ __block

__block self;self.block = [^{    NSLog(@"----------%p",self);}copy];

why? 上面關于 __block的總結

特別注意!上述條件僅在ARC環境下生效,如果是MRC環境下,block不會對內部auto變量產生強引用!(MRC下不會進行retain操作)

image.png

  • 方案3: 手動在block函數內將對象制空,並且必須手動保證block調用

MNObject *obj = [[MNObject alloc]init];__unsafe_unretained MNObject *weakObj = obj;obj.block = [^{    NSLog(@"----------%p",obj);    obj = nil;}copy];obj.block();

image.png

但是這個一定要注意,block必須調用,因為對象指針的清空操作,是寫在block函數中的,如果沒調用block,循環引用問題還是會存在,所以不推薦使用。

實際開發中,循環引用的檢測工具推薦,facebook開源的 FBRetainCycleDetector,用過的都說好~

話外篇補充 – Block 和 delegate使用場景

個人愚見

  • 直接異步返回的,可以用block,比如網絡請求,無需其他人工動作觸發的

  • 如果是需要類似點擊才能觸發的,比如 Button的點擊事件,可以用 delegate


老實說,block其實非常難,能考得特別深,本文也只是簡單探究&總結下中級iOS常見的block考題,以及對Block底層的初步探究,如果是像我所在的三線城市,去面試那種非一線公司的話,如果能掌握本文,可能block相關的題目能答個八九不離十吧!(可能題目會變換組合,但是萬變不離其宗)

block的文章其實很多,但是如果要真的深入理解,還是得動手,這里推薦初中級iOSer可以跟著本文的思路,一步一步跟著探究試試,本文只是起個拋磚引玉的作用

友情演出:小馬哥MJ

參考資料

Objective-C 高級編程 iOS與OS X多線程和內存管理

實際開發中-Block導致循環引用的問題(ARC環境下)

招聘一個靠譜的 iOS

ChenYilong/iOSInterviewQuestions

作者︰小蠢驢打代碼

鏈接︰http://www.wxcool.cnp/11f7c40076e9

贊(0) 打賞
未經允許不得轉載︰ » 面試驅動电竞 – Block看我就夠了【干貨】
分享到︰ 更多 (0)

評論 搶沙發

中文字幕亚洲无线码|国产亚洲观看视频在线|性欧美长视频免费观看

關于我們聯系我們

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

微信掃一掃打賞