深入了解Block的奧秘

前言

block可以叫回調(diào)代碼塊,是iOS開發(fā)中至關(guān)重要的形式之一。不同的編程語言都會用到block, 只是體現(xiàn)形式有所不同,例如c/c++函數(shù)指針javascript叫它閉包。它用簡單的方式幫我們解決了很多復(fù)雜的問題。

block如何將變量傳遞及持有

測試代碼:

傳遞原則:

  1. 捕獲對象是基礎(chǔ)類型變量,如int, double類型時,是值傳遞。
  2. 捕獲對象是一個object,那么它會被強引用

我們來驗證一下,我們在block賦值之后修改a的值
ViewController.m:

    int a = 10;
    _foo.testBlock= ^() {
        _testView.backgroundColor = nil;//持有了self
        NSLog(@"a:%d", a);
    };
    a+= 10;
    _foo.testBlock();

輸出結(jié)果:仍然是10,它不會被外界改變。
但是如果我們用__block來修飾int a,也就是 __block int a = 10, 最終a的值就是20,它被外界改變了,__block幫我們解決問題。

但是Why? block內(nèi)部是以什么形式存在,并捕獲值的呢?接下來我們要一探究竟。

準(zhǔn)備工作:clang命令

大家可以用clang(或者gcc) -rewrite-objc xxxxx.m命令來查看轉(zhuǎn)化成的c++代碼來了解內(nèi)幕。如果你引用了UIKIt庫,這個命令會報錯,那個因為命令里沒有指定sdk的版本,此時用下面的命令完美解決:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

__block的奧秘

不帶__block的轉(zhuǎn)化cpp代碼:

    int a = 10;
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, a, 570425344)));
    a+= 10;
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();

帶__block轉(zhuǎn)化代碼:

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};//對a的封裝進行初始化
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, (__Block_byref_a_0 *)&a, 570425344)));
    (a.__forwarding->a)+= 10;
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();

__Block_byref_a_0的定義如下:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;//---->它就是傳遞進來的a
};

我們發(fā)現(xiàn)編譯器把int a封裝成了一個叫__Block_byref_a_0的結(jié)構(gòu)體,最終用&符號將它的地址傳遞給了block,所以后面a被修改時,block里面的a也被同時修改。__block的奧秘就是這個!

Block的存在形式

object-c的Block最終以轉(zhuǎn)化成多個了結(jié)構(gòu)體,每個結(jié)構(gòu)體都不同的職責(zé)。
__ViewController__viewDidLoad_block_impl_0 包含了block相關(guān)的所有信息的
基本構(gòu)造是:

  1. impl
  2. desc
  3. 引用變量列表:ivar1, ivar2, ivar...
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;//block的一個簡化類,它里面存儲了函數(shù)指針
  struct __ViewController__viewDidLoad_block_desc_0* Desc;//block的描述類
  ViewController *self;//被引用的viewcontroller
  __Block_byref_a_0 *a; // 被引用的a的封裝
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *_self, __Block_byref_a_0 *_a, int flags=0) : self(_self), a(_a->__forwarding) {//構(gòu)造函數(shù)
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__block_impl定義如下,很像一個類,里面有isa指針和block的函數(shù)指針,所以我們可以把它當(dāng)作一個類

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

我們可以發(fā)現(xiàn)__ViewController__viewDidLoad_block_impl_0結(jié)構(gòu)體的第一個參數(shù)是__block_impl,所以這兩個結(jié)構(gòu)體實例的地址是相同的。那到底oc的block是誰呢?我們一般會認(rèn)為oc的block就是__block_impl,因為它正好有isa,可以作為一個類,這樣大家都容易理解。

Block的三個子類

1.我們可以打印一個block的類及父類的名字:(這段代碼摘自facebook的FBRetainCycleDetector)

static Class _BlockClass() {
  static dispatch_once_t onceToken;
  static Class blockClass;
  dispatch_once(&onceToken, ^{
    void (^testBlock)() = [^{} copy];
    blockClass = [testBlock class];
    while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) {
      blockClass = class_getSuperclass(blockClass);
    }
    [testBlock release];
  });
  return blockClass;
}

結(jié)果是__NSGlobalBlock -> NSBlock -> NSObject
事實上block有三種形式:

  • __NSGlobalBlock 全局 (未捕獲變量)
  • __NSStackBlock 棧 捕獲變量
  • __NSMallocBlock 堆 捕獲變量

在 ARC 中,捕獲外部了變量的 block 的類會是 NSMallocBlock 或者 NSStackBlock,如果 block 被賦值給了某個變量在這個過程中會執(zhí)行 _Block_copy 將原有的 NSStackBlock 變成 NSMallocBlock;但是如果 block 沒有被賦值給某個變量,那它的類型就是 NSStackBlock;沒有捕獲外部變量的 block 的類會是 NSGlobalBlock 即不在堆上,也不在棧上,它類似 C 語言函數(shù)一樣會在代碼段中。

2.那什么時候在堆上,什么時候在棧上呢?
在ARC有效時,大多數(shù)情況下編譯器會進行判斷,自動生成將Block從棧上復(fù)制到堆上的代碼,以下幾種情況棧上的Block會自動復(fù)制到堆上:

  1. 調(diào)用Block的copy方法
  2. 將Block作為函數(shù)返回值時
  3. 將Block賦值給__strong修改的變量時
  4. 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數(shù)時

3.block用strong修飾還是copy修飾呢?
實際上調(diào)用retain方法時, block會調(diào)用copy方法,所以這兩種修飾是相同的。但是為了語義的明確,推薦用copy修飾。

獲取block引用的對象

現(xiàn)在不講源碼,只講實際應(yīng)用。如何知道一個block對象,如何知道它持有的所有對象呢?facebook的FBRetainCycleDetector檢測循環(huán)引用的庫就實現(xiàn)這樣的功能,非常的巧妙!我們就FBRetainCycleDetector的源碼展出分析。
1.調(diào)用allRetainedObjects來獲取:

- (NSSet *)allRetainedObjects
{
  NSMutableArray *results = [[[super allRetainedObjects] allObjects] mutableCopy];

  // Grab a strong reference to the object, otherwise it can crash while doing
  // nasty stuff on deallocation
  __attribute__((objc_precise_lifetime)) id anObject = self.object;//objc_precise_lifetime 翻譯一下就是:精確生命周期,其實是強引用了object對象,防止在運行期間被釋放

  void *blockObjectReference = (__bridge void *)anObject;
  NSArray *allRetainedReferences = FBGetBlockStrongReferences(blockObjectReference);//獲得所有引用對象,就是下面要講的方法

  for (id object in allRetainedReferences) {
    FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, object, self.configuration);//對每個對象進行包裝成box對象
    if (element) {
      [results addObject:element];
    }
  }

  return [NSSet setWithArray:results];
}

2.FBGetBlockStrongReferences方法通過調(diào)用_GetBlockStrongLayout(block)方法返回的持有對象的位置Index, 然后通過偏移量來取得對應(yīng)的對象:

NSArray *FBGetBlockStrongReferences(void *block) {
  if (!FBObjectIsBlock(block)) {//是否是block類型
    return nil;
  }
  
  NSMutableArray *results = [NSMutableArray new];

  void **blockReference = block;
  NSIndexSet *strongLayout = _GetBlockStrongLayout(block);//得到block里強引用的對象
  [strongLayout enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
    void **reference = &blockReference[idx];//把它當(dāng)作了一個鏈表來取值,它本是block結(jié)構(gòu)體,正好它的align對齊字節(jié)大小是void*類型大小。怪不得blockReference要用void **來修飾,要以把它當(dāng)作(void *)*blockReference,所以blockReference是一個指向(void *)的指針。

    if (reference && (*reference)) {
      id object = (id)(*reference);

      if (object) {
        [results addObject:object];
      }
    }
  }];

  return [results autorelease];
}

我寫了一個demo, block里引用了viewcontroller, 但是viewController的idx是4,為什么呢?我們打印了blockLiteral結(jié)構(gòu)體及成員變量的地址:


B8A28F62-BFEB-4BEF-B26A-41D0DBC475CA.png
struct BlockLiteral {
  void *isa;  //0x00000001702493f0  占8個字節(jié)
  int flags;//0x00000001702493f8  占4個字節(jié)
  int reserved;//0x00000001702493fc 占4個字節(jié)
  void (*invoke)(void *, ...);//0x0000000170249400 占8個字節(jié)
  struct BlockDescriptor *descriptor;//0x0000000170249408 占8個字節(jié)
  // imported variables
};

所有指針類型全是占8個字節(jié),int類型4個字節(jié),所以flags+reserved加起來是相當(dāng)于一個指針類型,不管怎么說,BlockLiteral的大小是固定的,它的對齊字節(jié)是8,它的大小正好是sizeof(void)的整數(shù)倍。這也解釋了為什么定義了void **blockReference = block;,它把blockReference定義成了指向(void)的指針!(這里寫的有點啰唆,實際上void*是最長的字節(jié)8,因為Int類型字節(jié)長只有4,所以是以void*作為進行字節(jié)對齊的)
3._GetBlockStrongLayout是整個過程最關(guān)鍵,也是最巧妙的地方:

static NSIndexSet *_GetBlockStrongLayout(void *block) {
  struct BlockLiteral *blockLiteral = block;

  /**
   BLOCK_HAS_CTOR - Block has a C++ constructor/destructor, which gives us a good chance it retains
   objects that are not pointer aligned, so omit them.

   !BLOCK_HAS_COPY_DISPOSE - Block doesn't have a dispose function, so it does not retain objects and
   we are not able to blackbox it.
   */
  if ((blockLiteral->flags & BLOCK_HAS_CTOR)
      || !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) {
    return nil;
  }

  void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper;
  const size_t ptrSize = sizeof(void *);

  // Figure out the number of pointers it takes to fill out the object, rounding up.
  const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize;

  // Create a fake object of the appropriate length.
  void *obj[elements];
  void *detectors[elements];

  for (size_t i = 0; i < elements; ++i) {
    FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
    obj[i] = detectors[i] = detector;
  }

  @autoreleasepool {
    dispose_helper(obj);
  }

  // Run through the release detectors and add each one that got released to the object's
  // strong ivar layout.
  NSMutableIndexSet *layout = [NSMutableIndexSet indexSet];

  for (size_t i = 0; i < elements; ++i) {
    FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]);
    if (detector.isStrong) {
      [layout addIndex:i];
    }

    // Destroy detectors
    [detector trueRelease];
  }

  return layout;
}

重寫了FBBlockStrongRelationDetector類的release方法:

- (oneway void)release
{
  _strong = YES;
}

這個過程是:

  1. 獲取銷毀函數(shù)dispose_helper的函數(shù)指針
  2. 通過descriptor->size計算出block里成員變量個數(shù)(上面我們說過它把兩個int算作了一個void*, 所以成員變量的個數(shù)實際上少了1個)
  3. 創(chuàng)建兩個相同個數(shù)的fake數(shù)組obj,detectors,然后通過dispose_helper來只釋放obj數(shù)組,dispose_helper會向調(diào)用每個對象的release方法, 而它又重寫了release方法,在release時作了標(biāo)記,通過這個標(biāo)記就可以判斷是為是引用的對象。它其實是欺騙了dispose_helper函數(shù),因為它只認(rèn)位置Index, 并不關(guān)心數(shù)組存儲的是什么...

不得不贊嘆fb的源碼的深度和創(chuàng)新~

結(jié)語

還是那句話:源碼下面無秘密
蘋果底層對于block實現(xiàn)真是煞費苦心。我們了解了原理,用起來會更加得深應(yīng)手。

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

推薦閱讀更多精彩內(nèi)容

  • 前言 block可以叫回調(diào)代碼塊,是iOS開發(fā)中至關(guān)重要的形式之一。不同的編程語言都會用到block, 只是體現(xiàn)形...
    人仙兒a閱讀 308評論 0 0
  • 摘要 Blocks是C語言的擴充功能, iOS 4中引入了這個新功能“Blocks”,那么block到底是什么東西...
    CholMay閱讀 1,193評論 2 10
  • Blocks Blocks Blocks 是帶有局部變量的匿名函數(shù) 截取自動變量值 int main(){ ...
    南京小伙閱讀 974評論 1 3
  • 摘要block是2010年WWDC蘋果為Objective-C提供的一個新特性,它為我們開發(fā)提供了便利,比如GCD...
    西門吹雪123閱讀 939評論 0 4
  • 1. Block的底層結(jié)構(gòu) 以下是一個沒有參數(shù)和返回值的最簡單的Block: 為了探索Block的底層結(jié)構(gòu),需要將...
    再好一點點閱讀 484評論 0 4