Objective-C高階程式設計讀書筆記(二)

kim_jin發表於2017-12-26

Blocks

含義:帶有自動變數值的匿名函式

Blocks 模式

Block 語法

^ 返回值型別 引數列表 表示式

與C語言函式的定義相比,不同點在於:沒有函式名,帶有^(插入記號)

Block 型別變數

引數型別 (^ 引數名稱) (block引數列表)

自動變數的捕獲

int main() {
  int dmy = 256;
  int val = 10;
  const char *fmt = "val = %d\n";
  void (^blk)(void) = ^{printf (fmt, val); };

  val = 2;
  fmt = "These values were changed, val = %d\n";
  blk();
  return 0;
}

// 執行結果
// val = 10
複製程式碼

從上面的程式碼可以看出,Block表示式使用的變數值,是在block宣告時變數的瞬間值。也就是說block會捕獲自動變數的瞬時值。在之後哪怕更改了變數的值,也不會對block造成影響。

__block說明符

block所捕獲的值不能再block中進行修改或重新賦值。如果想要修改捕獲的值的話,需要在block外對該變數使用__block進行修飾: __block int a = 0;

但是,對於oc物件的話,如果呼叫的是變更物件的方法的話,則沒有問題。比方說給捕獲到的陣列新增元素。

Blocks 的實現

實質

int main() {
  void (^blk)(void) = ^{ printf("Block\n"); };

  blk();
  return 0;
}
複製程式碼

使用-rewrite-obj將上面的程式碼,轉換為C++程式碼:

struct __block_impl {
  void *isa;  
  int Flags;  // 標誌
  int Reserved;  // 今後版本升級所需的區域
  void *FuncPtr;  // 函式指標
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}
複製程式碼

通過和原始碼的比對,可以看出對應的就是void (*blk) (void) = ((void(*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));這一句程式碼。這句程式碼的作用就是將棧上生成的__main_block_impl_0結構體例項的指標賦值給blk。

__main_block_impl_0()這個結構體的建構函式接受兩個引數,第一個是c語言的函式指標,第二個是__main_block_desc_0的結構體例項指標。

換句話說,就是將__main_block_impl_0中的__block_impl初始化為:

isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
複製程式碼

而Block的呼叫的話,就是簡單的通過函式指標來呼叫函式:(*blk->impl.FuncPtr)(blk);

捕獲變數

int main(int argc, char * argv[]) {
    
    int i  = 1;
    void (^blk)(void) = ^{
        printf("%d", i);
    };
    blk();
    return 0;
}
複製程式碼

同樣將上面的程式碼改寫為C++

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int i = __cself->i; // bound by copy

        printf("%d", i);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {

    int i = 1;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}
複製程式碼

可以看到__main_block_impl_0的結構體中,block外的變數被作為成員變數新增到了結構體當中。然後其建構函式變成了__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i)這樣:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
i = 1;
複製程式碼

因此可以看到,block的自動捕獲變數的功能,實際上是將該變數值儲存到block的結構體例項中(block自身)。

__block

在block中,靜態變數,靜態全域性變數和全域性變數這三種型別的變數是允許進行改寫的,除此之外,其他型別的變數,如果想在block中進行修改,就只能使用__block變數。

int i = 0;加上__block這個說明符,然後改寫成C++:

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_i_0 *i = __cself->i; // bound by ref

        printf("%d", (i->__forwarding->i));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, char * argv[]) {

    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}
複製程式碼

從程式碼我們看到,變數i變成了__Block_byref_i_0這個結構體例項(__block修飾的變數會被轉化為__Block_byref_ParameterName_0這樣的結構體),該結構體的宣告如下:

struct __Block_byref_i_0 {
  void *__isa;
  __Block_byref_i_0 *__forwarding;
  int __flags;
  int __size;
  int i; // 原變數
}
複製程式碼

如果在block中為__block變數賦值的話,其轉換程式碼為:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_i_0 *i = __cself->i; // bound by ref

        (i->__forwarding->i) = 2;
    }
複製程式碼

__Block_byref_val_0結構體例項的成員變數__forwarding是個指向例項自身的指標:

image.png

並且__Block_byref_i_0這個結構體並不在__main_block_impl_0這個結構體的例項中,這是因為為了在多個block中使用__block變數

Block 儲存域

Block實際上會轉換為__main_block_impl_0這樣的結構體型別的自動變數,__block變數會轉換為__Block_byref_para_0這樣的結構體型別自動變數。(結構體型別自動變數指的是棧上生成該結構體的例項)

名稱 實質
Block 棧上Block的結構體例項
__block變數 棧上__block變數的結構體例項

Block作為OC物件來看時,其類是_NSConcreteStackBlock,除了這個類之外,還有:

設定物件的儲存域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程式的資料區域
_NSConcreteMallocBlock

程式記憶體分配

可以看到block的型別有三種,可是怎麼知道block的型別呢?

當block為全域性變數或者是未使用捕獲自動變數時,為__NSConcreteGlobalBlock,其他情況為__NSConcreteStackBlock。 使用__block變數時,實際上是將block和變數從棧上覆制到堆上,此時,block的型別為__NSConcreteMallocBlock

Block的類 副本源的配置儲存域 複製效果
_NSConcreteStackBlock 從棧複製到堆
_NSConcreteGlobalBlock 程式的資料區域 什麼也不做
_NSConcreteMallocBlock 引用計數增加

而對於__block變數的話:

__block變數的配置儲存域 Block被複制到堆時的影響
從棧複製到堆並被Block持有
被Block持有

在Block被複制到堆上的時候,原有的__forwarding的值會指向堆上的Block的__block變數的結構體例項的地址

捕獲OC物件

通過__main_block_copy_0__main_block_dispose_0來改變物件的引用計數數值。 在以下幾種情況下,Block會被複制到堆上:

  • 呼叫block的copy方法
  • Block作為函式返回值返回時
  • 將Block賦值給帶有__strong的id型別的類或者Block型別的成員變數
  • 使用方法名稱含有usingBlock的Cocoa框架方法或者是GCD的block時

迴圈引用

解決block迴圈引用的方法有兩種,一種是__weak,另一種是__block.不過是引用__block的話,必須執行block,且在block中要將變數賦值為nil, 使用__block解決迴圈引用的優點在於可以控制物件的持有時間。

相關文章