理清 Block 底層結構及其捕獲行爲

Block 的本質

本質

  1. Block 的本質是一個 Objective-C 對象,它內部也擁有一個 isa 指針。
  2. Block 是封裝了函數及其調用環境的 Objective-C 對象

底層數據結構

一個簡單示例:ios

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

    void (^block)(void) = ^{
        NSLog(@"hey");
    };
    block();
    return 0;
}

複製代碼

將以上 Objective-C 源碼轉換成 c++ 相關源碼,使用命令行 : xcrun -sdk iphoneos xclang -arch arm64 -rewrite-objc 文件名c++

c++ 的結構體與通常的類類似。api

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

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}
複製代碼

其中 Block 的數據結構爲:bash

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};
複製代碼

impl 變量數據結構:數據結構

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr; 
};
複製代碼

FuncPtr:函數實際調用的地址,由於 Block 可看做是捕獲自動變量的匿名函數。iphone

Desc 變量數據結構:函數

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}
複製代碼

Block 的類型

Objective-C 中 Block 有三種類型,其最終類型都是 NSBlock 。ui

  • NSGlobalBlock (_NSConcreteGlobalBlock)
  • NSStackBlock (_NSConcreteStackBlock)
  • NSMallocBlock (_NSConcreteMallocBlock)

Block 類型的不一樣,主要根據捕獲變量的不一樣行爲產生:this

Block 類型 行爲
NSGlobalBlock 沒有訪問 auto 變量
NSStackBlock 訪問 auto 變量
NSMallocBlock NSStackBlock 調用 copy

在內存中的存儲位置

內存五大區:棧、堆、靜態區(BSS 段)、常量區(數據段)、代碼段spa

copy 行爲

不一樣類型的 Block 調用 copy 操做,也會產生不一樣的複製效果:

Block 類型 副本源的配置存儲域 複製效果
__NSConcreteStackBlock 從棧複製到堆
__NSConcreteGlobalBlock 數據段(常量區) 什麼也不作
__NSConcreteMallocBlock 引用計數增長
  • 在 ARC 環境下,編譯器會在如下狀況自動將棧上的 Block 複製到堆上:
  1. Block 做爲函數返回值
  2. 將 Block 賦值給 __strong 指針
  3. 蘋果 Cocoa、GCD 等 api 中方法參數是 block 類型

在 ARC 環境下,聲明的 block 屬性用 copy 或 strong 修飾的效果是同樣的,但在 MRC 環境下,則用 copy 修飾。

捕獲變量

爲了保證在 Block 內部可以正常訪問外部變量,Block 有一套變量捕獲機制:

變量類型 是否捕獲到 Block 內部 訪問方式
局部 auto 變量 值傳遞
局部 static 變量 指針傳遞
全局變量 直接訪問

若局部 static 變量是基礎類型 int val ,則訪問方式爲 int *val 若局部 static 變量是對象類型 JAObject *obj ,則訪問方式爲 JAObject **obj

基礎類型變量

一個簡單示例:

int age = 10;
// static int age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", age);
};
block();
複製代碼
  • 捕獲局部 auto 基礎類型變量生成的 Block 結構體 struct __main_block_impl_0 變爲:
struct __main_block_impl_0 {
  ···
  int age; // 傳遞值
}
複製代碼
  • 捕獲局部 static 基礎類型變量生成的 Block 結構體 struct __main_block_impl_0 變爲:
struct __main_block_impl_0 {
  ···
  int *age; // 傳遞指針
}
複製代碼
  • 捕獲全局基礎類型變量生成的結構體 struct __main_block_impl_0 沒有包含 age ,由於做用域爲全局,可直接訪問。

對象類型變量

一個簡單示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();
複製代碼
  • 捕獲局部 auto 對象類型變量生成的 Block 結構體 struct __main_block_impl_0 變爲:
struct __main_block_impl_0 {
  ···
  JAPerson *person;
}
複製代碼
  • 捕獲局部 static 對象類型變量生成的 Block 結構體 struct __main_block_impl_0 變爲:
struct __main_block_impl_0 {
  ···
  JAPerson **person;
}
複製代碼
  • 捕獲全局對象類型變量生成的結構體 struct __main_block_impl_0 沒有包含 person ,由於做用域爲全局,可直接訪問。

copy 和 dispose 函數

當捕獲的變量是對象類型或者使用 __Block 將變量包裝成一個 __Block_byref_變量名_0 類型的 Objective-C 對象時,會產生 copydispose 函數。

一個簡單示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
void (^block)(void) = ^{
     NSLog(@"age is %d", person.age);
};
block();
複製代碼

其中生成的 Block 的數據結構中多了 JAPerson 類型指針變量 person :

struct __main_block_impl_0 {
  ···
  JAPerson *person;
}
複製代碼

Desc 變量數據結構多了內存管理相關的函數:

static struct __main_block_desc_0 {
  ···
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}
複製代碼

這兩個函數的調用時機:

函數 調用時機
copy 棧上的 Block 複製到堆時
dispose 堆上的 Block 被廢棄時

copy 和 dispose 底層相關源碼

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);


// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
複製代碼

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

  • 若是 Block 是在棧上,將不會對 auto 變量產生強引用。
  • 若是 Block 被拷貝到堆上,會調用 Block 內部的 copy 函數,copy 函數內部會調用 _Block_object_assign 函數,_Block_object_assign 函數會根據 auto 變量的修飾符(__strong、__weak、__unsafe_unretain)做出相應的內存管理操做。

注意:若此時變量類型爲對象類型,這裏僅限於 ARC 時會 retain ,MRC 時不會 retain 。

  • 若是 Block 從堆上移除,會調用 Block 內部的 dispose 函數,dispose 函數內部會調用 _Block_object_dispose 函數,_Block_object_dispose 函數會自動 release 引用的 auto 變量。

使用 __weak 修飾的 OC 代碼轉換對應的 c++ 代碼會報錯: error: cannot create __weak reference because the current deployment target does not support weak references 此時終端命令需支持 ARC 並指定 Runtime 版本: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

內存管理

修改局部 auto 變量

局部 static 變量(指針訪問)、全局變量(直接訪問)均可以在 Block 內部直接修改捕獲的變量,而局部 auto 變量則主要經過使用 __block 存儲域修飾符來修改捕獲的變量。

  • __block 修飾符能夠用於解決 Block 內部沒法修改局部 auto 變量值的問題
  • __block 修飾符不能用於修飾全局變量、靜態變量(static)

編譯器會將 __block 修飾的變量包裝成一個 Objective-C 對象。

一個簡單示例:

__block int age = 10;
void (^block)(void) = ^{
   NSLog(@"age is %d", age);
};
block();
複製代碼

其中 Block 的數據結構多了一個 __Block_byref_age_0 類型的指針:

struct __main_block_impl_0 {
  ···
  __Block_byref_age_0 *age; // by ref
}
複製代碼

__Block_byref_age_0 結構體:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age; // age 真正存儲的地方
};
複製代碼

兩個注意點:

    1. 此處指針 val 是指向 age 的指針,而第二個 val 指的是 age 的值。

    1. 源碼裏面經過 age->__forwarding->age 的方式去取值,是由於這兩個 age 均可能仍在棧上,此時直接 age->age 訪問會有問題,而 copy 操做時 __forwarding 會指向堆上的 __Block_byref_age_0 ,此時就算第一個 age 仍在棧上,經過 age->__forwarding 會從新指向堆上的 __Block_byref_age_0 ,此時再訪問 age 便不會有問題 age->__forwarding->age

__block 的內存管理

使用 __block 修飾符時的內存管理狀況:

  • 當 Block 存儲在棧上時,並不會對 __block 變量強引用。
  • 當 Block 被 copy 到堆上時,會調用 Block 內部的 copy 函數,copy 函數會調用 __main_block_copy_0 函數對 __block 變量產生一個強引用。以下圖

  • 當 Block 從堆上被移除時,會調用 Block 內部的 dispose 函數,dispose 函數會調用 _Block_object_dispose 函數自動 release __block 變量。以下圖

__weak 和 __block 修飾時的引用狀況

    1. 僅用 __weak 修飾

一個簡單的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
    NSLog(@"person‘s age is %d", weakPerson.age);
};
複製代碼

    1. 使用 __block __weak 修飾

一個簡單的示例:

JAPerson *person = [[JAPerson alloc] init];
person.age = 10;
__block __weak typeof(person) weakPerson = person;
void (^block)(void) = ^{
     NSLog(@"person‘s age is %d", weakPerson.age);
};
block();
return 0;
複製代碼

循環引用

常見的循環引用問題:

ARC 環境下解決循環引用

    1. 弱引用持有:使用 __weak 或 __unsafe__unretain 解決

    1. 手動將一方置爲 nil :使用 __block 解決,在 block 內部將一方置爲 nil ,所以必須執行該 block

MRC 環境下解決循環引用

    1. 弱引用持有:使用 __unsafe__unretain 解決
    1. 直接使用 __block 解決,無需手動將一方置爲 nil ,由於底層 _Block_object_assign 函數在 MRC 環境下對 block 內部的對象不會進行 retain 操做。
相關文章
相關標籤/搜索