iOS底层原理之`OC语法`(Block)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Bolted_snail/article/details/82841371

block本质

对于iOS中的block很对人都会说是封装了一块代码或者是说就是一个代码块,这种回答虽然是对的,但是很浅显。那block究竟是个什么东西呢?我们可以编译后看一下底层的实现。
原文件:
示例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void (^blocktest)(void) = ^{
            NSLog(@"%d",a);
        };
        blocktest();
       
    }
    return 0;
}

编译后的cpp文件

//mian函数
int main(int argc, const char * argv[]) {
    { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        //block定义
        void (*blocktest)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        //block调用
        ((void (*)(__block_impl *))((__block_impl *)blocktest)->FuncPtr)((__block_impl *)blocktest);

    }
    return 0;
}

//block定义
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; //指向block的真实类型
    impl.Flags = flags;
    impl.FuncPtr = fp;//用于block调用
    Desc = desc;//block的一些描述
  }
};

//__main_block_impl_0结构体中的第一个元素(block的内部实现)
struct __block_impl {
  void *isa;//isa指向block的类
  int Flags;
  int Reserved;
  void *FuncPtr;//通过该指针实现调用
};

//__main_block_impl_0结构体中的第二个元素
static struct __main_block_desc_0 {
  size_t reserved;//默认是0
  size_t Block_size;// 描述bloc的大小
}

由上面可以看出:block是封装了函数调用以及函数调用环境的OC对象。(是一个结构体并且有isa指针,所以是一个oc对象; block定义的结构体中__main_block_impl_0 里面的FuncPtr指针是用来block调用的),其底层结构如下:
block底层结构

变量的捕获

  • 为了保证block内部能够正常访问外部的变量,block有个变量捕获机制
    变量捕获机制
  • 变量一般分为全局变量(函数外面声明的变量,声明以后的函数和外面都能访问)和局部变量(函数内部声明的变量,只有函数内部能访问),而局部变量又分为自动局部变量(auto修饰的,不写默认就是自动变量,离开作用域就会被销毁 )和静态局部变量(static修饰的,只创建一次,离开作用域不会被销毁)。
    原文件:
int a = 10;//全局变量
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int b = 20;//自动局部变量
        static int c = 30;//静态局部变量
        void (^blocktest)(void) = ^{
            a = 40;
//            b = 50; 会报错
            c = 60;
           NSLog(@"a : %d",a);
            NSLog(@"b : %d",b);
            NSLog(@"c : %d",c);
        };
        blocktest();
    }
    return 0;
}

编译后的cpp文件:

int a = 10;
//block 声明
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *c;
  int b;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_c, int _b, int flags=0) : c(_c), b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//block 实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *c = __cself->c; // bound by copy
  int b = __cself->b; // bound by copy
            a = 40;
            (*c) = 60;
             NSLog((NSString*)&__NSConstantStringImpl__var_folders_w1_l3yxhrdj1210bnj2d6hrz4p00000gp_T_main_f85032_mi_0,a);
             NSLog((NSString*)&__NSConstantStringImpl__var_folders_w1_l3yxhrdj1210bnj2d6hrz4p00000gp_T_main_f85032_mi_1,b);      
             NSLog((NSString*)&__NSConstantStringImpl__var_folders_w1_l3yxhrdj1210bnj2d6hrz4p00000gp_T_main_f85032_mi_2,(*c));
//main 函数
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        auto int b = 20;
        static int c = 30;
        void (*blocktest)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &c, b));
        ((void (*)(__block_impl *))((__block_impl *)blocktest)->FuncPtr)((__block_impl *)blocktest);
    }
    return 0;
}

打印结果:
打印结果

  • 由上面可以看出:
  1. 如果是全局变量,block里面是可以直接访问的,所以可以修改其值;
  2. 如果是自动局部变量,在block 定义的时候内包会声明一个同样的名称的变量,并将值赋值给该变量,所以此时无法访问到外面的自动变量,故不能修改其值。(此时获取该变量的值实际上是block内包的同名变量,不是外面的变量,可以通过打印地址验证);
  3. 如果是静态局部变量,在block 定义的时候内包会声明一个同样的名称的带*号的变量,指向的是该变量的地址,可以通过地址来修改该变量的值。

注意:自动局部变量是基本数据类型时是值传递,block内部不能访问该,但是如果是OC对象变量,由于传进去的是个对象即指针(*p),所以是可以直接访问的。

block类型

  • block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
    __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
    __NSStackBlock__ ( _NSConcreteStackBlock )
    __NSMallocBlock__ ( _NSConcreteMallocBlock )
  • 首先看一下三种block的内存分配图
    block的内存分配
  1. 程序区域局是放代码原码,很小在最前面;
  2. 数据区域:主要存储全局变量;
  3. 堆:存放alloc出的对象,需要程序员自己申请和管理内存;
  4. 栈:存储基本数据类型的局部变量,会自动分配和释放内存(作用域)。
  • 那么如何区别一个block是那种类型呢,判定条件如下:
    block类型的判定条件
    示例:
int a = 1;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void (^block1)(void) = ^{
            NSLog(@"a : %d",a);
        };
        int b = 2;
        void (^block2)(void)= ^ {
            NSLog(@"b : %d",b);
        };
         int c = 3;
        void (^block3)(void) = [^{
            NSLog(@"c : %d",c);
        } copy];
        
        block1();
        block2();
        block3();
        
        NSLog(@"block1父类的父类 : %@",[[[block1 class] superclass] superclass]);
        NSLog(@"block1 : %@",[block1 class]);
        NSLog(@"block2父类的父类  : %@",[[[block2 class] superclass] superclass]);
        NSLog(@"block2 : %@",[block2 class]);
        NSLog(@"block3 : %@",[block3 class]);
    }
    return 0;
}

打印结果:
block类型
结果可以看出:block2的类型应该是__NSStackBlock__的,但是为啥是__NSMallocBlock__呢?这是因为在ARC环境下,当访问自动局部变量时,block会自动进行copy操作。
该为mrc环境
改为MRC环境
打印结果:
MRC打印结果
MRC下block2的类型为__NSStackBlock__

  • 每一种类型的block调用copy后的结果如下所示
    block进行copy操作

  • 在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
    block作为函数返回值时;
    将block赋值给__strong指针时;
    block作为Cocoa API中方法名含有usingBlock的方法参数时;
    block作为GCD API的方法参数时。

  • MRC下block属性的建议写法
    @property (copy, nonatomic) void (^block)(void);

  • ARC下block属性的建议写法
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);

  • 为什么当是基本数据类型局部自动变量时,要进行copy呢?
    因为访问基本数据类型局部自动变量时,block是在栈上,将不会对auto变量产生强引用,这样就有可能在block访问auto变量前,变量就已经销毁了。

  • 如果block被拷贝到堆上,会调用block内部的copy函数了,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)。
    在这里插入图片描述

  • 在使用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-10.0.0 main.m

__block修饰符

  • 上面可以看出当时自动局部变量(基本数据类型)时,block内部是不能修改该auto变量的值的(因为拿不到该变量),但是用__block 修饰该变量时就可以访问该变量了。
    示例:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block auto int a = 10;
         NSLog(@"&a : %p",&a);
        void (^block)(void) = ^{
            a = 20;
            NSLog(@"a : %d",a);
            NSLog(@"&a : %p",&a);
        };
        block();
    }
    return 0;
}

编译后cpp文件

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

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

编译器会将__block变量包装成一个对象,其结构如下:
__block

  • __block的内存管理
    当block在栈上时,并不会对__block变量产生强引用;
    当block被copy到堆时;
    会调用block内部的copy函数;
    copy函数内部会调用_Block_object_assign函数;
    _Block_object_assign函数会对__block变量形成强引用(retain);
    一旦__block变量呗复制到堆中后,其他的block再访问该变量时,该变量不会再一次复制到堆,也就是该__block变量只会被复制到堆上一次;

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

  • __block的__forwarding指针
    __forwarding
    由上面示意图可以看出: 如果__block变量结构体只在栈中, __forwarding使用指向栈中的该变量结构体;如果复制到__block变量结构体复制到了堆中,这时栈和堆中都有该__block变量结构体,都有__forwarding指针,但是不管该__forwarding是在栈中还是在堆中,都会指向堆中的变量结构体,保证了唯一性。

循环引用问题

循环引用
当一个对象持有block,而block又持有该对象,这样就产生的循环引用,就会造成这两块内存都没法释放,从而导致内存泄漏问题。

#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person * p = [[Person alloc]init];
        [p test];
    }
    return 0;
}

#import "Person.h"
@interface Person()
@property(nonatomic ,copy) void(^block)(void);
@end
@implementation Person
-(void)test{
    self.block = ^{
        NSLog(@"%@",NSStringFromClass([self class]));
    };
    self.block();
}
-(void)dealloc{
    NSLog(@"%s",__func__);
}
@end

上面示例可以看出:Person实例化时被p强引用了 Person 强引用了block,在block中访问了self变量,所以又强应用了Person,当程序运行完,p这个指针不再指向Person初始化的这块儿地址,但是block还在指向个Person实例对象,导致该Person对象无法释放。

  • 解决方案:用__weak__unsafe_unretained解决
    __weak typeof(self)weakSelf = self;
    self.block = ^{
        NSLog(@"%@",NSStringFromClass([weakSelf class]));
    };
    或者
     __unsafe_unretained typeof(self)weakSelf = self;
    self.block = ^{
        NSLog(@"%@",NSStringFromClass([weakSelf class]));
    };

__weak__unsafe_unretained修饰self时,block就会弱引用Person,这时候当程序运行完p指针不在指向Person实例对象,block是弱引用,这时没有强指针,所以该Person实例对象会销毁,在销毁前也会先销毁block(进行一次release操作)。
其原理图如下:
弱引用

  • 注意:self是一个隐示参数,每一个方法都含有self和SEL这两个隐示参数,所以self是一个局部变量;成员变量是保存到对象中,访问成员变量其实本质是通过self->成员变量,其实也访问了self。
  • __weak__unsafe_unretained的区别:
    __weak 不会产生强引用,指向的对象销毁时,会自动让指针置为nil;
    __unsafe_unretained 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变(可能产生僵尸对象)。
  • 上面是ARC的情况,如果是MRC情况如何解决循环引用问题呢?
    使用__unsafe_unretained__block修饰self即可(同上),因为MRC没有weak关键字,所以不能用__weak修饰。

block的具体使用

  • 声明Block类型变量语法:

返回值类型 (^变量名)(参数列表) = Block表达式
示例:

int (^test)(int) = ^(int a){
    return  a + 1;
};
  • block作为属性
typedef int (^Test)(int);
@interface Person : NSObject{
    int (^_a)(int);
    Test _test;
}
//block作为属性
@property(nonatomic ,copy) int(^a)(int b);
@property(nonatomic ,copy) Test test;
@end

@implementation Person
- (void)setA:(int (^)(int))a{
    _a = a;
}
-(int (^)(int))a{
    return _a;
}
- (void)setTest:(Test)test{
    _test = test;
}
- (Test)test{
    return _test;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
//        NSLog(@"%d", test(10));
        Person * p = [[Person alloc]init];
        p.a = ^int(int a) {
            return a+1;
        };
        NSLog(@"%d", p.a(10));
    }
    return 0;
}
  • block作为方法参数
//还是给上面Person类添加的方法
- (void)funcWithBlock:(int (^)(int))parblock{
    NSLog(@"%d",parblock(100));
}
//调用
 [p funcWithBlock:^int(int a) {
            return a+1;
        }];
  • block作为方法的返回值
//block作为返回值
-(int (^)(int))returnBlockFunc{
    return ^(int count){
        return count+1000;
    };
}
//调用
int (^returnValue)(int) = [p returnBlockFunc];
NSLog(@"%d", returnValue(1000));
  • 利用typedef起别名简化block
typedef int (^Test)(int);
//属性
@property(nonatomic ,copy) Test test;
//参数
- (void)funcWithBlock:(Test)parblock;
//返回值
- (Test)returnBlockFunc;

面试题

  • block的原理是怎样的?本质是什么?
    封装了函数调用以及调用环境的OC对象。
  • __block的作用是什么?有什么使用注意点?
    可以将局部自动变量转换为一个结构体,以达到在block内部访问到该自动变量。如果是对象内向的局部自动变量,因为本身就是指针类型,所以不需要用__block修饰就可以直接访问。
  • block的属性修饰词为什么是copy?使用block有哪些使用注意?
    block一旦没有进行copy操作,就不会在堆上,注意循环引用问题。
    block在修改NSMutableArray,需不需要添加__block?
    不需要,因为NSMutableArray不是基本数据类型,在block中是指针传递。


相关原码:OC底层原理之OC语法
相关课件:OC底层原理之OC语法课件

猜你喜欢

转载自blog.csdn.net/Bolted_snail/article/details/82841371
今日推荐