美文网首页iOS
iOS-底层原理28:block底层原理

iOS-底层原理28:block底层原理

作者: AcmenL | 来源:发表于2021-04-06 12:19 被阅读0次

本文主要介绍:
1、block的本质
2、block捕获变量
3、block的类型
4、__block原理

本质

通过clang分析Block底层

step1: 定义block.c文件

#include "stdio.h"

int main(){

    void(^block)(void) = ^{
        printf("lbh");
    };
    return 0;
}

step2: 通过xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c,将block.c 编译成 block.cpp,其中block在底层被编译成了以下的形式

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 void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("lbh");
}

//******简化******
//__main_block_impl_0 是构造函数,在结构体中
//参数__main_block_func_0 是闭包中具体实现
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));//构造函数

block->FuncPtr(block);//block调用执行

相当于block等于__main_block_impl_0,是一个函数,第一个参数是__main_block_func_0,它是个函数,代码块的实现函数。

step3: 查看__main_block_impl_0,是一个结构体

//**block代码块的结构体类型**
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;
  }
};

//**block的结构体类型**
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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

struct __main_block_impl_0包含一个struct __block_impl类型的impl和一个struct __main_block_desc_0*类型的Desc
struct __block_impl包含一个isa指针,说明block其实是一个对象。
static struct __main_block_desc_0中包含一个Block_size表示block占用内存空间

构造函数__main_block_impl_0将第一个参数__main_block_func_0传给了FuncPtr,所以FuncPtr指向block具体实现函数的地址。

block通过clang编译后的源码间的关系如下所示,以__block修饰的变量为例

总结block本质上也是一个oc对象,他内部也有一个isa指针block是封装了函数调用以及函数调用环境的OC对象。

捕获基本数据类型

捕获局部变量--auto

定义一个变量,并在block中调用

int main(){

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

输出结果

lbh--10

底层编译成如下

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) {
// : a(_a)  c++语法  会自动将_a赋值给a   a = _a
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//代码块放在函数中
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy  值拷贝

        printf("lbh--%d", a);
}

int main(){

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

    a = 20;

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}

在构造函数创建block时,传入的第三个参数是a,然后将a存入到结构体__main_block_impl_0里的成员变量a中,调用block时,直接从结构体中取出变量a的值 所以局部自动变量是值拷贝, 如果此时在block内部实现中作 a++操作,是有问题的,会造成编译器的代码歧义,即此时的a是只读的

捕获局部变量--static

int main(int argc, char * argv[]) {
    
      static int a = 10;
       void(^block)(void) = ^{
           printf("lbh--%d", a);
       };
       
       a = 20;
       
       block();
    
    return 0;
}

输出结果

lbh--20

底层编译如下

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;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *a = __cself->a; // bound by copy

        printf("lbh--%d", (*a));
    }

int main(){

    static int a = 10;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));

    a = 20;

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}

在通过构造函数创建block时,第三个参数传的是&a,即变量a的地址,然后将&a 存入结构体__main_block_impl_0中的成员变量*a中,调用block时,从结构体中取出a,因为存入的是a的地址所以是指针拷贝。

全局变量

static int a= 10;
int main(int argc, char * argv[]) {
    
       void(^block)(void) = ^{
           printf("lbh--%d", a);
       };
       
       a = 20;
       
       block();
    
    return 0;
}

输出结果

lbh--20

底层编译

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("lbh--%d", a);
}

int main(){

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

    a = 20;

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}

在通过构造函数创建block时,并没有以参数的形式将a传进去,在结构体__main_block_impl_0中也没有生成新的成员变量,在调用时直接访问变量a

总结

局部变量都会被block捕获,自动变量是值捕获,静态变量为地址捕获。全局变量则不会被block捕获

问题: 为什么全局变量不需要捕获,而局部变量需要捕获?

解答: 因为全局变量都可以访问,捕获是多此一举,局部变量因为作用域的问题需要捕获,请看下面的例子

void (^block) (void);

void test()
{
    auto int a = 10;
    static int b = 20;
    
    block = ^{
        NSLog(@" a = %d b = %d", a,b);
    };

}

//static int a= 10;
int main(int argc, char * argv[]) {
    
    test();
    block();
    
    return 0;
}

ab的作用域是在test函数中,而block代码块封装在另一个函数中,block调用时调用的是这个封装的函数,超出了ab的作用域,如果不进行变量捕获,会出现访问异常。

问题:下面代码中self会不会被捕获? 为什么?

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

看底层编译

struct __LBHPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __LBHPerson__test_block_desc_0* Desc;
  LBHPerson *self;
  __LBHPerson__test_block_impl_0(void *fp, struct __LBHPerson__test_block_desc_0 *desc, LBHPerson *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void _I_LBHPerson_test(LBHPerson * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__LBHPerson__test_block_impl_0((void *)__LBHPerson__test_block_func_0, &__LBHPerson__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

__LBHPerson__test_block_impl_0结构体中增加了一个成员变量LBHPerson *self;,所以self是会被捕获的,因为oc函数默认是会有两个参数(LBHPerson * self, SEL _cmd),而参数是局部变量,局部变量是会被捕获的。

问题: 如果在block中使用成员变量或者调用实例的属性会有什么不同的结果

@interface LBHPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LBHPerson
- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"%@",self.name);
        NSLog(@"%@",_name);
    };
    block();
}
@end

通过xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc LBHPerson.m 编译成底层代码

struct __LBHPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __LBHPerson__test_block_desc_0* Desc;
  LBHPerson *self;
  __LBHPerson__test_block_impl_0(void *fp, struct __LBHPerson__test_block_desc_0 *desc, LBHPerson *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __LBHPerson__test_block_func_0(struct __LBHPerson__test_block_impl_0 *__cself) {
  LBHPerson *self = __cself->self; // bound by copy

//通过调用self的getter方法获取属性值
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f9___44hp612k7gxwp82fznnkpm0000gn_T_LBHPerson_03f602_mi_0,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
//通过self从其ivars中找到成员变量
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f9___44hp612k7gxwp82fznnkpm0000gn_T_LBHPerson_03f602_mi_1,(*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_LBHPerson$_name)));
    }

__LBHPerson__test_block_impl_0结构体中只捕获了LBHPerson *self;,在由代码块中封装的函数中,是通过self间接获取的。

block 类型

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

注意:在不同版本的xcode上可能会有差异,但最终都继承于NSObjcet

从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象

block的3种类型

通过代码查看一下block在什么情况下其类型会各不相同

int c = 10;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 1. 内部没有调用外部变量的block
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        // 2. 内部调用auto变量的block
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d",a);
        };
        
        // 3. 内部调用局部static变量的block
        //static int b = 10;
        //void (^block3)(void) = ^{
        //    NSLog(@"Hello - %d",b);
        //};
        
        // 4. 内部调用全局变量的block
        //void (^block4)(void) = ^{
        //    NSLog(@"Hello - %d",c);
        //};
        
        // 5. 直接调用的block的class
        NSLog(@"%@ %@ %@ %@ %@", [block1 class], [block2 class],[^{
            NSLog(@"%d",a);
        } class]);
        
    }
    return 0;
}

输出结果

__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

block可以分为三种各类型

__NSGlobalBlock__ ( _NSConcreteGlobalBlock ) //全局block
__NSStackBlock__ ( _NSConcreteStackBlock )    //栈区block
__NSMallocBlock__ ( _NSConcreteMallocBlock ) //堆区block

看下通过clang编译结果

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

struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  int a;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __main_block_impl_2 {
  struct __block_impl impl;
  struct __main_block_desc_2* Desc;
  int a;
  __main_block_impl_2(void *fp, struct __main_block_desc_2 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

在底层C++代码中,blockimpl.isa = &_NSConcreteStackBlock;,三个block的isa指针全部都指向_NSConcreteStackBlock类型地址。

问题: 为什么底层编译结果和打印结果不一致

runtime运行时过程中也许对类型进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。

block在内存中的存储

通过下面一张图看一下不同block的存放区域

数据段中的NSGlobalBlock直到程序结束才会被回收,不过我们很少使用到NSGlobalBlock类型的block,因为这样使用block并没有什么意义。

NSStackBlock类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放,而在相同的作用域中定义block并且调用block似乎也多此一举。

NSMallocBlock是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

block是如何定义其类型

block是如何定义其类型,依据什么来为block定义不同的类型并分配在不同的空间呢?首先看下面一张图

1、没有访问auto变量是全局block
2、访问了auto变量是栈区block
3、栈区block调用copy变成堆区block

注意:这是对MRC环境得出的结论,ARC打印结果会有差别,因为ARC内部会做很多事情

先关闭ARC环境,看下这个结论是否正确

// MRC环境!!!
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // Global:没有访问auto变量:__NSGlobalBlock__
        void (^block1)(void) = ^{
            NSLog(@"block1---------");
        };
        // Stack:访问了auto变量: __NSStackBlock__
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"block2---------%d", a);
        };
        
        // Global:访问局部static变量:__NSGlobalBlock__
        static int b = 10;
        void (^block3)(void) = ^{
            NSLog(@"block2---------%d", b);
        };
        
        NSLog(@"%@ %@ %@ %@", [block1 class], [block2 class],[block3 class],[[block2 copy] class]);

    }
    return 0;
}

输出结果

  • 没有访问auto变量的block是__NSGlobalBlock__类型的,存放在数据段中。
  • 访问了auto变量的block是__NSStackBlock__类型的,存放在栈中。
  • __NSStackBlock__类型的block调用copy成为__NSMallocBlock__类型并被复制存放在堆中。

NSGlobalBlock类型的我们很少使用到,因为如果不需要访问外界的变量,直接通过函数实现就可以了,不需要使用block。

但是__NSStackBlock__访问了auto变量,并且是存放在栈中的,上面提到过,栈中的代码在作用域结束之后内存就会被销毁,那么我们很有可能block内存销毁之后才去调用他,那样就会发生问题,通过下面代码可以证实这个问题。

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

输出结果

问题: 可以发现a的值变为了不可控的一个数字。为什么会发生这种情况呢?

因为上述代码中创建的block是__NSStackBlock__类型的,因此block是存储在栈中的,那么当test函数执行完毕之后,栈内存中block所占用的内存已经被系统回收,因此就有可能出现乱得数据。查看其c++代码可以更清楚的理解。

为了避免这种情况发生,可以通过copy将NSStackBlock类型的block转化为NSMallocBlock类型的block,将block存储在堆中,以下是修改后的代码

//MRC
void (^block)(void);
void test()
{
    // __NSStackBlock__ 调用copy 转化为__NSMallocBlock__
    int age = 10;
    block = [^{
        NSLog(@"block---------%d", age);
    } copy];
    [block release];
}

看下此时的输出结果

那么其他类型的block调用copy会改变block类型吗?

所以在平时开发过程中MRC环境下经常需要使用copy来保存block,将栈上的block拷贝到堆中,即使栈上的block被销毁,堆上的block也不会被销毁,需要我们自己调用release操作来销毁。而在ARC环境下系统会自动调用copy操作,使block不会被销毁。

ARC帮我们做了什么

在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上。

将上面的例子放在ARC环境中,看下是否会被copy到堆区

//ARC
void (^block)(void);
void test()
{
    // __NSStackBlock__
    int a = 10;
    block = ^{
        NSLog(@"block---------%d", a);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
        NSLog(@"== %@",[block class]);
    }
    return 0;
}

输出结果

ARC下block被copy到堆区

block对对象变量的捕获

block一般使用过程中都是对对象变量的捕获,那么对象变量的捕获同基本数据类型变量相同吗?

//ARC

@interface LBHPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation LBHPerson
- (void)dealloc
{
    NSLog(@"%s",__func__);
}
@end

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            LBHPerson *person = [[LBHPerson alloc] init];
            person.name = @"liu";
            
            block = ^{
                NSLog(@"------block内部%@",person.name);
            };
        } // 执行完毕,person没有被释放
        NSLog(@"--------");
    } // person 释放
    return 0; 
}

运行程序

person是小括号(116-123行)中的一个auto变量,按理说它的生命周期超出这个括号时就已经结束,但是在124行的断点处,person的dealloc方法并没有执行,继续运行

运行到断点126行,发现person调用了dealloc方法,此时person才被销毁,由于personauto变量,block代码块中有使用了person,所以block会捕获person,即block对person有一个强引用,所以block不被销毁的话,peroson也不会销毁。

查看源代码

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

将上述代码转移到MRC环境下,在MRC环境下即使block还在,person却被释放掉了。因为MRC环境下block在栈空间,栈空间对外面的person不会进行强引用。

//MRC环境下代码
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            LBHPerson *person = [[LBHPerson alloc] init];
            person.name = @"liu";
            
            block = ^{
                NSLog(@"------block内部%@",person.name);
            };
            
            [person release];
        } // person被释放
        NSLog(@"--------");
    }
    return 0;
}

运行结果

在126行的断点处person调用了dealloc,被销毁了。

block调用copy操作之后,person不会被释放。

block = [^{
   NSLog(@"------block内部%d",person.age);
} copy];

前面提到过,只需要对栈空间的block进行一次copy操作,将栈空间的block拷贝到堆中person就不会被释放,说明堆空间的block可能会对person进行一次retain操作,以保证person不会被销毁。堆空间的block自己销毁之后也会对持有的对象进行release操作。

栈空间上的block不会对对象强引用,堆空间的block有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操作

__weak

__weak添加之后,person在作用域执行完毕之后就被销毁了

typedef void (^Block)(void);
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            LBHPerson *person = [[LBHPerson alloc] init];
            person.name = @"liu";
            
            __weak LBHPerson *waekPerson = person;
            block = ^{
                NSLog(@"------block内部%@",waekPerson.name);
            };
        }
        NSLog(@"--------");
    }
    return 0;
}

通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m编译成底层源码

注意: __weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错

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

__main_block_impl_0中也是用__weak去修饰的

__main_block_copy_0 和 __main_block_dispose_0

当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copydispose函数,查看源码

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

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

copydispose函数中传入的都是__main_block_impl_0结构体本身

copy本质就是__main_block_copy_0函数__main_block_copy_0函数内部调用_Block_object_assign函数

dispose本质就是__main_block_dispose_0函数__main_block_dispose_0函数内部调用_Block_object_dispose函数

_Block_object_assign函数调用时机及作用

block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。

_Block_object_assign函数会自动根据__main_block_impl_0结构体内部的person是什么类型的指针对person对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对person进行引用计数器的操作,如果__main_block_impl_0结构体内person指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内person指针是__weak类型,则为弱引用,引用计数不变。

_Block_object_dispose函数调用时机及作用

block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。

_Block_object_dispose会对person对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

后续继续补充

Block循环引用

  • 正常释放:是指A持有B的引用,当A调用dealloc方法,给B发送release信号,B收到release信号,如果此时B的retainCount(即引用计数)为0时,则调用B的dealloc方法

  • 循环引用:A、B相互持有,所以导致A无法调用dealloc方法给B发送release信号,而B也无法接收到release信号。所以A、B此时都无法释放

如下图所示:

相关例子

例1
//代码一
NSString *name = @"LBH";
self.block = ^(void){
    NSLog(@"%@",self.name);
};
self.block();

//代码二
UIView animateWithDuration:1 animations:^{
    NSLog(@"%@",self.name);
};

问题: 上述两段代码是否出现循环引用?

解答:
代码一种发生了循环引用,因为在block内部使用了外部变量name,导致block持有了self,而self原本是持有block的,所以导致了self和block的相互持有
代码二中无循环引用,虽然也使用了外部变量,但是self并没有持有animation的block,仅仅只有animation持有self,不构成相互持有

例2

新建一个页面B,它是从页面A push过来的

//类扩展
typedef void(^LBHBlock)(void);
@interface ViewController ()
@property (nonatomic, copy) LBHBlock lbhblock;
@property (nonatomic, copy) NSString *name;
@end

//实现
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 循环引用
    self.name = @"lbh";

    [self test1];    
}

- (void)test1
{
   // __weak typeof(self) weakSelf = self;
    
    self.lbhblock = ^{
        NSLog(@"%@",self.name);
    };
}

- (void)dealloc{
    NSLog(@"dealloc 来了");
}

@end

从当前的页面B 返回到页面A ,页面B的dealloc并没有执行,由于循环引用导致页面无法释放

解决循环引用

解决循环引用常见的方式有以下几种:

1、weak-strong-dance
2、__block修饰对象(需要注意的是在block内部需要置空对象,而且block必须调用
3、传递对象self作为block的参数,提供给block内部使用
4、使用NSProxy

weak-strong-dance

  • 如果block内部并未嵌套block,直接使用__weak修饰self即可
- (void)test1
{
    __weak typeof(self) weakSelf = self;
    
    self.lbhblock = ^{
        NSLog(@"%@",weakSelf.name);
    };
}

此时的weakSelf 和 self 指向同一片内存空间,且使用__weak不会导致self的引用计数发生变化,可以通过打印weakSelf和self的指针地址,以及self的引用计数来验证

- (void)test2
{
    NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));
     __weak typeof(self) weakSelf = self;
     NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));
     
     self.lbhblock = ^{
         NSLog(@"%@",weakSelf.name);
     };
     NSLog(@"%p %p",weakSelf, self);
     NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self.lbhblock)));
     NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));
}
  • 如果block内部嵌套block,需要同时使用__weak 和 __strong
- (void)test3
{
    __weak typeof(self) weakSelf = self;
    
    self.lbhblock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%s %@",__func__,strongSelf.name);
        });
    };
    
    self.lbhblock();
}

运行结果

【注意】:实际上嵌套的block内部使用weakSelf并不一定会出现问题,不过为了程序的严谨通常还是会使用strongSelf

__block修饰变量

这种方式同样依赖于中介者模式,属于手动释放,是通过__block修饰对象,主要是因为__block修饰的对象是可以改变的

- (void)test4
{
    __block ViewController *vc = self;
    
    self.lbhblock = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%s %@",__func__,vc.name);
            vc = nil;
        });
    };
    
    self.lbhblock();
}

运行结果

【注意】 这里的block必须调用,如果不调用block,vc就不会置空,那么依旧是循环引用,self和block都不会被释放

对象self作为参数

主要是将对象self作为参数,提供给block内部使用,不会有引用计数问题

//声明一个新的block
typedef void(^LBH2Block)(ViewController *);

@property (nonatomic, copy) LBH2Block lbh2block;

- (void)test5
{
    self.lbh2block = ^(ViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%s %@",__func__,vc.name);
        });
    };
    self.lbh2block(self);
}

运行结果

NSProxy 虚拟类

  • OC是只能单继承的语言,但是它是基于运行时的机制,所以可以通过NSProxy来实现 伪多继承,填补了多继承的空白

  • NSProxyNSObject是同级的一个类,也可以说是一个虚拟类,只是实现了NSObject的协议

  • NSProxy 其实是一个消息重定向封装的一个抽象类,类似一个代理人,中间件,可以通过继承它,并重写下面两个方法来实现消息转发到另一个实例

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
使用场景

NSProxy的使用场景主要有两种:

  • 实现多继承功能
  • 解决了NSTimer&CADisplayLink创建时对self强引用问题,参考YYKitYYWeakProxy
循环引用解决原理

主要是通过自定义的NSProxy类的对象来代替self,并使用方法实现消息转发

下面是NSProxy子类的实现以及使用的场景

step1 自定义一个NSProxy的子类LBHProxy

@interface LBHProxy : NSProxy

- (id)transformObjc:(NSObject *)objc;

+ (instancetype)proxyWithObjc:(id)objc;

@end

@interface LBHProxy ()

@property(nonatomic, weak, readonly) NSObject *objc;

@end

@implementation LBHProxy

- (id)transformObjc:(NSObject *)objc{
   _objc = objc;
    return self;
}

+ (instancetype)proxyWithObjc:(id)objc{
    return  [[self alloc] transformObjc:objc];
}



//2.有了方法签名之后就会调用方法实现
- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.objc respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.objc];
    }
}

//1、查询该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    NSMethodSignature *signature;
    if (self.objc) {
        signature = [self.objc methodSignatureForSelector:sel];
    }else{
        signature = [super methodSignatureForSelector:sel];
    }
    return signature;
}

- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.objc respondsToSelector:aSelector];
}


@end

step2: 自定义Cat类和Dog类

//********Cat类********
@interface Cat : NSObject
@end

@implementation Cat
- (void)eat{
   NSLog(@"猫吃鱼");
}
@end

//********Dog类********
@interface Dog : NSObject
@end

@implementation Dog
- (void)shut{
    NSLog(@"狗叫");
}
@end

step3: 通过LBHProxy实现多继承功能

- (void)lbh_proxyTest{
    Dog *dog = [[Dog alloc] init];
    Cat *cat = [[Cat alloc] init];
    LBHProxy *proxy = [LBHProxy alloc];
    
    [proxy transformObjc:cat];
    [proxy performSelector:@selector(eat)];
    
    [proxy transformObjc:dog];
    [proxy performSelector:@selector(shut)];
}

通过LBHProxy解决定时器中self的强引用问题

self.timer = [NSTimer timerWithTimeInterval:1 target:[LBHProxy proxyWithObjc:self] selector:@selector(print) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

总结

循环应用的解决方式从根本上来说就两种,以self -> block -> self为例

  • 打破self 对 block的强引用,可以block属性修饰符使用weak,但是这样会导致block还没创建完就释放了,所以从这里打破强引用行不通

  • 打破block对self的强引用,主要就是self的作用域和block作用域的通讯,通讯有代理、传值、通知、传参等几种方式,用于解决循环,常见的解决方式如下:

    • weak-strong-dance

    • __block(block内对象置空,且调用block)

    • 将对象self作为block的参数

    • 通过NSProxy的子类代替self

问题: block为什么需要调用?

解答:

block在底层是类型为__main_block_impl_0结构体,通过其同名构造函数创建,第一个传入的block的内部实现代码块,即__main_block_func_0,用fp表示,然后赋值给impl的FuncPtr属性,然后在main中进行了调用,这也是block为什么需要调用的原因。如果不调用,block内部实现的代码块将无法执行,可以总结为以下两点

  • 函数声明:即block内部实现声明成了一个函数__main_block_func_0
  • 执行具体的函数实现:通过调用block的FuncPtr指针,调用block执行

4.3 __block的原理

对a加一个__block,然后在block中对a进行++操作

int main(){

    __block int a = 11;
    void(^block)(void) = ^{
        a++;
        printf("%d", a);
    };
    
     block();
    return 0;
}

底层编译成如下

struct __Block_byref_a_0 {//__block修饰的外界变量的结构体
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {//block的结构体类型
  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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {//block内部实现
  __Block_byref_a_0 *a = __cself->a; // bound by ref 指针拷贝,此时的对象a 与 __cself对象的a 指向同一片地址空间
        //等同于 外界的 a++
        (a->__forwarding->a)++;
        printf("%d", (a->__forwarding->a));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

int main(){
    //__Block_byref_a_0 是结构体,a 等于 结构体的赋值,即将外界变量a 封装成对象
    //&a 是外界变量a的地址
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 11};
    //__main_block_impl_0中的第三个参数&a,是封装的对象a的地址
    void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

     ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}
  • main中的a是通过外界变量封装的对象

  • __main_block_impl_0中,将对象a的地址&a给构造函数

  • __main_block_func_0内部对a的处理是指针拷贝,此时创建的对象a与传入对象的a指向同一片内存空间

总结:
  • 外界变量会生成__Block_byref_a_0结构体,结构体用来保存原始变量的指针和值

  • 将变量生成的结构体对象的指针地址传递给block,然后在block内部就可以对外界变量进行操作了

两种拷贝对比如下

  • 值拷贝 - 深拷贝,只是拷贝数值,且拷贝的值不可更改,指向不同的内存空间,案例中普通变量a就是值拷贝

  • 指针拷贝 - 浅拷贝,生成的对象指向同一片内存空间,案例中经过__block修饰的变量a就是指针拷贝

相关文章

网友评论

    本文标题:iOS-底层原理28:block底层原理

    本文链接:https://www.haomeiwen.com/subject/daqskltx.html