iOS底层学习笔记-Block

251 阅读11分钟

结论

Block本质上也是一个OC对象

Block是封装了函数调用以及函数调用环境的OC对象

1、Block的本质认识

1.1、Block是什么

先写一个简单的block代码(macOS程序)如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 18;
        
        void(^block)(int, int) = ^(int a, int b) {
            NSLog(@"%i", age);
            NSLog(@"%i", a);
            NSLog(@"%i", b);
        };
        
        block(10, 20);
    }
    return 0;

利用clang编译以上代码,命令如下:

`xcrun - sdk iphoneos clang -arch arm64 -rewrite-objc main.m`

查看编译出的main.cpp文件

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int age = 18;

        void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

        struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;

        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);
    }
    return 0;
}

通过C++代码分析,block被定义为了__main_block_impl_0 类型的结构体:

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

其中impl的定义为如下结构体:

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

如果将impl的定义直接放入到__main_block_impl_0结构体中,我们可以发现其第一个变量即是isa指针(OC对象isa也是第一变量),因此block在本质上讲在底层实现上来看其就是一个OC对象。

1.2、Block布局结构

查看main.cpp中block的定义以及实现代码:

  void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

以及__main_block_func_0的内容:

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_w5_2p585ycs4vsfdrh02hdfjwtw0000gn_T_main_a96687_mi_0, age);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_w5_2p585ycs4vsfdrh02hdfjwtw0000gn_T_main_a96687_mi_1, a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_w5_2p585ycs4vsfdrh02hdfjwtw0000gn_T_main_a96687_mi_2, b);
        }

以上可以分析出:

__main_block_func_0函数作为参数传递给了__main_block_impl_0函数, 既fp函数指针,而fp函数指针又赋值给了__main_block_impl_0中的impl的FuncPtr函数指针

 ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);

而block的调用就是找到block对象的FuncPtr,然后调用

分析FuncPtr可知:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  NSString *str;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, NSString *_str, int flags=0) : age(_age), str(_str) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

因impl为__main_block_impl_0首元素,通过block->FuncPtr可直接找到__main_block_func_0函数

1.3 如何验证block为__main_block_impl_0 结构体?

我们按照上述分析,定义出三个结构体,并在main.m中增加一行对block的引用

struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

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;
    NSString *str;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 18;
        
        void(^block)(int, int) = ^(int a, int b) {
            NSLog(@"%i", age);
            NSLog(@"%i", a);
            NSLog(@"%i", b);
        };
        
        struct __main_block_impl_0* blockStruct = (__bridge struct __main_block_impl_0*)block;
        
        
        
        block(10, 20);
    }
    return 0;
}turn 0;
}

分别在block中和blockStruct加断点

可以看到age变量会被block捕获

打开汇编断点

继续执行断点

对比发现blockStruct中的FuncPtr和block断点到的函数起始地址的值是一致的。

2、Block对变量的捕获

变量类型 是否捕获到block内部 变量访问方式
局部变量(auto) ✔️ 值传递
局部变量(static) ✔️ 指针传递
全局变量 直接访问

2.1、局部变量--auto变量

有如下代码:

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

查看block对应代码:

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

通过 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) 可以分析出,__main_block_impl_0结构体内部声明了一个age 来接收_age的值,即block对于age的捕获是值捕获。所以block外部age的修改,并不影响block内部age的值,所以执行block后的age值仍然是18。

2.2、局部变量--static变量

void(^block)(void);

void testBlock(){
    auto int age = 18;
    static int weight = 100;
    
    block = ^{
        NSLog(@"age=%i weight=%i", age, weight);
    };
    
    age = 20;
    weight = 120;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        testBlock();
        block();
    }
    return 0;
}

查看block对应代码:

    struct __testBlock_block_impl_0 {
        struct __block_impl impl;
        struct __testBlock_block_desc_0* Desc;
        int age;
        int *weight;
        __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
         impl.isa = &_NSConcreteStackBlock;
         impl.Flags = flags;
         impl.FuncPtr = fp;
         Desc = desc;
    }
};

可以看到block对于static类型变量的捕获方式为地址捕获

思考:auto 与static 声明的变量,为什么捕获方式不同?

  • auto变量生命周期小于block生命周期,所以block需要捕获变量值
  • static变量生命周期大于等于block生命周期,所以block只需要捕获变量指针(地址)

2.3、全局变量

全局变量,不需要捕获,直接访问。

如下测试代码:

int height = 88;

void(^block)(void);

void testBlock(){
    block = ^{
        NSLog(@"%i", height);
    };
    
    height = 180;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        testBlock();
        block();
    }
    return 0;
}

.cpp查看

static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_w5_2p585ycs4vsfdrh02hdfjwtw0000gn_T_main_8e351d_mi_0, height);
    }

可以看到对于全局变量,block会直接访问该变量。

2.4 self的捕获

创建一个类,加上如下方法:

#import "TestObject.h"

@implementation TestObject

void(^block)(void);

- (void)testMethod
{
    block = ^{
        NSLog(@"%@", self);
    };
}

@end

查看.cpp文件

static void _I_TestObject_testMethod(TestObject * self, SEL _cmd) {
    block = ((void (*)())&__TestObject__testMethod_block_impl_0((void *)__TestObject__testMethod_block_func_0, &__TestObject__testMethod_block_desc_0_DATA, self, 570425344));
}

static void __TestObject__testMethod_block_func_0(struct __TestObject__testMethod_block_impl_0 *__cself) {
  TestObject *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_w5_2p585ycs4vsfdrh02hdfjwtw0000gn_T_TestObject_8441b2_mi_0, self);
    }

可以看到调用testMethod方法的时候,self作为参数(局部变量)传递给方法使用,block会捕获self。

2.5 对象成员变量的捕获

#import <Foundation/Foundation.h>

@interface TestObject : NSObject

@property (nonatomic, assign) NSInteger age;

@end

#import "TestObject.h"

@implementation TestObject

void(^block)(void);

- (void)testMethod
{
    block = ^{
        NSLog(@"%d", _age);
    };
}

@end

查看.cpp

static void _I_TestObject_testMethod(TestObject * self, SEL _cmd) {
    block = ((void (*)())&__TestObject__testMethod_block_impl_0((void *)__TestObject__testMethod_block_func_0, &__TestObject__testMethod_block_desc_0_DATA, self, 570425344));
}
static void __TestObject__testMethod_block_func_0(struct __TestObject__testMethod_block_impl_0 *__cself) {
  TestObject *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_w5_2p585ycs4vsfdrh02hdfjwtw0000gn_T_TestObject_feb7ec_mi_0, (*(NSInteger *)((char *)self + OBJC_IVAR_$_TestObject$_age)));
    }

可以看到,对于成员变量的捕获是捕获了self,然后根据(*(NSInteger *)((char *)self + OBJC_IVAR_$_TestObject$_age))找到age的值。

对于其他情况下block对于变量的捕获原则就是,如果变量是局部变量,则会捕获,全局变量不捕获!!!

3、Block的类型

3.1、block也是一种OC对象

通过isa可以直接查看,如下代码也可以验证。

void(^block)(void);

void testBlock(){
    block = ^{
       
    };
    
    NSLog(@"%@", [block class]);
    NSLog(@"%@", [[block class] superclass]);
    NSLog(@"%@", [[[block class] superclass] superclass]);
    NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        testBlock();
        block();
    }
    return 0;
}

查看打印结果:

2019-08-25 23:07:44.800168+0800 studyBlock[8665:371790] __NSGlobalBlock__
2019-08-25 23:07:44.800380+0800 studyBlock[8665:371790] __NSGlobalBlock
2019-08-25 23:07:44.800399+0800 studyBlock[8665:371790] NSBlock
2019-08-25 23:07:44.800414+0800 studyBlock[8665:371790] NSObject

由此可以看出,block也是一种OC对象

3.2、三种类型的block

NSGlobalBlocck NSStackBlock NSMallocBlock

block有如上三种类型

如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block1)(void) = ^{
            NSLog(@"block1");
        };

        int age = 18;
         void (^block2)(void) = ^{
            NSLog(@"block2-%d", age);
        };

        NSLog(@"%@ \n %@ \n  %@ \n", [block1 class], [block2 class], [^{
                NSLog(@"block3 %d", age);
        } class]);
    }
    return 0;
}

执行打印结果如下:

NSGlobalBlock

NSMallocBlock

NSStackBlock

3种Block的内存位置如下图:

三种Block的判断:

3.3、三种类型的block对于对象类型的auto变量的访问

栈上的block对于对象类型的auto变量,不管对于auto变量是strong修饰还是weak修饰,都不会强引用该变量,因为栈上的block生命周期短暂,随时都可能销毁。

而栈上的block在被copy到堆上的时候,会根据auto的修饰符是strong还是weak,产生不同的效果。 copy函数内部会调用_Block_object_assign函数

_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

如果block从堆上移除

会调用block内部的dispose函数

dispose函数内部会调用_Block_object_dispose函数

_Block_object_dispose函数会自动释放引用的auto变量(release)

copy和dispose的调用时机如下:

__weak问题解决

在使用clang转换OC为C++代码时,可能会遇到以下问题

cannot create __weak reference in file using manual reference

解决方案:支持ARC、指定运行时系统版本,比如

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

4、Block对变量的修改

因block对于局部变量的捕获是值捕获,所以无法修改变量(block捕获的变量和局部变量在不同的函数栈) 修改方式:

把局部变量用static修饰
把局部变量升级为全局变量
添加__block修饰符

__block可以用于解决block内部无法修改auto变量值的问题

__block不能修饰全局变量、静态变量(static)

编译器会将__block变量包装成一个对象

如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        __block int age = 18;
        
        ^{
            NSLog(@"%d", age);
        }();
    }
    return 0;
}

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m编译后查看block:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

age被包装成如下对象:

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

被__block修饰的变量包装成的对象结构如下:

5、Block的内存管理

5.1、block对外部变量的引用

如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        __block int age = 18;
        NSObject* obj = [[NSObject alloc] init];
        __weak NSObject* weakObj = obj;
        
        ^{
            NSLog(@"%d", age);
            NSLog(@"%@", obj);
            NSLog(@"%@", weakObj);
        }();
    }
    return 0;
}

编译上面代码: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

查看:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong obj;
  NSObject *__weak weakObj;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, int flags=0) : obj(_obj), weakObj(_weakObj), age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
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->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

当block在栈上时,并不会对__block变量产生强引用

当block被copy到堆时 会调用block内部的copy函数 copy函数内部会调用_Block_object_assign函数 _Block_object_assign函数会对__block变量形成强引用(retain)

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

当block从堆中移除时 会调用block内部的dispose函数 dispose函数内部会调用_Block_object_dispose函数 _Block_object_dispose函数会自动释放引用的__block变量(release)

5.2、__block的__forwarding指针

forwarding指针保证了访问栈和堆上的block时捕获的变量的一致性。

5.2、被__Block修饰的对象类型

如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        __block int age = 18;
        __block TestObject* obj = [[TestObject alloc] init];
        //__weak TestObject* weakObj = obj;
        
        ^{
            NSLog(@"%@", obj);
        }();
    }
    return 0;
}

编译查看:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_obj_1 *obj; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_1 *_obj, int flags=0) : obj(_obj->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
struct __Block_byref_obj_1 {
  void *__isa;
__Block_byref_obj_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 TestObject *obj;
};

如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
         TestObject* obj = [[TestObject alloc] init];
        
        __block __weak TestObject* weakObj = obj;
        
        ^{
            NSLog(@"%@", weakObj);
        }();
    }
    return 0;
}

编译查看:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_weakObj_0 *weakObj; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakObj_0 *_weakObj, int flags=0) : weakObj(_weakObj->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
struct __Block_byref_weakObj_0 {
  void *__isa;
__Block_byref_weakObj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 TestObject *__weak weakObj;
};

可以看到,__main_block_impl_0中__Block_byref_weakObj_0指针指向__Block_byref_weakObj_0结构体,而__Block_byref_obj_1对于obj对象的引用是strong还是weak取决于block捕获的变量的修饰符(weak 或者 strong) 内存图如下图所示:

6、Block的循环引用

6.1、循环引用的原因

当一个对象持有block的时候,block如果同时持有该对象,则会引起循环引用问题,导致对象本身和block均不能释放,从而造成内存泄漏问题。

6.1、循环引用的解决

通过__weak、__unsafe_unretained 来解决

两者均不会对对象强引用

而两者的区别在于,__weak 会在对象销毁的时候自动将引用(指针)置为nil,而__unsafe_unretained会继续保留引用的对象的地址值,从未留下野指针隐患。

通过__block来解决, 必须调用blcok

__block id weakSelf = self;
self.blcok = ^{
    NSLog(@"%@", weakSelf);
    weakSelf = nil;
}
self.block();

如上代码,weakSelf 本身会持有对象,block也会持有对象,对象持有block,三者关系如左图所示,存在三角形循环引用问题,当我们执行block之后,weakSelf被手动置为nil,变为右图所示,解除了循环引用。

在ARC下使用__weak是解决循环引用的最佳姿势。

6.3、防止weakself引发的崩溃或者其他问题

场景一:

当你在某个界面请求网络数据的时候,用户不愿意等待点击了返回按钮,此时在Block当中用如下的方式使用weakSelf的话,有可能会奔溃(因为在并发编程的情况下,虽然在if判断的时候weakself不为空,但是不保证if语句里面的weakself不为空),所以为了安全起见要加上strongSelf

if (weakSelf != nil) {
       // last remaining strong reference released by another thread. 
       // weakSelf is now set to nil.
       [myArray addObject:weakSelf];
}

场景二:

在block内部需要执行self相关的任务,而此时weakSelf可能已经释放而无法完成。

分析如下代码:

  __weak TestBlock* weakSelf = self;
    
    self.block = ^(){
        __strong TestBlock* strongSelf = weakSelf;
        NSLog(@"%ld", (long)strongSelf.age);
    };

编译查看:

static void __TestBlock__test_block_func_0(struct __TestBlock__test_block_impl_0 *__cself) {
  TestBlock *__weak weakSelf = __cself->weakSelf; // bound by copy

        __attribute__((objc_ownership(strong))) TestBlock* strongSelf = weakSelf;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_w5_2p585ycs4vsfdrh02hdfjwtw0000gn_T_TestBlock_c9c9c2_mi_0, (long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)strongSelf, sel_registerName("age")));
    }

strongself其实是利用了循环引用的特性保证了在block执行的时候self对象不会被析构,strongSelf是局部变量执行完成后就会释放。