Block究竟是什么?

887 阅读11分钟

Block的类型

Block在日常开发中常见的有三种类型
__NSGlobalBlock__(全局Block)
__NSStackBlock__(栈Block)
__NSMallocBlock__(堆Block)
我们可以测试一下

Block源码中我们可以看到还有_NSConcreteFinalizingBlock,_NSConcreteAutoBlock,_NSConcreteWeakBlockVariable类型的Block,但是我们开发中不会遇到,就不去讨论了

Block到底是个什么?

我们已经知道Block是分为三种类型,但是Block到底是个什么东西?
我们在main.m中写个最简单的Block

int main(int argc, const char * argv[]) {
	id block = ^{};
	return 0;
}

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m编译成C++代码,删掉无用代码后

main函数去掉各种强转后变成

int main(int argc, const char * argv[]) {
	id block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
	return 0;
}

在main中调用了__main_block_impl_0方法,并传入了__main_block_func_0&__main_block_desc_0_DATA两个参数从上面代码中可以看到__main_block_impl_0是一个结构体,而__main_block_impl_0()是同名的构造函数,而在这个函数中主要是给Block内部的成员赋值,也可以看到Block也含有一个isa指针,从先前的文章我们可以了解到所有包含isa指针的都是一个OC对象。然后我们看到__main_block_impl_0结构体中包含了__block_impl结构体:

struct __block_impl {
	void *isa;
	int Flags;
	int Reserved;
	void *FuncPtr;
};
  • isa 指针,指向类
  • Flags 标志变量,在实现block的内部操作时会用到
  • Reserved 保留变量
  • FuncPtr 函数指针,Block调用的就是这个函数指针
    还包含了__main_block_desc_0结构体
static struct __main_block_desc_0 {
	size_t reserved;
	size_t Block_size;
} 
  • reserved 保留变量
  • Block_size Block的大小

所以在我们声明一个Block的时候其实是生成了一个结构体,并将函数指针与__main_block_desc_0结构体一并传入
Block源码中我们可以看到Block的真正结构体

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};
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
};

与我们编译出来的C++代码除了部分成员名称不一样,剩下是完全一样的

ARC中Block的Copy

在以前MRC时代Block的赋值都需要使用[block copy],这是因为生成的Block(^{})是在栈上的,栈上的Block的释放不归我们管,所以需要使用copy将Block拷贝到堆上,这样可以由我们自己手动的去管理内存;ARC时代后编译器对我们进行了优化,声明一个Block内部使用了非静态变量并且将这个Block赋值给一个强指针时会自动的copy到堆上。
在在Block源码中的runtime.c文件中可以找到如下方法

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

在最下面的那个else里面可以看到栈上Block拷贝到堆上的操作,首先先以Block的大小创建一个新的Block结构体,然后将栈上Block的内容赋值给堆上的Block,接着修改flags,isa的值,所以,copy到堆上的Block除了静态区域的Block_descriptor_1void (*invoke)(void *, ...);指针,剩下的值都从栈copy 到堆

_Block_call_copy_helper方法是对Block中所持有的变量进行copy 操作的,暂且不表,下面会说

Block对变量的捕捉

Block捕获基础数据类型

我们都知道这么写是会报错的,那么究竟是为什么会报错?

同样,我们把代码进行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m转成C++,删除无用代码

因为在Block中直接修改i值会报错,所以讲Block中代码修改为NSlog

可以看到,这次编译出来的代码与上一次编译出来的有了一些不一样的地方

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

__main_block_impl_0结构体中多了个int i成员,同时,在main方法中

int main(int argc, const char * argv[]) {
	int i = 10;
	id block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, i));
	return 0;
}

是将i传入到Block中,传入的是个值类型,所以在我们block内部修改的时候会报错

Block捕获OC对象类型

那么Block内部使用的是对象类型会是什么样?
main编辑为

int main(int argc, const char * argv[]) {
	int i = 10;
	id objc = [NSObject new];
	id block = ^{ NSLog(@"%@",objc);};
	return 0;
}

编译后

这次编译的结果与前两次又有不一样的地方了,__main_block_desc_0结构体中多了两个函数指针

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 void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
	_Block_object_assign( (void*)&dst->objc, (void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{
_Block_object_dispose((void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

上面我们已经说了,ARC环境下会将栈上的Block拷贝到堆上,但是我们有个方法还没说

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    return (struct Block_descriptor_2 *)desc;
}

这个方法是从Block中根据偏移获取Block_descriptor_2结构体

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

调用(*desc->copy)(result, aBlock);则是可以在编译的C++代码中分析到正是__main_block_copy_0方法,传入的参数第一个是堆上的Block,第二份参数是栈上的Block,而__main_block_copy_0方法调用了_Block_object_assign方法

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/
        *dest = _Block_byref_copy(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/
        *dest = object;
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/
        *dest = object;
        break;
      default:
        break;
    }
}

__main_block_copy_0_Block_object_assign( (void*)&dst->objc, (void*)src->objc, 3/*BLOCK_FIELD_IS_OBJECT*/);传入的枚举值是BLOCK_FIELD_IS_OBJECT,所以调用的是_Block_retain_object(object);方法

void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
    _Block_retain_object = callbacks->retain;
    _Block_release_object = callbacks->release;
    _Block_destructInstance = callbacks->destructInstance;
}

_Block_retain_object则是retain指针,继续找,在libdispatch源码中中可以看到

void
_os_object_init(void)
{
	_objc_init();
	Block_callbacks_RR callbacks = {
		sizeof(Block_callbacks_RR),
		(void (*)(const void *))&objc_retain,
		(void (*)(const void *))&objc_release,
		(void (*)(const void *))&_os_objc_destructInstance
	};
	_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
	const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
	_os_object_debug_missing_pools = v && !strcmp(v, "YES");
#endif
}
void*
os_retain(void *obj)
{
	return objc_retain(obj);
}

最终还是调用的是objc4源码中的objc_retain方法,所以Block捕获的是OC对象时,只是对这个对象进行了retain操作

_os_object_init方法是在_objc_init之前调用的

在block销毁的时候会调用_Block_object_dispose进行销毁操作,这里会对应的做出release操作

Block捕获__block修饰的基础数据类型

我们都知道如果在Block中想要修改一个局部变量的值需要使用__block去修饰,那么为什么使用__block修饰后就可以修改了?我们写如下代码

int main(int argc, const char * argv[]) {
	__block	int i = 10;
	id 	block = ^{
		i = 100;
		NSLog(@"%d",i);
	};
	return 0;
}

编译成C++

这次编译的结果与前面依然不太一样(废话,一样的话还分析干嘛~),编译结果多了个__Block_byref_i_0结构体

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

我们用__block修饰的int i是它的一个成员,所以我们使用__block修饰一个变量后会被编译器自动的编译成一个结构体,因为成员变成了一个结构体,所以修改结构体中一个成员是不会有问题的。
__Block_byref_i_0结构体中有一个__Block_byref_i_0 *__forwarding;指针,在main函数里面设置这个指针指向自身。同样__main_block_desc_0包含了copydispose方法,但是传入的枚举值是BLOCK_FIELD_IS_BYREF,在_Block_object_assign方法中对应

*dest = _Block_byref_copy(object);
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
        //根据栈上的Block_byref大小创建个堆上的Block_byref结构体
        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;
}

代码中有注释,如果src指针在栈上时走if((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0)分支,可以从这段代码上看出来在Block从栈上拷贝到堆上时会在堆上创建一个Block_byref结构体,并将栈上的forwarding与堆上新创建的forwarding都指向堆上的Block_byref,所以在Block里面改变了__block修饰的变量的值后外部也会跟着一块被修改。相对应的_Block_object_dispose方法会调用_Block_byref_release方法将堆上的Block_byref释放
还有个(*src2->byref_keep)(copy, src);,这个我们下面来分析

Block捕获__block修饰的OC对象类型

不多说,先编写一段代码在转成C++

int main(int argc, const char * argv[]) {
	__block	NSObject *object = [NSObject new];
	id 	block = ^{
		object = [NSObject new];
		NSLog(@"%@",object);
	};
	return 0;
}

因为代码过长,不易截图,我就贴上与前面不一样的部分

struct __Block_byref_object_0 {
	void *__isa;//8
	__Block_byref_object_0 *__forwarding;//8
	int __flags;//4
	int __size;//4
	void (*__Block_byref_id_object_copy)(void*, void*);//8
	void (*__Block_byref_id_object_dispose)(void*);//8
	NSObject *object;
};
int main(int argc, const char * argv[]) {
	__attribute__((__blocks__(byref))) __Block_byref_object_0 object = {
		(void*)0,
		(__Block_byref_object_0 *)&object,
		33554432,
		sizeof(__Block_byref_object_0),
		__Block_byref_id_object_copy_131,
		__Block_byref_id_object_dispose_131,
		((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))
	};
	id block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_object_0 *)&object, 570425344));
	return 0;
}
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_byref_object_0结构体中多了两个函数指针__Block_byref_id_object_copy__Block_byref_id_object_dispose。上面说到还有个_Block_byref_copy方法中有个(*src2->byref_keep)(copy, src);没有去分析,现在我们就来看看,首先使用struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);获取到Block_byref_2结构体

struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
    void (*byref_destroy)(struct Block_byref *);
};

所以byref_keep正是我们传入的__Block_byref_id_object_copy_131方法,这个方法中调用了我们熟悉的_Block_object_assign方法,不过不一样的是这次编码出来的参数看着比较诡异,我们一点点的看
(char*)dst + 40首先,我们可以看到dst是传出的栈上的Block_byref结构体指针地址,而我们__Block_byref_object_0前面固定的成员所占空间刚好是40,所以(char*)dst + 40是拿到NSObject *object指针;相对的*(void * *) ((char*)src + 40)是拿到栈上的NSObject *object指针,但是这个前面多了个*所以这是取出这个指针所指向的值; 前面两个分析清楚了,那么131是什么?查看枚举值发现BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT(3 | 128)的值刚好就是131,我们接着去_Block_object_assign方法中去查看

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;

当传入的值是131时,是把栈上的NSObject *object的值赋值给堆上的NSObject *object

当然_Block_object_assign方法中并不简单的只有这四种情况,后面的其他情况就不在去分析了,有兴趣的朋友可以去源码中分析

循环引用

看完上面的朋友们,想必不用我说都知道为什么会有循环引用了,当一个对象持有block,而且这个block也持有这个对象时就会造成循环引用,那么怎么解除循环引用我们也很清楚只要使用__weak就可以解决了,不过现在还有种比较神奇的解决方案,使用__block

	__block Person * block_self = self;
		self.block = ^{
			NSLog(@"%@",block_self);
			block_self = nil;
		};
		self.block();

一定要调用这个block,不然block_self不会被置为nil,还是没有打破循环引用

最后一提

Masonry的Block为什么可以在里面直接使用self?

	[self.tableview mas_makeConstraints:^(MASConstraintMaker *make) {
		make.centerX.equalTo(self.view);
		make.bottom.mas_equalTo(-30.f);
	}];

点进源码中可以看到

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

self并没有持有或间接持有block,所以不会造成循环引用,同理[UIView animateWithDuration:animations:]可以在内部使用block是一样的道理

疑惑

本来把block看完了,以为对block已经算是有些了解了,但是看到孙源大神在微博上发的题发现自己还是太天真了

第一题算是能搞定

struct YTY_Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};
struct YTY_Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
};
void hookInvokePrintHelloWorld(void *obj, ...){
    printf("Hello,world\n");
}
void HookBlockToPrintHelloWorld(id block){
    struct YTY_Block_layout *yty_block = (__bridge struct YTY_Block_layout *)block;
    yty_block->invoke = &hookInvokePrintHelloWorld;
	((__bridge struct YTY_Block_layout *)block)->invoke = &hookInvokePrintHelloWorld;
}

但是第二题,第三题实在搞不定,尝试过使用method swizzling去替换NSBlock中的- (void)invoke方法,方法能替换但是Block不会调用。不知道有没有大神能帮忙解答下这几个问题


文章参考:
破弓的Block专题
Block源码