底层原理:Block

基本用法
#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //最简单的调用方式:
        ^{
             NSLog(@"this is a block");
          }();

         //一般通过将其存起来的方式进行调用
         void ^(block) (void) = ^{
              NSLog(@"this is a block");
          };
          block();
    }
    return 0;
}
block的本质
  • block本身也是一个OC对象,它内部也有一个isa指针
  • block是封装了函数调用以及函数调用环境的OC对象
  • block内部有两个基本的东西:
    1. 一个是impl存放着isa指针,代表block是什么类型的,impl还存放着FuncPtr,指向了我们将来要执行的函数地址。
    2. 另外一个是Desc,是block的描述信息,比如block的大小。
    3. 其他信息则为要捕获的信息。
block底层代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^ {
            NSLog(@"Hello World!");
        };
        block();
    }
    return 0;
}

我们先通过终端 cd到程序 main.m 的目录下,执行通过执行命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 将其转换成 C++ 代码,部分重要代码如下:

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;
  __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) {
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_5l_0xn052bn6dgb9z7pfk8bbg740000gn_T_main_88f00d_mi_0);
}

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(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        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;
}

我们把强制转换部分的代码去掉,于是 main 函数里最终会变成这样:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        //定义block变量
        void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
        //执行block内部的代码
        block->FuncPtr(block);
    }
    return 0;
}
block底层结构图
变量捕获
int age = 10;
static int height = 10;
void (^ block)(void) = ^{
    NSLog(@"age is %d",age);
    NSLog(@"height is %d",height);
}
age = 20;
height = 20;
block();

上面的结果age是10,height是20,因为在定义block的时候age的值已经被捕获进来了,而height传递的是地址

  • 为了保证block内部能够正常访问外部的变量,block有个变量捕获的机制,目前的结论是只要是基本类型变量,一定会进行变量捕获(待定)

之所以这两种变量的捕获方式有区别是因为auto类型的变量可能会被释放,内存会消失,所以要捕获到内存里,而 static 的变量是一直存放在内存中的。

  • 局部变量分为两种,一种是 auto,另外一种是 static,我们定义的局部变量默认是auto类型的,auto可以省略,被称为自动变量,离开作用域就销毁,auto只存在于局部变量里
  • 捕获到内部就是block内部会自动生成一个变量来存储刚才那个值。
  • 之所以会这样是因为局部变量在使用完成后内存就被回收了,为了防止block访问局部变量的时候变量被回收,故要将局部变量的值捕获进block内部了,而static修饰的局部变量还是在内存中存放的,所以可以直接通过地址访问,如果是全局变量则不用捕获。
#import 

int age = 10;
static int height = 10;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^ {
            NSLog(@"age is %d, height is %d",age , height);
        };
        age = 20;
        height = 20;
        block();
    }
    return 0;
}

这种类型下,输出结果是:age 是20,height 也是20;

auto变量的捕获结构如下图所示:

void (^block)(void) = ^{
   NSLog(@"------%p",self);
}
block();
  • 在上面的代码里self也是有被捕获的,因为self属于局部变量,是否有捕获只需要关注是局部变量还是全局变量即可。如果是_name或者也是从self里进行捕获的,self是作为默认的参数传递给block的,即使不写self默认也是有传递的。

block类型

  • block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block1)(void) = ^ {
            NSLog(@"Hello World!");
        };
        block();
        NSLog(@"%@", [block1 class]);
        NSLog(@"%@", [[block1 class] superClass]);
        NSLog(@"%@",[[[block1 class] superClass] superClass]);

        int age = 10;
        void(^block1)(void)=^{
             NSLog(@"Hello World!, %@", age);
        };
        NSLog(@"%@", [block2 class]);

        NSLog(@"%@",[ ^{
             NSLog(@"Hello World!, %@", age);
        }  class]);
    }
    return 0;
}
  1. __NSGlobalBlock__ (_NSConcreteGlobalStackBlock)
  2. __NSStackBlock__ (_NSConcreteStackBlock)
  3. __NSMallocBlock__ (_NSConcreteMallocBlock)

以下图片展示的内存地址依次是由低到高:

  • 程序区域也称为代码段,平时我们编写的代码都是放在这个区域的
  • 数据区域一般用于放全局变量,GlobalBlock放在这个区域
  • 一般用于放我们allocmalloc出来的东西,是动态分配内存的,特点是内存需要开发者代码去申请,也需要程序员自己管理内存,MallocBlock放在堆区
    用于存放一些局部变量,特点是系统自动分配内存,而且会自动销毁内存,离开大括号即作用或就销毁,StackBlock则放在栈区
  • 代码段数据区域这两个也是由编译器决定好的,一旦代码编译成功则会被放在这两个地方,如果是代码也放在第一个,如果是全局变量则放在第二个。
  • 函数调用栈即在栈区开辟一块区域给函数用,使用完即被回收。

如果想要知道一个对象存放在内存中的什么位置,可以使用如下方法,看它跟哪个对象的地址值相对即可推测出来

#import 
#import "AppDelegate.h"

int age = 10;
int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        int a = 20;
        NSLog(@"数据段:age %p",&age);
        NSLog(@"栈:a %p",&a);
        NSLog(@"堆:obj %p", [[NSObject alloc]init]);
        NSLog(@"class %p",[AppDelegate class]);
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

确定block属于哪种类型根据下图:

Block类型

但是验证这一步骤的时候要将Build Settings -> automatic Reference Counting 中将YES必为NO才可以,也就是切换成非ARC模式下。放在栈上的block有个问题,其内部访问的变量可能早就被释放了,栈上的block经过copy之后就会变成放在堆上。global类型的block经过copy后还是global类型的,什么也不做,上的block经过copy后引用计数增加,还是在上。

各类型block调用copy

block的copy

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

参考如下:

#import 
#import "AppDelegate.h"

typedef void (^ myBlock) (void);

myBlock blockTest(void)
{
    int age = 10;
    myBlock block =  ^{
        NSLog(@"----%d",age);
    };
    return block;
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        //情况1
        myBlock block = blockTest();
        block();
        NSLog(@"%@",[block class]);
        
        
        //情况2
        int age = 10;
        myBlock block1 = ^{
            NSLog(@"----%d",age);
        };
        NSLog(@"%@",[block1 class]);
        
        //情况3
        NSArray *array = @[];
        [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                    
        }];
        
        //情况4
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
        });
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
对象类型的auto变量

我们先来看下在ARC环境下这段代码里 person 对象何时释放

#import 

typedef void (^ myBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

       MJBlock block;
       {
          Person *person = [[Person alloc]init];
          person.age = 10;
          block = ^{
             NSLog(@"---------%d",person.age);
          };
          //[person release];   //MRC时加上这句
       }
        NSLog(@"--------");
        //block();
    }
    return 0;
}

上面这段代码,在 NSLog 调用的时候,person 对象是没有被释放的,但是在 MRC 下,加上上面的 [person release] 以后,在 NSLog 调用的时候,person 对象已经被释放,原因是在MRC上时,这个block是放在栈上的,而在ARC下此时block是存放在堆上的,MRC时对block拷贝后,person对象也不再释放,总结:存放在栈空间的block是不会保住person对象的命的,堆空间的block可以

  • 弱引用这种技术需要运行时的支持,因此如果在ARC模式下把OC代码转为C++代码时,可能会遇到以下问题:
    can not 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

下面的操作是在 ARC

  • 当block内部访问了对象类型的auto变量时

    1. 如果block是在栈上,将不会对auto变量产生强引用,因为block自己可能随时被释放。
  • 如果block被拷贝到了堆上(如果block被强指针引用着则自动会进行copy操作)

    1. 会自动调用block内部的 copy 函数
    2. copy函数内部会调用 _Block_object_assign函数,这个函数会根据 auto 变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,类似于 retain(形成强引用、弱引用)
  • 如果block从堆上移除

    1. 会自动调用block内部的 dispose 函数
    2. dispose函数会调用 _Block_object_dispose 函数,_Block_object_dispose 函数会自动释放引用的auto 变量,类似于 release
函数 调用时机
copy函数 栈上的Block复制到堆时
dispose函数 堆上的Block被废弃时

block如何在内部修改局部变量的值

默认情况下,block只可以读取外面的变量的值,不能修改值,如下面代码

#import 

typedef void (^ myBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
          int age = 10;
          myBlock block = ^{
              //加上这句代码就编译不通过
              //age = 20;
              NSLog(@"age is %d",age);
          };
          block();
    }
    return 0;
}
  • 将局部变量声明为static(变量永久在内存中),如
static int age = 10;
  • 声明为全局变量
  • 加__block,如下所示
#import 

typedef void (^ myBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
          __block int age = 10;
          myBlock block = ^{
              age = 20;
              NSLog(@"age is %d",age);
          };
          block();
    }
    return 0;
}

__block修饰符

  • __block 可以用于解决block内部无法修改 auto 变量值的问题
  • __block 不能修饰全局变量、静态变量(static
  • 编译器会将 __block 修饰的(基本)变量包装成一个对象(内部有 isa 指针),通过指针找到结构体,对象内部有这个基本类型变量,通过指针来访问这个变量,之前不加 __block的时候是被捕获进block内

__block 会在编译时候将修饰的变量包装成一个对象。我们来验证一下,我们把以下代码转成 C++ 代码:

__block int age = 18;
void (^block)(void) =  ^{
     NSLog(@"Hello Block %d", age);
 };
block();

可观察 int 类型的 age 被 __block 包转成了一个对象:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding; // 指向自己的指针
 int __flags;
 int __size;
 int age;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  ...
}

下面这种情况是不需要加 __block 的,这种是使用 array 这个指针,并不是修改 array 这个指针

#import 

typedef void (^ myBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
          NSMutableArray *array = [NSMutableArray array];
          myBlock block = ^{
              
              [array addObject:@"123"];
              //这种情况才需要添加 __block
              //array = nil;
          };
          block();
    }
    return 0;
}

__block内存管理

基本类型的变量默认是分配在栈上,block默认也是在栈上的,在ARC环境下,一旦block被强指针引用着,会对在栈上的block进行copy操作,并且对block内部用到的__block修饰的变量也拷贝到堆上,而且对该变量形成的是强引用

  • 当block在栈上时,并不会对__block变量产生强引用
  • 当block被copy到堆时:
    1. 会调用block内部的 copy 函数
    2. copy 函数内部会调用 _Block_object_assign 函数
    3. _Block_object_assign 函数会根据所指向的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain
  • 当block从堆中移除时
    1. 会调用block内部的 dispose 函数
    2. dispose 函数会调用 _Block_object_dispose 函数,
    3. _Block_object_dispose 函数会自动释放引用的 __block 变量(release)
  • 当block在栈上时,并不会对 __block 修饰的变量和无 __block 修饰的基本类型变量产生强引用
  • 当block拷贝到堆上时,都会通过 copy 函数来处理它们:
    1. __block变量(假设变量名叫做a),_Block_object_assign((void *)&dst -> a, (void *)src -> a, 8);
    2. 对象类型的 auto 变量(假设变量名叫做p), _Block_object_assign((void *)&dst -> p, (void *)src -> p, 3);
  • block 从堆上移除时,都会通过 dispose 函数来释放它们
    • __block变量(假设变量名叫做a)
    • _Block_object_dispose((void *)src -> a, 8);
  • 对象类型的auto变量(假设变量名叫做p)
    • _Block_object_dispose((void *)src -> p, 3);
对象 BLOCK_FIELD_IS_OBJECT
__block变量 BLOCK_FIELD_IS_BYREF
对象类型的auto变量、__block变量

总结:

  • 当block在栈上时,对它们都不会产生强引用
  • 当block拷贝到堆上时,都会通过 copy 函数来处理它们:
    • __block变量(假设变量名叫做a)
    • _Block_object_assign((void)&dst->a,(void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
  • 对象类型的auto变量(假设变量名叫做p)
    • _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_0BJECT/);
  • 当block从堆上移除时,都会通过djspose函数来释放它们
    _block变量(假设变量名叫做a)
    _Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
    对象类型的auto变量(假设变量名叫做p)
    _Block_object_dispose((void
    )src->p, 3/BL0CK_FIELD_IS_0BJECT/);
    对象
    BLOCK_FIELD_IS_OBJECT
    block变量
    BLOCK_FIELD_IS_BYREF
__block的__forwarding指针
__block修饰的对象类型
#import "Person.h"
#import 

typedef void (^ myBlock) (void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
          __block Person *person = [[Person alloc]init];
          myBlock block = ^{
              NSLog(@"%@",person);
          };
          block();
    }
    return 0;
}
  • __block __weak不能用来修饰基本类型变量,但是可以修饰对象类型
Person *person = [[Person alloc]init];
__block __weak Person *weakPerson = person;
block循环引用

代码如下所示:

typedef  void (^ myBlock) block; 
@interface Person : NSObject
@property (assign, nonatomic) int age;
@property (copy, nonatomic) myBlock block;
@end

#import 
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

     Person *person = [[Person alloc]init];
     person.ag = 10;
     person.block = ^{
         //这里即使只写_age也是会产生循环引用的,因为_age存在于person内部
         NSLog("age is %d",person.age);
     };
    }
    return 0;
}

解决循环引用问题 - ARC

  • __weak__unsafe_unretained 解决,如下所示:
#import 
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

     Person *person = [[Person alloc]init];
     person.ag = 10;
     __weak Person *weakPerson = person; 
     //或者用这种方式:__weak typeof(person) weakPerson = person;  其中 typeof(person)  就等于 Person *
     //还有__unsafe_unretained Person *weakPerson = person; 这种方式
     //__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil 
     //__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变
     person.block = ^{
         //这里即使只写_age也是会产生循环引用的,因为_age存在于person内部
         NSLog("age is %d",weakPerson.age);
     };
    }
    return 0;
}
  • __block 来解决,这种方式的弊端是必须要执行block,如下所示:
#import 
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

     __block Person *person = [[Person alloc]init];
     person.ag = 10;
     person.block = ^{
         NSLog("age is %d",weakPerson.age);
         person = nil;
     };
     person.block();
    }
    return 0;
}

解决循环引用问题 - MRC

  • MRC下虽然weak不能用,但是__unsafe_unretained是可以使用的,所以可以使用它解决循环引用问题。
//用__unsafe_unretained解决
__unsafe_unretained id weakSelf = seif;
self.block = ^{
    NSLog(@"%p",weakSelf);
};

//用__block解决
__block id weakSelf = self;
self.block = ^{
    printf("%p",weakSelf);
};
面试题
  • block的原理是怎样的?本质是什么?
  • __block的作用是什么?有什么使用注意点?
  • block的属性修饰词为什么是copy?使用block有哪些使用注意?
  • block在修改NSMutableArray时,需不需要添加__block?

你可能感兴趣的:(底层原理:Block)