iOS原理篇(五):Block探究

  • Block原理
  • Block变量捕获
  • Block类型
  • copy操作和Block内部访问对象类型的变量
  • __block修改变量及其本质
  • __block内存管理
  • Block循环引用问题

Block是一种可以在CC++以及Objective-C代码中使用,类似于“闭包(closure)”的代码块,借助Block机制,开发者可以将代码像对象一样在不同的上下文环境中进行传递。
(这里说的不同上下文环境,我举个例子:比如在A函数中定义了一个变量,它是一个局部变量,那么我要在B函数中去访问,这里就属于两个不同的上下文环境)

一、Block原理

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

将上面main.m编译生成C++代码:

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

main()函数

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int age = 20;
        void (*block)(int,int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
    }
    return 0;
}

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

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

struct __maib_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

我们定义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_impl_0内部的构造函数初始化一个结构体出来,然后取结构体地址&__main_block_impl_0赋给block指针,所以block底层是下面结构体;调用构造函数传了三个参数:
(void *)__main_block_func_0&__main_block_desc_0_DATAage

其中(void *)__main_block_func_0是下面函数的地址,这个函数就是封装了block执行逻辑的函数,通过上面的构造函数传给__block_impl结构体的FuncPtr

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__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_b54551_mi_0,a,b,age);
}

同样,第二个参数类型&__main_block_desc_0_DATA是下面结构体地址,最终通过构造函数赋给了Desc,其中Block_size表示block结构体的大小;

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

main()函数中调用block(3, 5)最终转化为下面代码,通过将block强制转换为__block_impl(这里__block_impl类型是__main_block_impl_0结构体第一个成员,所以可以转) ,最终直接找到impl中的FuncPtr进行调用

((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);

二、Block变量捕获

Block变量捕获是指在Block内部访问外部变量时,如果外部变量是局部变量,则Block内部会将其捕获,具体捕获形式看外部的这个局部变量是auto类型还是static类型:
如果是auto类型,直接将变量的值传递给Block内部,Block结构体内部会生成一个变量来存储传进来的值,所以在Block外边改变age=20,调用block()时内部打印的结果依然是age=10,因为此时进行的是值传递;
如果是static类型,会将变量的地址传递给Block内部,block结构体内部会生成一个指针变量来存储传进来的地址值,所以在block外边改变height=20,调用block()时内部打印的结果是height=20,因为此时进行的是指针传递;

下面进行验证:

  1. 局部变量两种情况:
// 局部变量两种情况
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // case-1: auto变量,离开作用域就销毁
        auto int age = 10; //等价于 int age = 10;
        // case-2: static变量
        static int height = 10;
        
        void (^block)(void) = ^{
            NSLog(@"age is %d, height is %d",age, height);
        };
        age = 20;
        height = 20;
        
        block();
    }
    return 0;
}

打印结果:

age is 10, height is 20

编译成C++:

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

从编译生成的结构体看出,age是值传递,height是指针传递;定义完block就将10&height捕获到block内部,后边调用block时访问的结构体内部age是捕获到的值10,height是捕获到的地址&height

  1. 全局变量:因为是在全局区,所以任何函数内部可以直接访问

总结一下:


Block的本质:Block本质上也是一个OC对象,它内部也有个isa指针,但它是一个封装了函数调用以及函数调用环境的OC对象;

三、Block类型

Block有三种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^{
            NSLog(@"Hello World!");
        };
        
        NSLog(@"%@",[block class]);
        NSLog(@"%@",[[block class] superclass]);
        NSLog(@"%@",[[[block class] superclass] superclass]);
        NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}

打印结果:
05-block类型[46881:69217078] __NSGlobalBlock__
05-block类型[46881:69217078] __NSGlobalBlock
05-block类型[46881:69217078] NSBlock
05-block类型[46881:69217078] NSObject

三种类型:

  • __NSGlobalBlock__ (_NSConcreteGlobalBlock)
  • __NSStackBlock___NSConcreteStackBlock
  • __NSMallocBlock___NSConcreteMallocBlock
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d", age);
        };
        NSLog(@"%@ %@ %@",[block1 class], [block2 class], [^{
            NSLog(@"%d",age);
        } class]);
    }
    return 0;
}

打印结果:
05-block类型[47475:69339707] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

那不同类型Block分别对应什么情况呢?

static int height = 30;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 没有访问外部变量
        void (^block1)(void) = ^{
            NSLog(@"--------------");
        };
        
        // 访问 auto 变量
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"--------------%d", age);
        };
        
        // 访问 static 变量
        void (^block3)(void) = ^{
            NSLog(@"=-------------%d", height);
        };
        
        NSLog(@"%@ %@ %@",[block1 class], [block2 class], [block3 class]);
    }
    return 0;
}

打印结果:
05-block类型[48630:69576321] __NSGlobalBlock__ __NSMallocBlock__ __NSGlobalBlock__

可以看出,在没有访问外部变量的情况下,block1是一个__NSGlobalBlock__类型,存放在数据区,此时的block1就相当于我们定一个了一个函数,函数中的代码没有访问另外一个函数(此处为main())中的变量;同理,block3虽然访问外部变量,但static变量是全局的,同样相当于单独拿出去定义一个和main()函数上下文无关的函数;
由于block2访问了auto变量,相当于在block2封装的函数中访问了另外一个函数内部的变量(main()函数中的局部变量age),此时block2变为__NSStackBlock__,因为它需要保存这个局部变量,由于是在ARC环境,会自动对__NSStackBlock__类型进行copy操作,所以 block2打印类型是一个 __NSMallocBlock__类型;

关闭ARCMRC环境下打印:

打印结果:
05-block类型[49786:69814242] __NSGlobalBlock__ __NSStackBlock__ __NSGlobalBlock__

可以看出block2确实是一个__NSStackBlock__类型;

四、copy操作和Block内部访问对象类型的变量


copy操作分MRCARC两种情况:
  • MRC环境:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        // 情况一:没有访问外部 auto 变量
        // 它是一个 NSGlobalBlock
        // 内存是放在数据段
        void (^block)(void) = ^{
            NSLog(@"---------");
        };
        

        // 情况二:访问外部 auto 变量 age
        // 它是一个 NSStackBlock 随时会被回收
        // 内存是在栈上
        // 通过 copy 操作转变为 NSMallocBlock 把它放到堆上保活
        void (^block2)(void) = [^{
            NSLog(@"---------%d",age);
        } copy];
        
        // 因为在 MRC 环境 不用时要进行 release 操作
        [block2 release];
    
    }
    return 0;
}
  • ARC环境:
    ARC环境下,编译器会根据情况自动将栈上的Block拷贝到堆上,即自动进行一次copy操作,比如以下情况:
  1. 情况一:Block作为函数返回值
// 定义一个block类型
typedef void (^DJTBlock)(void);

// block作为函数返回值
DJTBlock myblock()
{
    // case1: 这里没有访问auto变量 是一个NSGlobalBlock
    return ^{
        NSLog(@"------------");
    };
    // 相当于下面这样写
    // DJTBlock block = ^{
    //   NSLog(@"------------");
    // };
    // return block;
    
    //-----------------------------------------------------------------
    
    // case2: 这里访问了auto 是一个NSSackBlock 作为函数返回值ARC下自动copy成NSMallocBlock
    // int age = 10;
    // return ^{
    //   NSLog(@"------------%d",age);
    // };
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // ARC环境下 调用myblock()函数
        // DJTBlock作为myblock函数的返回值 编译器自动进行一次 copy 操作
        // 所以 block变量指向的 DTJBlock 此时已经在堆上
        DJTBlock block = myblock();
        block();
        
        // 打印 block 类型
        NSLog(@"%@",[block class]);
    
    }
    return 0;
}
打印结果:
05-block--copy[64907:9167520] ------------
05-block--copy[64907:9167520] __NSGlobalBlock__

打印结果是一个NSGlobalBlock类型,这是因为在函数my block()内部没有访问auto变量(上面block类型有阐述),而对NSGlobalBlock类型的Block执行copy操作生成的Block还是NSGlobalBlock,所以如果将返回改为myblock()函数内注释部分,就会打印__NSMallocBlock__

  1. 情况二:将Block赋值给__strong强指针时
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        // Block被强指针指着
        DJTBlock block = ^{
            NSLog(@"------------%d",age);
        };
        block();
       
        // 打印 block 类型
        NSLog(@"%@",[block class]);
    }
    return 0;
}
打印结果:
05-block--copy[69520:9293376] ------------10
05-block--copy[69520:9293376] __NSMallocBlock__
  1. 情况三:Block作为Cocoa API 中方法各含有usingBlock的方法参数时:
NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

这个参数Block也是一个堆上的block;

  1. 情况四:Block作为GCD API的方法参数时:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});
    
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
});

Block内部访问对象类型的变量

先看一个有趣的现象:

// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end

// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
    NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
        }
        NSLog(@"-----------------");// 打断点
    }
    return 0;
}

在上面NSLog(@"-----------------");处打断点,运行程序发现控制台打印:

05-block访问对象类型的auto变量[77563:9561984] DJTPerson----dealloc
(lldb) 

说明在断点前的中括号结束,person变量就已经释放,接着我们定义一个block,在内部访问personage属性:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DJTBlock block;
        
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"-----------%d",person.age);
            };
        }
        
        NSLog(@"-----------------");// 打断点
        
    }
    return 0;
}

通用在NSLog(@"-----------------");处打断点,运行程序发现控制台无打印,说明person没被回收。

为什么被第二种情况下person没有被回收呢?为了验证我们将代码简化并编译成C++来进行底层原理分析:

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DJTPerson *person = [[DJTPerson alloc] init];
        person.age = 10;
        
        DJTBlock block = ^{
            NSLog(@"-----------%d",person.age);
        };
    }
    return 0;
}

日常操作命令:

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

在编译生成的C++文件中查看生成的block结构体:

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

由于personDJTPerson *类型,所以捕获到block内部也是DJTPerson *类型,即struct __main_block_impl_0结构体内部可以看到有一个DJTPerson *类型变量person;下面先从一个角度理解为什么person没有被释放:

在上面代码中,我们定义的Block是被一个DJTBlock类型的变量block强引用的,即这句代码:

DJTBlock block = ^{
   NSLog(@"-----------%d",person.age);
};

ARC环境下,被强引用的这个Block(访问了auto变量)会自动拷贝到堆上,而这个Block内部(编译成C++即为struct __main_block_impl_0结构体)又有一个DJTPerson*类型的指针指向外面这个person对象,所以只要这个Block在,那么这个强指针就在,所以外边的person对象不会被释放;

换成MRC环境:

// DJTPerson.h
@interface DJTPerson : NSObject
@property(nonatomic, assign) int age;
@end

// DJTPerson.m
@implementation DJTPerson
- (void)dealloc
{
   [super dealloc];
    NSLog(@"DJTPerson----dealloc");
}
@end
// main.m
#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DJTBlock block;
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"-----------%d",person.age);
            };
            
            [person release];
        }
        NSLog(@"---------------");// 打断点
    }
    return 0;
}

依然在NSLog(@"---------------");打断点,发现控制台打印结果:

05-block访问对象类型的auto变量[83896:9803518] DJTPerson----dealloc

发现person被释放,这是因为即使block内部访问了person对象,MRC环境下,block内部访问了auto变量,它是一个栈上block,但并不会自动拷贝到堆上,由于它是一个NSStackBlock,内部并不会对外部person强引用(这里说强引用并不准确,在MRC环境没有强引用说法,应该描述为没有对外边person进行retain操作,但为了好理解 so...),所以在执行完[person release]以后,虽然Block还没有离开其作用域(Block作用域到return 0;前到大括号),但person就被释放;可以通过[block copy]将其复制到堆上,这样内部就会对外边的person强引用(其实是retain操作)从而保活person,当然在Block销毁的时候,内部对person还会进行一次release操作,这样一加一减,就保持了平衡;

要点:栈空间的BlockNSStackBlock)是不会对外边auto对象进行保活(ARC环境表现为不会强引用,MRC下表现为不会进行retain操作),只有拷贝到堆上(NSMallocBlock)才会对其自动保活。

回到ARC环境:
看一下__weak作用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DJTBlock block;
        {
            DJTPerson *person = [[DJTPerson alloc] init];
            person.age = 10;
            
            // 这里使用 __weak 修饰
            __weak DJTPerson *weakPerson = person;
            block = ^{
                NSLog(@"-----------%d",weakPerson.age);
            };
            
        }
        NSLog(@"---------------"); // 打断点
    }
    return 0;
}

依然在NSLog(@"---------------");处打断点,打印结果为:

05-block访问对象类型的auto变量[87323:9930285] DJTPerson----dealloc

这说明,即使在ARC环境,Block被拷贝到堆上,由于我们用__weak类型的__weakPerson访问了外部auto变量,它也不会对外部person进行强引用。

同样我们把上述代码编译成C++,由于弱引用需要运行时机制来支持,所以我们不能进行静态编译,还需要运行时调用,指定运行时系统版本,所以编译命令如下:

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

在生成的C++代码中找到__main_block_impl_0结构体,发现是一个弱引用:

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

总结一下:

  • Block在栈上,无论 ARCMRC环境,block内部都不会对外部对象类型的auto变量产生强引用,就算Block内部生成强指针,也不会对外部person产生强引用,因为Block自己就在栈上,随时可能被销毁;
  • Block在堆上:
    ARC环境下,访问外部对象类型的auto变量,编译后:
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  DJTPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, DJTPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_desc_0结构体中,多了两个函数:__main_block_copy_0__main_block_dispose_0

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->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

当对Block进行copy操作时,会调用这个__main_block_copy_0函数,在它内部调用_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/),会对外部person对象产生强引用或者弱引用,这取决于block内部使用__strong指针还是__weak指针访问。

Block从堆上移除,会调用__main_block_dispose_0函数,它内部调用_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);,会对外部person对象进行一次release操作。

MRC环境下,也是由这两个函数决定是否进行retainrelease操作。

五、__block修改变量及其本质

我们先看下面一段代码:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        DJTBlock block = ^{
            age = 20; // 报错:Variable is not assignable (missing __block type specifier)
            NSLog(@"-----------%d",age);
        };   
    }
    return 0;
}

ARC下直接在Block内部修改age会报错,这就相当于在block生成的结构体中FuncPtr指向的函数中去修改main函数中的局部变量(如果这里agestatic或者全局变量,可以修改,因为这两种变量一直在内存中),上下文环境发生了改变,所以不能直接访问age;我们使用__block修饰age变量,然后编译成C++

typedef void (^DJTBlock)(void);

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

会生成下面结构体:

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

可以看到在Block结构体中多了__Block_byref_age_0 *age;,看一下 __Block_byref_age_0发现它也是一个结构体:

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

这里看出编译器将 __block修饰的变量(这里是age)包装成一个对象__Block_byref_age_0(因为它内部有isa指针,所以可以认为它是个对象),Block内部(__main_block_impl_0结构体中)并不会直接拥有这个变量age,而是拥有__Block_byref_age_0这个结构体,然后__Block_byref_age_0结构体中有一个int age变量,我们在Block内部改变age = 20,实际上就是赋值给__Block_byref_age_0结构体中的age变量。

我们对__block int age = 10转化成的C++代码进行简化:

// __block int age = 10;对应下面c++代码:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

// 进行简化
__Block_byref_age_0 age = {
     0,
     &age,
     0,
     sizeof(__Block_byref_age_0),
     10    
 };

对应到下面结构体初始化:

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

__forwarding指针传入&age指向__Block_byref_age_0 age结构体自己(这里&age是结构体地址,不要混淆),10赋值给了__Block_byref_age_0结构体内部的age变量;我们再看下修改age为20的代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
 // 这里是 age = 20;
  (age->__forwarding->age) = 20;
   NSLog((NSString *)&__NSConstantStringImpl__var_folders__h_yv1h9mrx1155q6tq7brn8b9m0000gp_T_main_05a705_mi_0,(age->__forwarding->age));
}

可以发现先通过 __cself->age找到__Block_byref_age_0结构体,然后(age->__forwarding->age) = 20;通过__forwarding指针修改结构体内部的age变量,__forwarding指向结构体自己,那为什么要多此一举通过__forwarding指针去修改内部age,而不通过结构体指针直接去修改呢?这是为了保证Blockcopy到堆上时,不管访问栈上还是堆上Block,通过forwarding指针都是找到堆上。

这里如果__block修饰的是一个对象类型,比如下面代码:

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        __block NSObject *obj = [[NSObject alloc] init];
        DJTBlock block = ^{
            obj = nil;
            age = 20;
        };
    }
    return 0;
}

转换为C++同样会多生成一个对应的结构体,只不过内部会多出两个方法copy``和dispose方法来负责相应的内存管理:

// __block age 对应的结构体
struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

// __block NSObject 对应的结构体
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*);
 NSObject *__strong obj;
};

下面看一个例子:

typedef void (^DJTBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *mutarray = [NSMutableArray array];
        DJTBlock block = ^{
            [mutarray addObject:@(12)];
            [mutarray addObject:@(13)];
        };
    }
    return 0;
}

这里不会报错,是因为我们并没有修改mutarray指针,而是在使用mutarray指针,除非我们修改mutarray指针的值,比如 mutarray = nil;才需要__block来修饰;

六、__block的内存管理

我们知道,当Block内部访问外部对象类型的变量时,如下面简单代码:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
        DJTBlock block = ^{
            NSLog(@"%p", object);
        };
        block();
    }
    return 0;
}

block编译成C++后的结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong object; //内部强引用外部变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, int flags=0) : object(_object) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到在block结构体内部会生成一个强指针指向外边的object对象,并且在block被拷贝到堆上时,调用__main_block_desc_0中的copy函数,对这个指针指向的对象进行一次retain操作,即引用计数+1,当然如果用__weak修饰object会生NSObject *__weak object;此时不会强引用;
那当我们用__block修饰变量时,比如分别修饰基础数据类型age,和对象类型obj1,如下代码:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        __block int age = 10;
        __block NSObject *obj1 = [[NSObject alloc] init];
        NSObject *object = [[NSObject alloc] init];
        DJTBlock block = ^{
            NSLog(@"%d %p %p", age,obj1, object);
        };
        block();
    }
    return 0;
}

编译成C++:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};
struct __Block_byref_obj1_1 {
  void *__isa;
__Block_byref_obj1_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__strong obj1;
};

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

__block修饰的变量ageobj1分别生成构体__Block_byref_age_0__Block_byref_obj1_1,它们的本质就是OC对象,所以在block对应的结构体内部生成两个结构体指针指向这两个对象,即

__Block_byref_age_0 *age; // by ref
__Block_byref_obj1_1 *obj1; // by ref

它们其实和object一样,因为__block修饰的变量也是转换成结构体,而且内部有isa指针,其实就是OC对象,所以也会在__main_block_desc_0中生成两个函数:copydispose,来管理对象的内存,可以看下结构:

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->obj1, (void*)src->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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->obj1, 8/*BLOCK_FIELD_IS_BYREF*/);
  _Block_object_dispose((void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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

所以__block内存管理可以总结一下:

  • Block在栈上时,内部并不会对__block修饰的外部变量产生强引用

  • Blockcopy到堆上时,会调用Block内部的copy函数,而copy函数内部会调用_Block_object_assign函数,它内部会对__block变量形成强引用(retain)。


  • Block从堆上移除时,会调用Block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,在_Block_object_dispose函数中会对引用的__block变量进行引用计数-1release


下面我们对比下Block内部访问外部变量几种情况:

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
         //直接将20存放在Block生成的结构体中
        int num = 20; 
        //Block结构体内部生成一个强指针 强引用object对象
        NSObject *object = [[NSObject alloc] init]; 
        // Block内部生成一个弱指针 弱引用object对象
        __weak NSObject *weakObject = object; 
        // Block内部生成一个结构体指针,指针指向的结构体内部存储着变量age
        __block int age = 10; 
        //Block内部生成一个结构体指针,指针指向的结构体内部存储着变量obj1
        __block NSObject *obj1 = [[NSObject alloc] init];
        
        DJTBlock block = ^{
            NSLog(@"%d %d %p %p %p",num, age, obj1, object, weakObject);
        };
        block();
    }
    return 0;
}

编译成C++看看block结构体,和上边注释的一致:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int num;
  NSObject *__strong object;
  NSObject *__weak weakObject;
  __Block_byref_age_0 *age; // by ref
  __Block_byref_obj1_1 *obj1; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, NSObject *__strong _object, NSObject *__weak _weakObject, __Block_byref_age_0 *_age, __Block_byref_obj1_1 *_obj1, int flags=0) : num(_num), object(_object), weakObject(_weakObject), age(_age->__forwarding), obj1(_obj1->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

这里主要对比一下 对象类型的auto变量和__block修饰的变量内存管理的区别:

  • 相同点:
    • Block在栈上时,对它们都不会产生强引用
    • Block拷贝到堆上时,都会通过copy函数来处理它们:
      (1)__block变量(假设变量名叫做a
      _Block_object_assign((void*)&dst->a, (void*)src->a,   8/*BLOCK_FIELD_IS_BYREF*/);
      
      (2)对象类型的auto变量(假设变量名叫做p
      _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
      
    • Block从堆上移除时,都会通过dispose函数来释放它们
      (1)__block变量(假设变量名叫做a
      _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*
      
      (2)对象类型的auto变量(假设变量名叫做p
      _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
      
  • 不同点(主要是在引用的问题上)
    (1)对象类型的auto变量,根据传进来时是__strong还是__weak类型决定调用copy函数时Block内部对传进来的变量进行强还是弱引用。
    (2)如果时__block类型的变量,比如__block int age = 20;,它被封装成一个OC对象,调用copy函数时Block内部直接对它产生强引用,对它的内存进行管理,不存在__weak修饰int age这种操作,所以没有弱引用这一说。(这里强引用的是age转换成的结构体对象,真正的age变量的值存储在结构体里边);

但是如果是下面代码

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        NSObject *object = [[NSObject alloc] init];
        __block __weak NSObject *weakObject = object;
        DJTBlock block = ^{
            NSLog(@"%p %p", object, weakObject);
        };
        block();
    }
    return 0;
}

编译成C++:

struct __Block_byref_weakObject_0 {
  void *__isa;
__Block_byref_weakObject_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__weak weakObject;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong object;
  __Block_byref_weakObject_0 *weakObject; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _object, __Block_byref_weakObject_0 *_weakObject, int flags=0) : object(_object), weakObject(_weakObject->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到在__block修饰变量生成的结构体 __Block_byref_weakObject_0内部,通过__weak弱引用变量weakObject,即Block结构体内部是一个强指针指向__block生成的结构体,即这句代码

__Block_byref_weakObject_0 *weakObject;
(注意虽然名字中有`weak`但这是一个强指针)

而在结构体__Block_byref_weakObject_0内部:

NSObject *__weak weakObject;

这才是一个弱指针,指向外部传入的弱引用对象weakObject,它表达了外部传入变量的类型是__weak还是__strong

注意:这里在MRC下有个特殊情况,在__block生成的结构体内部,始终都是弱引用,不会对外边对象进行强引用。


MRC环境下验证, 下面代码在block();调用前person就已经挂了,说明确实内部没有强引用:

#import "DJTPerson.h"

typedef void (^DJTBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        __block DJTPerson *person = [[DJTPerson alloc] init];
        DJTBlock block = [^{
            NSLog(@" %p", person);
        } copy];
        [person release];
        block();
    }
    return 0;
}

七、Block相关问题

  • Block的原理是怎样的?本质是什么?
  • __block的作用是什么?有什么使用注意点?
  • Block的属性修饰词为什么是copy?使用Block有哪些使用注意?
  • Block在修改NSMutableArray,需不需要添加__block

理解上边原理再回答这些问题应该不难吧。

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

推荐阅读更多精彩内容