首頁 > 軟體

詳解Objective C 中Block如何捕獲外部值

2022-09-05 14:00:06

引言

Block 本質上也是一個 Objective-C 物件,它內部也有個 isa指標。Block 是封裝了函數呼叫以及函數呼叫環境的 Objective-C 物件。Block 的底層結構如下圖所示:

Block 對於不同型別的值會有不同的捕獲方式,本文將通過程式碼展示其對於各種場景下的外部值是如何進行捕獲的。

自動變數

首先展示原始碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger value = 0;
        void(^block)(void) = ^{
            NSLog(@"%zd", value);
        };
        block();
    }
    return 0;
}

經過 clang -rewrite-objc 之後,得到的程式碼如下,可以看到,對於自動變數的捕獲,是會在 Block 結構體中生成一個對應型別的成員變數來實現捕獲的能力,這也解釋了為什麼在 Block 中修改捕獲的值的內容,無法對 Block 外的值產生影響。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger value; // 捕獲的 NSInteger value
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSInteger value = __cself->value; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_e3ca95_mi_0, value);
        }
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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSInteger value = 0;
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

靜態變數、靜態全域性變數與全域性變數

對於靜態變數、靜態全域性變數與全域性變數的捕獲,會稍有不同,其中:

  • 全域性變數與靜態全域性變數:直接使用,因為地址一直是可以直接獲取的。
  • 靜態變數:捕獲地址使用,因為 block 有可能會傳遞出建立時的作用域。
NSInteger globalValue = 1;
static NSInteger staticGlobalValue = 2;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static NSInteger staticValue = 3;
        void(^block)(void) = ^{
            globalValue += 1;
            staticGlobalValue += 2;
            staticValue += 3;
        };
        block();
    }
    return 0;
}
NSInteger globalValue = 1;
static NSInteger staticGlobalValue = 2;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger *staticValue;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger *_staticValue, int flags=0) : staticValue(_staticValue) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSInteger *staticValue = __cself->staticValue; // bound by copy
            globalValue += 1;
            staticGlobalValue += 2;
            (*staticValue) += 3;
        }
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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        static NSInteger staticValue = 3;
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staticValue));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

帶 __block 的自動變數

__block 修飾的自動變數,可以在 Block 內部對其外部的值進行修改:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSInteger value = 0;
        void(^block)(void) = ^{
            value = 10;
        };
        block();
        NSLog(@"%zd", value);
    }
    return 0;
}

這次生成的程式碼複雜了一些,不過只關注 value 部分的話可以發現,Block 為了捕獲 __block 型別的自動變數,會生成 __Block_byref_value_0 結構體,並通過該結構體來實現對外部 __block 自動變數的捕獲。

struct __Block_byref_value_0 { // 為捕獲 __block 的自動變數,生成的結構體。為的是方便多個 Block 同時捕獲一個自動變數時使用。
  void *__isa; // isa 指標
__Block_byref_value_0 *__forwarding; // 在 Block 單純在棧上是,指向的是自己,拷貝到堆上後,指向的是在堆上的 Block。之所以需要這樣的指標是因為當 Block 拷貝到堆上時,呼叫方式是統一的。
 int __flags;
 int __size;
 NSInteger value; // 具體的值
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_value_0 *value; // 通過參照的方式捕獲 value,其中變數型別為 __Block_byref_value_0
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__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_value_0 *value = __cself->value; // bound by ref
            (value->__forwarding->value) = 10; // 賦值程式碼
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 0};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_6bf1c6_mi_0, (value.__forwarding->value));
    }
    return 0;
}

__block 可以用於解決 block 內部無法修改 auto 變數值的問題,__block 不能修飾全域性變數、靜態變數(static),編譯器會將 __block 變數包裝成一個物件。

block 在棧上時,並不會對 __block 變數產生強參照。

blockcopy 到堆時,會呼叫 block 內部的 copy 函數,copy 函數內部會呼叫 _Block_object_assign 函數,_Block_object_assign 函數會對 __block 變數形成強參照(retain)。

block 從堆中移除時,會呼叫 block 內部的 dispose 函數,dispose 函數內部會呼叫 _Block_object_dispose 函數,_Block_object_dispose 函數會自動釋放參照的 __block 變數(release)。

捕獲物件

在探究完對標量型別的捕獲之後,讓我們看一下對物件型別的捕獲:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray *array = [NSArray array];
        void(^block)(void) = ^{
            NSLog(@"%@", array);
        };
        block();
    }
    return 0;
}

通過轉譯的程式碼可以看出,因為物件型別本身已經是儲存在堆上的值了,所以直接獲取其地址即可,同時其新增了兩個函數 __main_block_copy_0__main_block_dispose_0,這兩個函數是用來將物件拷貝到堆上和被從堆上移除時呼叫的,其內部又分別呼叫了 _Block_object_assign_Block_object_dispose 用來對捕獲的物件進行參照計數的增加和減少。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSArray *array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSArray *_array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSArray *array = __cself->array; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_8ba4f7_mi_0, array);
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSArray *array = ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"));
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

Block 物件本身分為三種型別:

  • NSGlobalBlock:沒有存取 auto 變數,呼叫 copy 方法之後不會發生變化。
  • NSStackBlock:存取了 auto 變數,呼叫 copy 方法之後儲存位置從棧變為堆。
  • NSMallocBlock__NSStackBlock__ 呼叫了 copy 方法之後,參照計數增加。

ARC 環境下,編譯器會根據情況自動將棧上的 block 複製到堆上,比如以下情況:

  • Block 作為函數返回值時
  • Block 賦值給 __strong 指標時
  • Block 作為 Cocoa API 中方法名含有 usingBlock 的方法引數時
  • Block 作為 GCD API 的方法引數時

所以,當 Block 內部存取了物件型別的 auto 變數時。如果 Block 是在棧上,將不會對 auto 變數產生強參照。

如果 Block 被拷貝到堆上,會呼叫 Block 內部的 copy 函數,copy 函數內部會呼叫 _Block_object_assign 函數,_Block_object_assign 函數會根據 auto 變數的修飾符(__strong__weak__unsafe_unretained)做出相應的操作,形成強參照或者弱參照。

如果 Block 從堆上移除,會呼叫 Block 內部的 dispose 函數,dispose 函數內部會呼叫 _Block_object_dispose 函數。_Block_object_dispose 函數會自動釋放參照的 auto 變數(release)。

__block 物件型別的捕獲

如果想在 Block 中,對捕獲的物件的指標指向進行修改,則需要新增 __block 關鍵字:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSArray *array = [NSArray array];
        void(^block)(void) = ^{
            array = [NSArray array];
            NSLog(@"%@", array);
        };
        block();
    }
    return 0;
}

通過轉譯我們可以看出,跟 __block 修飾的標量型別相似,同樣會生成 __Block_byref_array_0 結構體來捕獲物件型別。同時其內部生成了 __Block_byref_id_object_copy__Block_byref_id_object_dispose 兩個函數指標,用於對被結構體包裝的物件進行記憶體管理。

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
struct __Block_byref_array_0 {
  void *__isa;
__Block_byref_array_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSArray *array;
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_array_0 *array; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_array_0 *_array, int flags=0) : array(_array->__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_array_0 *array = __cself->array; // bound by ref
            (array->__forwarding->array) = ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_zyxvpxvq6csfxvn_n0000000000000_T_main_3593f0_mi_0, (array->__forwarding->array));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_array_0 array = {(void*)0,(__Block_byref_array_0 *)&array, 33554432, sizeof(__Block_byref_array_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("array"))};
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_array_0 *)&array, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

block 在棧上時,對它們都不會產生強參照。

block 拷貝到堆上時,都會通過 copy 函數來處理它們,__block 變數(假設變數名叫做 a):

_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

物件型別的 auto 變數(假設變數名叫做 p):

_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

block 從堆上移除時,都會通過 dispose 函數來釋放它們,__block 變數(假設變數名叫做 a):

_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

物件型別的 auto 變數(假設變數名叫做 p):

_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

以上就是詳解Objective C 中Block如何捕獲外部值的詳細內容,更多關於Objective C Block捕獲外部值的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com