OC - Block

343 阅读6分钟

可以用xcrun -sdk iphoneos clang -arch arm64 -fobjc-arc -fobjc-runtime=ios-10.0.0  -rewrite-objc main.m -o main.cpp 来探究block底层

写法:

fuckingclosuresyntax.com/

fuckingblocksyntax.com/

block的原理

本质是一个对象,成员变量包括函数的地址以及函数中用到的局部变量

block的底层结构

通过源码 源码地址

descriptor是表面是一个Block_descriptor_1类型的指针, 但blockflag不同, descriptor其指向的真实内存可能包含Block_descriptor_1 \ Block_descriptor_2 \ Block_descriptor_3, 搭配flag, 可以获取Block_descriptor_1 \ Block_descriptor_2 \ Block_descriptor_3的值

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime  
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime 
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime 
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler 
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime 
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler 
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler 
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler 
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

// block
struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;  // 如果
    // imported variables
};

image.png

通过clang rewrite 来验证一下, 发现基本上是类似的

int main(int argc, const char * argv[]) {
    int age = 0;
    void(^block)(void) = ^{
        age;
    };
    block();
    return 0;
}
 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;
   int age; // 捕获的局部变量
   __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
     impl.isa = &_NSConcreteStackBlock;
     impl.Flags = flags;
     impl.FuncPtr = fp;
     Desc = desc;
   }
 };

 static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
   int age = __cself->age; // bound by copy
   age;
 }

 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)};
 
 void(*block)(void) = &__main_block_impl_0(
                                          __main_block_func_0,
                                          &__main_block_desc_0_DATA,
                                          age
                                          );

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

变量捕获

遇到普通局部变量, 会进行值捕获, 因为这个局部变量, 可能在block调用的时候, 已经不存在了 遇到静态局部变量(static), 会进行地址捕获 遇到全局变量, 不会进行捕获

self本质

OC

@interface WYPerson : NSObject
@end
@implementation WYPerson
- (void)test {}
@end

C++

static void _I_WYPerson_test(WYPerson * self, SEL _cmd) {}

所以, self本质是个参数, objc_msgsend的时候,也会传递消息接收者SEL, 所以self也是局部变量, 所以block也会对self进行值捕获

block类型

因为block是有isa指针的, 所以必然是有类型的

一共有三种类型

全局block: __NSGlobalBlock__

堆block: __NSMallocBlock__

栈block: __NSStackBlock__

如何区分

如果没有捕获局部变量, 则这个block就是__NSGlobalBlock__, 存在数据段(全局区)

如果有捕获局部变量, 则这个block就是__NSStackBlock__ , 存在栈

__NSStackBlock__执行copy操作的返回值就是__NSMallocBlock__ , 存在堆

类型copy操作
__NSGlobalBlock__不进行任何操作
__NSMallocBlock__引用计数 + 1
__NSStackBlock__copy到堆上

arc环境下, 会自动对__NSStackBlock__ 进行copy操作, 比如:

  1. block作为返回值
  2. block被强指针引用
  3. gcd中使用的block

小tips, 可以通过在build settings中关闭arc

截屏2021-10-09 下午6.41.44.png

捕捉对象类型的变量时, block的内存结构变化

只有__main_block_desc_0会变化, 对应到源码上的话, 就是descriptor其指向的真实内存会包含Block_descriptor_2

捕捉对象类型的变量时

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->weakperson, (void*)src->weakperson, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->weakperson, 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};

捕捉普通类型的变量时

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)};

block在copy时,会调用__main_block_copy_0函数, 其中会针对每个捕获的对象,调用_Block_object_assign函数, 会根据捕捉的对象类型的成员的修饰类型(__weak, __strong), 来决定是否执行类似retain的操作

block在dealloc时,会调用__main_block_dispose_0函数, 其中会调用_Block_object_dispose函数, 会根据捕捉的对象类型的成员的修饰类型(__weak, __strong), 来决定是否执行类似release的操作

所以, 栈block的捕获的对象变量, 在copy变成堆block之前, 是不会对捕获的对象变量进行内存管理的(不进行类似retain, release的操作)

__block

__block修饰的变量, 会被转成一个特殊的结构体, 这个结构体中会有一个成员变量的值就是这个被修饰的变量, 看源码不太直观, 看clang rewrite的更加直观一点

源码

// Values for Block_byref->flags to describe __block variables
enum {
    // Byref refcount must use the same bits as Block_layout's refcount.
    // BLOCK_DEALLOCATING =      (0x0001),  // runtime
    // BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime

    BLOCK_BYREF_LAYOUT_MASK =       (0xf << 28), // compiler
    BLOCK_BYREF_LAYOUT_EXTENDED =   (  1 << 28), // compiler
    BLOCK_BYREF_LAYOUT_NON_OBJECT = (  2 << 28), // compiler
    BLOCK_BYREF_LAYOUT_STRONG =     (  3 << 28), // compiler
    BLOCK_BYREF_LAYOUT_WEAK =       (  4 << 28), // compiler
    BLOCK_BYREF_LAYOUT_UNRETAINED = (  5 << 28), // compiler

    BLOCK_BYREF_IS_GC =             (  1 << 27), // runtime

    BLOCK_BYREF_HAS_COPY_DISPOSE =  (  1 << 25), // compiler
    BLOCK_BYREF_NEEDS_FREE =        (  1 << 24), // runtime
};

struct Block_byref {
    void * __ptrauth_objc_isa_pointer isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};

struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep;
    BlockByrefDestroyFunction byref_destroy;
};

struct Block_byref_3 {
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
};

用clang rewrite

修饰普通变量时

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

修饰对象类型的变量时 (当对象变量用__weak修饰时, 结构体里WYPerson *__strong person;__strong会变成__weak)

struct __Block_byref_person_0 {
  void *__isa;
__Block_byref_person_0 *__forwarding;
 int __flags;
 int __size;
 // *******
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
  // *******
 WYPerson *__strong person; // __weak修饰时, __strong会变成__weak
};

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);
}

需要注意, 要捕捉用__block修饰的变量时, 不论变量是什么类型, block的成员变量__main_block_desc_0结构体中都会有copydispose的函数地址成员变量, 其中调用_Block_object_assign函数时,针对变量是否用__block修饰, 会传入不同的参数

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->person1, (void*)src->person1, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->person2, (void*)src->person2, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->person1, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->person2, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

还需注意的是, mrc时, 捕获的用__block修饰的成员变量, 在copy时, 不对该成员变量进行内存管理(retain,release)的操作

__block修饰的变量为什么这么设计, (person1->__forwarding->person1);

// oc
 MyBlock block = ^{
        person1;
    };

// c++
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_person1_0 *person1 = __cself->person1; // bound by ref

        (person1->__forwarding->person1);
        }
  1. __block修饰的变量,在编译时,会转换成一个结构体, 函数运行时, 该结构体初始化后, 存在栈上, 我们称它为A
  2. block初始化时, 会将A的地址作为参数传入构造方法中, block有一个指针类型的成员变量指向A的地址
  3. block进行copy时,会将block结构体的内容, copy到堆上, 于此同时, 会将A的内容也copy到堆上, 并将block指向A的指针的成员变量的值改成堆上A的地址
  4. 这时候有堆和栈上同时存储着A的内容, 这时候如果希望改动或访问时, 只访问堆上的内容,就需要改变栈上的A和堆上的A的forwarding指针, 都指向堆上的地址, 并且在访问时, 访问__forwording

ps: 所以,block的__Block_byref_person_0类型的成员变量的__forwarding指针,在block copy到堆上以前, 是指向栈上的地址, 在block copy到堆上以后, 会指到堆上的地址

因为希望任何地方的针对变量的改动都影响的是同一块内存,才这么设计

源码

static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // patch heap copy to point to itself
        // 这里是重点
        src->forwarding = copy;  // patch stack to point to heap copy 
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

为什么在block外加__weak, 在block中又加__strong

  1. __weak的作用是解决循环引用
  2. __strong的作用是一方面是为了避免在block执行过程中, 捕获的对象被释放, 另一方面是为了支持在block中使用指针的方法去访问捕获对象的成员变量

block也有方法实现, 如何获取对应的IMP和方法签名

源码中也有获取方法签名的函数, 跟下边的代码类似

// 获取block的方法实现
imp_implementationWithBlock

// 获取block方法签名
typedef NS_OPTIONS(int, BlockFlags) {
    BlockFlagsHasCopyDisposeHelpers = (1 << 25),
    BlockFlagsHasSignature          = (1 << 30)
};

typedef struct _Block {
    __unused Class isa;
    BlockFlags flags;
    __unused int reserved;
    void (__unused *invoke)(struct _Block *block, ...);
    struct {
        unsigned long int reserved;
        unsigned long int size;
        // requires BlockFlagsHasCopyDisposeHelpers
        void (*copy)(void *dst, const void *src);
        void (*dispose)(const void *);
        // requires BlockFlagsHasSignature
        const char *signature;
        const char *layout;
    } *descriptor;
    // imported variables
} *BlockRef;

static NSMethodSignature* typeSignatureForBlock(id block) {
    BlockRef layout = (__bridge void *)block;
    
    if (layout->flags & BlockFlagsHasSignature) {
        void *desc = layout->descriptor;
        desc += 2 * sizeof(unsigned long int);
        
        if (layout->flags & BlockFlagsHasCopyDisposeHelpers) {
            desc += 2 * sizeof(void *);
        }
        
        if (desc) {
            const char *signature = (*(const char **)desc);
            return [NSMethodSignature signatureWithObjCTypes:signature];
        }
    }
    
    return nil;
}

利用NSInvocation来调用block

  1. 设置参数要从第1位开始, 第0位系统会自动传block对象本身
  2. 利用invokeWithTarget: 传入block对象
  3. 如果不知道block的返回值和参数类型情况下, 可以利用block的NSMethodSignature来推断出
    int(^block)(int,int) = ^(int a, int b) {
        NSLog(@"%d  - %d", a, b);
        return a + b;
    };
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:typeSignatureForBlock(block)];
    int a = 5;
    int b = 10;
    [invocation setArgument:&a atIndex:1];
    [invocation setArgument:&b atIndex:2];
    [invocation invokeWithTarget:block];
    int res = 0;
    [invocation getReturnValue:&res];
    NSLog(@"%d", res);