iOS基础:Block底层实现及理解

本文用于记录近期学习block底层后的理解。
本文的参考博文:
Block技巧与底层解析
谈Objective-C block的实现

一、Block编译转换 OC->C++

通过使用命令clang -rewrite-objc实现。
1.首先,新建一个main.m文件。
2.打开终端,cd到main.m文件所在目录。
3.输入 clang -rewrite-objc main.m命令进行转换。
4.最后main.m文件所在的目录下,新生成一个main.cpp文件。

二、Block类型

1. NSConcreteGlobalBlock

以下两种情况下,block为NSConcreteGlobalBlock
a.记述全局变量的地方有block语法时。
b.block语法的表达式中不使用任何外部变量时。

更新(11.03 9:00):

这里有个快速判断的方法。如果Block的body里使用到了外部的非全局变量和非static静态变量,那么这个Block就会在栈上创建即_NSConcreteStackBlock。反之如果没有引用变量或者仅引用了全局变量或者static静态变量则是全局Block_NSConcreteGlobalBlock
来自:漫谈Block

举例1:

#include <stdio.h>

void (^globalBlock)() = ^{
    
};

int main()
{
    globalBlock();
    return 0;
}

转换后的C++代码:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __globalBlock_block_impl_0 {
  struct __block_impl impl;
  struct __globalBlock_block_desc_0* Desc;
  __globalBlock_block_impl_0(void *fp, struct __globalBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结构体__block_implisa指针指向NSConcreteGlobalBlock

举例2:
我尝试了在main函数中创建一个block,并且block不去截获变量,但是通过clang转换,发现isa指针指向的却是NSConcreteStackBlock。这一点很奇怪。
我在《oc高级编程》中看到这样一句话:

即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。

2. NSConcreteStackBlock

保存在栈中的 block,当函数返回时会被销毁。

#include <stdio.h>
int main() {
    int a = 100;
    void (^block2)(void) = ^{
        a;
    };
    return 0;
}

转换后的C++代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结构体__block_implisa指针指向NSConcreteStackBlock

3. NSConcreteMallocBlock

保存在堆中的 block,当引用计数为 0 时会被销毁。
但是NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,当[block copy]的时候,会被复制到堆中。
(以下代码来自:谈Objective-C block的实现)

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
    // 1
    if (!arg) return NULL;
    // 2
    aBlock = (struct Block_layout *)arg;
    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;
    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;
    // 8
    result->isa = _NSConcreteMallocBlock;
    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }
    return result;
}

当block被copy的时候,会调用_Block_copy_internal方法,在内部result->isa = _NSConcreteMallocBlock;

更新(11.03 9:45):

感谢 啊哈呵 ,在他给的源码中找到以下代码:

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

可以很显然的看到是通过结构体中的flags来判断是否copy。

下面举一个copy的例子,在ARC下,当Block作为函数返回值时也会拷贝成为NSConcreteMallocBlock 类型,本质是调用copy方法。
如下代码:

typedef void (^Block)();

Block getBlock() {
  char c = 'YQ';
  void (^block)() = ^{
    printf("%c", e);
  };
  return block;
}

void main {
  Block block = getBlock();
  block();
}

当在ARC环境下,能正常运行不会奔溃,是因为系统会在创建的时候调用objc_retainBlock方法,而objc_retainBlock方法实际上就是Block_copy方法。(来自runtime/objc-arr.mm)
因此本质上,以上代码的系统实现流程就变成了:在栈上创建block结构体对象,然后再通过Block_copy复制到堆上,然后把堆上的对象注册到自动释放池中,同时返回这个堆上的对象。
但是在MRC环境下,就要奔溃了,因为系统不会自动拷贝。所以需要手动拷贝:[block copy]

三、Block的拷贝

�三种类型的Block拷贝:
NSConcreteGlobalBlock拷贝后,什么也不做。
NSConcreteStackBlock拷贝后,从栈复制到堆中。
NSConcreteMallocBlock拷贝后,引用计数加一。

四、__block变量的拷贝

int main()
{
    __block int i = 0;
    void (^block)(void) = ^{
        i = 1;
    };
    return 0;
}
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

        (i->__forwarding->i) = 1;
    }
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()
{
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
    return 0;
}

当使用__block修饰符时,�基本数据类型 i 被转换成了__Block_byref_i_0结构体。__Block_byref_i_0结构体中带有 isa指针,说明它也是一个对象。
当block修改变量时,会调用下面代码:

 __Block_byref_i_0 *i = __cself->i; // bound by ref
(i->__forwarding->i) = 1;

发现绕来绕去,下面理一下。
最让人无法理解的是__forwarding指针。__forwarding指针始终指向自己。
__block int i在栈中的时候,__forwarding指向的是栈中的自己。
当 i 拷贝到堆中时候,__forwarding指向的是堆中的自己。

正如《oc高级编程》所说:

__block修饰变量用结构体成员变量__forwarding可以实现无论 __block变量配置在栈上还是堆上都能够正确的访问 __block变量。

五、Block拷贝对__block变量的影响

影响

如果Block使用了__block变量,当Block从栈拷贝到堆中,
a.栈中的__block变量会拷贝到堆中并被Block持有。
b.堆中的__block变量被Block持有。

这个和OC的引用计数内存管理相同。
下面有BlockA、BlockB 、 __block a、 __block b。
如果BlockA使用了栈上的a和b,当[BlockA copy]拷贝到堆上,a,b也会同时拷贝到堆上,并且堆上的BlockA持有堆上的a,b。
同理,当BlockA,BlockB都使用了栈上的a,当[BlockA copy],[BlockB copy]拷贝到堆上,BlockA和BlockB会同时持有堆上的a。

现在再回过头去看第四点的__block int i,就能理解为什么要有__forwarding指针了。
现有以下代码:

int main()
{
    __block int i = 0;
    void (^block)(void) = [^{
        ++i;
    } copy];
    ++i;
    block();
    printf("%d", i);
    return 0;
}

把上面代码转换为C++后:

// block中的++i实现
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);
    }
// main函数中的++i
++(i.__forwarding->i);

可以发现都是++(i.__forwarding->i);,也就是说指向的都是堆中的i。
因此输出为2。

六、一些题目,判断ARC和MRC环境下能否运行

1 都能运行

void exampleA() {
  char a = 'A';
  ^{
    printf("%cn", a);
  }();
}

2 ARC能运行

void exampleB_addBlockToArray(NSMutableArray *array) {
  char b = 'B';
  [array addObject:^{
    printf("%cn", b);
  }];
}

void exampleB() {
  NSMutableArray *array = [NSMutableArray array];
  exampleB_addBlockToArray(array);
  void (^block)() = [array objectAtIndex:0];
  block();
}

3 都能运行

void exampleC_addBlockToArray(NSMutableArray *array) {
  [array addObject:^{
    printf("Cn");
  }];
}

void exampleC() {
  NSMutableArray *array = [NSMutableArray array];
  exampleC_addBlockToArray(array);
  void (^block)() = [array objectAtIndex:0];
  block();
}

4 ARC能运行

typedef void (^dBlock)();

dBlock exampleD_getBlock() {
  char d = 'D';
  return ^{
    printf("%cn", d);
  };
}

void exampleD() {
  exampleD_getBlock()();
}

5 ARC能运行

typedef void (^eBlock)();

eBlock exampleE_getBlock() {
  char e = 'E';
  void (^block)() = ^{
    printf("%cn", e);
  };
  return block;
}

void exampleE() {
  eBlock block = exampleE_getBlock();
  block();
}

七、更新(11.03 12:00)block对各种变量的处理

在看了漫谈Block后半部分后,很想把这一部分整理一下。
首先!如果捕获的变量为id, NSObject, __attribute__((NSObject)), block类型变量和__block修饰的变量都会调用_Block_object_assign方法。我就把_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:
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break;
      
      ...
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      ...
      default:
        break;
    }
}

1.变量为基本数据类型 无__block

#include <stdio.h>
int main() {
    int a = 100;
    void (^block2)(void) = ^{
        a;
    };
    return 0;
}

只会在block的结构体内部中添加一个int a变量。不能做修改。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

2.变量为基本数据类型 有__block

int main()
{
    __block int i = 0;
    void (^block)(void) = ^{
        i = 1;
    };
    return 0;
}

有__block修饰的基本数据类型会转换成__Block_byref_i_0结构体。

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

然后当他被拷贝后,会进入_Block_object_assign方法的BLOCK_FIELD_IS_BYREFcase

      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
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;

            ...

            (*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));
        }
    }
    ...
    return src->forwarding;
}

通过代码发现,在堆上创建了一个copy对象。然后通过

        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy

让其始终指向堆上的自己。

3.变量为对象 无__block

因此会进入到BLOCK_FIELD_IS_OBJECT

case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;
static void _Block_retain_object_default(const void *ptr __unused) { }
// 默认_Block_retain_object被赋值为_Block_retain_object_default,即什么都不做
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;

// Called from CF to indicate MRR. Newer version uses a versioned structure, so we can add more functions
// without defining a new entry point.
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被赋值为_Block_retain_object_default,即什么都不做。也就是说,在ARC环境下,Block不会在这里持有对象。(ARC环境有了更完善的内存管理,如果外部变量由__strong、copy、strong修饰时,Block会把捕获的变量用__strong来修饰进而达到持有的目的。)在MRR环境下,Block会通过_Block_retain_object方法持有id, NSObject, __attribute__((NSObject))类型变量。

4.变量为对象 有__block

因此会进入到BLOCK_FIELD_IS_OBJECT

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

直接赋值。
所以通过__block修饰可以避免调用到_Block_retain_object方法,也就是在MRR环境下我们可以通过__block来避免Block强持有变量,进而避免循环引用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容