iOS的OC的block底层原理(面试来复习下底层)

链接:https://juejin.im/post/6890071440998498311

前言

在iOS开发中,相信大家在开发中很频繁使用block,使用block来作为参数,属性,回调等等。虽然知道怎么使用block,但是block在底层的原理是怎样的,应该还是有的人不是很清楚的,这篇文章就是主要介绍block的底层原理的。

1. Block的基础

Block是一个OC的对象,它封装了一段代码,这段代码可以在任何时候执行。Block可以作为函数参数或者函数的返回值,而其本身又可以带输入参数或返回值。可以嵌套定义,可以定义在方法内部和外部。

1.1 Block种类

在实际使用的Block种根据内存情况,可以将其分为3种

  • _NSConcreteGlobalBlock :全局Block。
  • _NSConcreteMallocBlock:堆Block。
  • _NSConcreteStackBlock:栈Block(copy之前的)。

通过一个小的demo可以分别打印出来

 void (^GlobalBlock)(void) = ^{
        NSLog(@"执行GlobalBlock");
    };
    int a = 10;
    void (^MallocBlock)(void) = ^{
        NSLog(@"执行MallocBlock- %d",a);
    };
    GlobalBlock();
    MallocBlock();
    NSLog(@"我是全局block--%@",GlobalBlock);
    NSLog(@"我是堆block--%@",MallocBlock);
    NSLog(@"我是栈%@",^{
        NSLog(@"执行StackBlock--%d",a);
    });

//打印结果===================
执行GlobalBlock
执行MallocBlock- 10
我是全局block--<__NSGlobalBlock__: 0x10c443090>
我是堆block--<__NSMallocBlock__: 0x600003b937b0>
我是栈<__NSStackBlock__: 0x7ffee37bb458>

但是Block的种类有6种,另外3种是系统级别的很少用到。Block的种类如下:

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

1.2 Block循环引用

在使用Block的时候,最容易遇到的就是循环引用这种错误了。

@property (nonatomic, copy) NSString *name;
@property(nonatomic,copy) void(^testBlock)(void);

    self.name = @"jason";
    self.testBlock = ^{
        NSLog(@"%@",self.name);
    };
    self.testBlock();

其实在Xcode上写这段代码的时候也会直接报

Capturing 'self' strongly in this block is likely to lead to a retain cycle

为什么会循环引用呢?因为self持有了block,block持有了self(self.name),就形成了self->block->self这样的闭环,然后导致循环引用。一般情况下,为了避免引起循环引用会加一个__weak来修饰。

    self.name = @"jason";
    __weak typeof(self) weakSelf = self;
    self.testBlock = ^{
        NSLog(@"%@",weakSelf.name);
    };
    self.testBlock();

此时的weakSelf是在一张弱引用表里面的,此时的持有状态是weakSelf持有了self,self持有了block,block持有了weakSelf,这时候看上去是不是还是一个闭环?但是weakSelf持有self的时候引用次数并没有处理的就是数量没有增加的,所以此时正常还是会执行到析构函数(delloc)。只是指向了self而已。如果在testBlock加一个延迟的然后再打印,此时再打印出来的是weakSelf.name是一个nil

    self.name = @"jason";
    __weak typeof(self) weakSelf = self;
    self.testBlock = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.name);
        });
    };
    self.testBlock();

因为在延迟的过程中,selfdelloc之后会被回收,此时的weakSelf就会被设置为nil了。为了防止这种情况发生,可以加一个__strong的修饰

       __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });

此时相当于一个临时的strong来持有了weakSelf,在strong还没被销毁的情况下weakSelf也不会销毁,只有打印完之后才会销毁。

2.Block的本质

为了方便接下来的介绍,创建一个block.c文件,通过clang来查看block的底层源码分析,其中block.c的源码如下

#include "stdio.h"
int main(){
    void(^block)(void) = ^{
        printf("TestJason");
    };
    block();
    return 0;
}

在该文件的目录下,用终端输入命令行,就会在该目录下生成一个block.cpp的文件

clang -rewrite-objc block.c -o block.cpp 

通过block.cpp文件可以看到部分的源码

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("TestJason");
    }

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

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

由源码可以知道__main_block_impl_0是一个结构体,也就是一个对象。其中在__main_block_impl_0这个构造函数中传进去的__main_block_func_0是一个函数并且是保存在impl.FuncPtr里面的。在main里面的block->FuncPtr函数调用其实就是调用__main_block_func_0这个函数,所以block在声明之后是需要调用才可以实现的。

2.1 Block捕获外部变量

还是用源码的代码,但是加多了一个变量int a,用clang重新来生成cpp文件

int main(){
    int a = 10;
    void(^block)(void) = ^{
        printf("TestJason---%d",a);
    };
    block();
    return 0;
}

此时生成的文件里面的源码与上面的有点不一样了,可以看一下红色圈出来的部分,此时的__main_block_impl_0的构造函数变了,并且自动生成了一个int a这个属性来捕获外界的变量。

image

从源码可以看到,在__main_block_func_0函数中传进来的__cself其实就是main函数里面生命的block,所以__main_block_func_0函数的int a = __cself->a的值是10,这时候其实就是一个值拷贝,此时的int a__cself->a不是同一个a,只是指向同一个值而已。所以如果在block中对a值进行修改(加或者减),其实就是对里面定义的int a进行修改,并不会对main函数里面的a有修改的。

2.2 Block修改外部变量

为了修改外部变量就在mian函数中添加__block,源码如下

int main(){
    __block int a = 10;
    void(^block)(void) = ^{
        a++;
        printf("TestJason---%d",a);
    };

    block();
    return 0;
}

还是通过clang来生成block.cpp的文件来看底层源码

image

image

从源码可以看到此时的a已经是一个__Block_byref_a_0的结构体,并且在main函数中初始化赋值,并且传入__main_block_func_0函数的值中是__Block_byref_a_0这个结构体的指针,里面的定义的a__Block_byref_a_0 *a = __cself->a;,此时这是指针的拷贝,在对a进行修改的时候是可以修改得到main函数的a的。所以加了__block修饰,生成了相应的结构体保存原始的指针和值,并且传递了一个指针地址给执行的函数。

2.3 Block的内存变化

通过上面的介绍可以知道,Block有栈block,堆block,但是block如何从栈block转变为堆block的呢?这个是接下来要介绍的。在ViewController中实现如下代码并开启汇编的模式。

- (void)viewDidLoad {
    [super viewDidLoad];
    int a = 10;

    void (^block1)(void) = ^{
        NSLog(@"JS_Block===%d",a);
    };
    block1();
}

进入到如下的界面,并且为objc_retainBlock打一个符号断点,进入到里面去。

image

通过查看寄存器可以看到,此时的block还是一个栈block
image

_Block_copy打一个符号断点,进入到里面去,在返回的时候打一个断点,此时可以通过查看寄存器知道,block变为了堆block。

image

由图可以知道栈block是通过_Block_copy这个函数变为堆block的。

3.Block签名

在编译出来的clang文件中

// Runtime copy/destroy helper functions (from Block_private.h)
#ifdef __OBJC_EXPORT_BLOCKS
extern "C" __declspec(dllexport) void _Block_object_assign(void *, const void *, const int);
extern "C" __declspec(dllexport) void _Block_object_dispose(const void *, const int);
extern "C" __declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" __declspec(dllexport) void *_NSConcreteStackBlock[32];
#else
__OBJC_RW_DLLIMPORT void _Block_object_assign(void *, const void *, const int);
__OBJC_RW_DLLIMPORT void _Block_object_dispose(const void *, const int);
__OBJC_RW_DLLIMPORT void *_NSConcreteGlobalBlock[32];
__OBJC_RW_DLLIMPORT void *_NSConcreteStackBlock[32];

从注释中可以知道,block的底层源码是从Block_private.h这个文件来的,而这个文件是在libclosure源码中的。

首先来看block结构体对象Block_layout源码,这个相当于clang出来的__main_block_impl_0结构体

#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
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

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

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor; //
    // imported variables
};

其中Block_layoutflags是记录状态的,BlockInvokeFunction invoke是调用的函数,在Block_layout中,Block_descriptor_2Block_descriptor_3以结构体的形式存在是可选的。为什么说这两个是可选的呢?由下面的源码可以说明

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

static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}

_Block_descriptor_2的值先用flags & BLOCK_HAS_COPY_DISPOSE,不存在就返回NULL,如果有就进行内存偏移,先通过aBlock->descriptor的得到BLOCK_DESCRIPTOR_1,然后内存偏移得到Block_descriptor_2。而Block_descriptor_3的值先用flags & BLOCK_HAS_SIGNATURE不存在就返回NULL,如果有先判断_Block_descriptor_2是否存在,并且也是通过内存偏移来得到的,其中签名是在Block_descriptor_3中的。其中flags的状态如下

// 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
};
  • 第一位:释放标记,一般常用BLOCK_NEEDS_FREE走位与操作一同传入flags,告知该block可释放。
  • 第16位:存储引用计数的值,是一个可选参数。
  • 第24位:第16位是否有效的标记,程序根据它来是否增加或者减少引用计数位的值。
  • 第25位:是否拥有拷贝辅助函数。
  • 第26位:是否拥有block的析构函数。
  • 第27位:标记是否有垃圾回收。
  • 第28位:标记是否是全局block。
  • 第29位:与BLOCK_USE_START相对,判断当前block是否拥有一个签名,用于runtime时动态调用。
  • 第30位:是否有签名
  • 第31位:是否有扩展决定Block_descriptor_3

通过查看寄存器
image

block的签名是在signature值,其中签名是@?

4.Block的3次copy

从第二部分的内容,可以知道,block从栈block变为堆block是通过了_Block_copy函数操作完成的,以下就是该函数的源码:

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
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 =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // 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;
    }
}

从源码中可以看到ifelse if的条件判断分别是引用计数全局block的判断,如果是都直接返回block出去。else的条件判断就是栈blockcopy操作。通过aBlock开启一个新的result空间,并且将aBlock的内容等平移到result中去。这时候就实现了由栈变成堆。但是具体是怎么做到从栈block到堆block的还是不是很清楚,这里面是不是隐藏是一些细节呢?

4.1clang查看源码

mian.m文件中写下如下代码:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        __block NSString *js_name = [NSString stringWithFormat:@"jason"];
        void (^block1)(void) = ^{ // block_copy
            js_name = @"js_jason";
            NSLog(@"JS_Block - %@",js_name);
        };
        block1();
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

通过终端clang命令:xcrun -sdk iphonesimulator clang -rewrite-objc main.m 会生成一个mian.cpp的文件,从这个文件中查看源码,从中可以看到js_name会生成一个__Block_byref_js_name_0这个类型,在源文件中可以看到这个类型的结构:

struct __Block_byref_js_name_0 {
  void *__isa;
__Block_byref_js_name_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSString *js_name;
};

main.cpp文件可以看到有两个函数

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

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

这就验证了Block_descriptor_2中分别有copy和dispose函数,其中__main_block_copy_0还调用了_Block_object_assign函数。通过源码可以找到这个函数的调用

// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
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;
    }
}

这个段代码的就是当block或者Block_byrefs持有对象,就会通过这段代码来进行分配复制,从上面的代码可以知道在执行copy函数调用_Block_object_assign会将js_name的对象传进来。并且switch里面的枚举如下

// Runtime support functions used by compiler when generating copy/dispose helpers

// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
    // see function implementation for a more complete description of these fields and combinations
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ... 对象
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable block变量
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable __block修饰的结构体
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers __weak修饰的变量
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines. 处理Block_byref内部对象内存的时候会加的一个额外标记,配合上面的枚举使用
};

因为当前是__block修饰的就会执行到_Block_byref_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;
}

这段代码大概的意思是将传进去的结构体重新创建一个,并且赋值相同的内存大小通过

        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
复制代码

指向相同的地址,这就使得__block有了修改的能力。最后通过满足的条件会执行到byref_keep函数,而这个函数恰恰是之前clang出来的__Block_byref_js_name_0结构体里面的__Block_byref_js_name_0函数

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

此时会再次执行_Block_object_assign函数,为什么会加上40呢?可以从上面得到的__Block_byref_js_name_0结构体的内存偏移可以知道,40是直接找到NSString js_name值。

struct __Block_byref_js_name_0 {
  void *__isa;
__Block_byref_js_name_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSString *js_name;
};

此时就是单纯的对象的内存copy了,至此block的三层copy就完成了。这里就是先通过_Block_copy进行一层copy,然后通过__main_block_copy_0函数执行_Block_object_assign函数,此时进去的是__block的状态进去的,执行到_Block_byref_copy函数进行第二层的copy。之后会执行(*src2->byref_keep)(copy, src);就是生成的__Block_byref_id_object_copy_131函数,再次执行 _Block_object_assign函数,此时进去就是通过内存偏移找到js_name的对象进去进行copy的。

5.最后

这篇文章介绍了block的基础,通过底层源码来分析了block的内存变化,由全局block变为栈block,再到堆block的过程。找到了block的签名是@?,从底层分析了block通过三层copy可以用__block对变量值的修改。有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:891488181 不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

文章到这里就结束了,你也可以私信我及时获取面试相关资料。如果你有什么意见和建议欢迎给我留言。

你可能感兴趣的:(iOS的OC的block底层原理(面试来复习下底层))