iOS-block2-copy操作、对象类型的auto变量、__block

上篇文章只是简单讲了MRC环境下block的copy操作。

一. ARC环境下,block的copy操作

接下来我们讲的都是在ARC环境下。

观察如下代码:

typedef void (^MJBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        //block变量强引用着右边的block
        MJBlock block = ^{
            NSLog(@"---------%d", age);
        };

        block();
        NSLog(@"%@", [block class]);
    }
    return 0;
}

打印:

---------10
__NSMallocBlock__

上文我们说过如果block访问了atuo变量就是__NSStackBlock__,存放在栈区,栈区的内存系统自动管理,那么在{}结束后block就被销毁了,这时候再访问block就是很危险的事,上面block也没有进行copy操作,但是现在为什么可以打印呢?

这是因为我们现在在ARC环境下,并且将block赋值给强指针指着了,编译器帮我们做了copy操作,将栈上的block复制到堆上,所以上面的打印才是__NSMallocBlock__类型。

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时

前两种情况比较好理解,就不解释了,后面两种情况看如下代码:

NSArray *arr = @[];
[arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    //block作为Cocoa API中方法名含有usingBlock的方法参数时
}];

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    //block作为GCD API的方法参数时
});

二. 对象类型的auto变量

先看几个例子:

如下代码:

  • MRC 不copy
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MJBlock block;

        {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;

            block = ^{
                NSLog(@"---------%d", person.age);
            };
            
            //如果是MRC person离开{}之前要进行release
            [person release];
        }

        NSLog(@"------"); //此处打断点,block还在,person被销毁了
    }
    return 0;
}

打印: MJPerson - dealloc ,person被释放。可以发现block还在,但是离开{}之后person就被释放掉了

  • MRC copy
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        MJBlock block;

        {
            MJPerson *person = [[MJPerson alloc] init];
            person.age = 10;

            block = [^{
                NSLog(@"---------%d", person.age);
            } copy];
            
            //如果是MRC person离开{}之前要进行release
            [person release];
        }

        NSLog(@"------");//打断点
    }
    return 0;
}

没打印,person没被释放。所以我们猜想在MRC环境下,copy操作之后,block内部对person做了[person retain]操作,所以person没被销毁。

  • ARC环境
typedef void (^MJBlock)(void);

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

ARC环境下,在NSLog处打断点,发现执行到NSLog,person对象没有调用dealloc方法,person没被释放。
这是因为:上面的block捕获了auto变量(MJPerson *person,ARC环境下默认是强引用的,如下所示:)所以是NSStackBlock,在栈空间。又因为是ARC环境并且block有强指针指着,所以编译器把block自动copy了一下,变成了NSMallocBlock,在堆空间,堆空间的block就不会随便被销毁了,所以block会一直存在,又因为block内部又有捕获的person指针指向person对象,如下,所以走到断点的时候,person对象不会被释放。

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    MJPerson *__strong person; //ARC环境下,默认强引用
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__strong _person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

小总结:不管是MRC还是ARC,栈空间的block是不会保住捕获的变量的命,堆空间的block可以保住捕获的变量的命。

  • ARC __weak
typedef void (^MJBlock)(void);

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

打印: MJPerson - dealloc ,person被释放。
ARC环境下,使用__weak修饰,发现person又被释放了,相信看完上面的各种例子也有点懵了,下面进行大总结:

大总结:
无论MRC、ARC:
当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)

下面将代码转成C++代码,验证刚才的大总结:

typedef void (*MJBlock)(void);

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  MJPerson *__strong person;//捕获的变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MJPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    //函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
    _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) {
    //函数会自动释放引用的auto变量(release)
    _Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size; //block大小
  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};
//初始化传入了__main_block_copy_0函数和__main_block_dispose_0函数的地址

我们主要看__main_block_desc_0,可以发现当捕获的是个对象时,这个结构体就多了三、四两个成员,初始化的时候,第三个成员传入__main_block_copy_0函数的地址,第四个成员传入__main_block_dispose_0函数的地址。为什么当捕获的是个对象就会多着两个函数呢?这也比较容易理解,既然捕获了对象,就要有内存管理相关了,所以这两个函数就需要了。这两个函数的作用可看上面注释,验证了我们上面的大总结:

下面用几个小题目测试p什么时候释放。

  • 测试
  1. 案例一
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    MJPerson *p = [[MJPerson alloc] init];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"-------%@", p);
    });

    NSLog(@"touchesBegan:withEvent:");
}

打印如下:

2019-11-29 16:15:44.964850+0800 Interview03-测试[65454:6090594] touchesBegan:withEvent:
2019-11-29 16:15:47.965174+0800 Interview03-测试[65454:6090594] -------
2019-11-29 16:15:47.965517+0800 Interview03-测试[65454:6090594] MJPerson - dealloc

点击空白之后,发现p不是立马被释放,而是3秒之后被释放了。为什么呢?

因为ARC环境下dispatch_after会默认对block进行Copy操作,从栈区Copy到堆区的时候,block内部会调用_Block_object_assign,又因为p默认是强引用,所以_Block_object_assign函数会对p进行retain操作,所以3秒后block销毁的时候p才会销毁。

  1. 案例二
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    MJPerson *p = [[MJPerson alloc] init];

    __weak MJPerson *weakP = p;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1-------%@",p);

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-------%@", weakP);
        });
    });

    NSLog(@"touchesBegan:withEvent:");
}

person对象在1s后释放

2019-12-02 09:12:06 touchesBegan:withEvent:
2019-12-02 09:12:07 1-------
2019-12-02 09:12:07 MJPerson - dealloc
2019-12-02 09:12:09 2-------(null)

因为外面的block捕获了p,并且是强引用,所以p会在外面的block执行完毕释放,所以是1s后

  1. 案例三
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    MJPerson *p = [[MJPerson alloc] init];

    __weak MJPerson *weakP = p;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1-------%@", weakP);

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-------%@", p);
        });
    });

    NSLog(@"touchesBegan:withEvent:");
}

因为里面的block捕获了p,并且是强引用,所以p会在里面的block执行完毕再释放,所以是3s后。

person对象在3s后释放

2019-12-02 09:13:29 touchesBegan:withEvent:
2019-12-02 09:13:30 1-------
2019-12-02 09:13:32 2-------
2019-12-02 09:13:32 MJPerson - dealloc

三. __block修饰变量

先看一个小案例:

typedef void (^MJBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 20;

        MJBlock block2 = ^{
            age = 30;
            NSLog(@"age is %d", age);
        };

        block2();
    }
    return 0;
}

如上代码,运行会报错,Variable is not assignable (missing __block type specifier),意思是“变量不可赋值,缺少__block修饰”。

为什么不能改?
从上面我们分析C++代码可知,block里面的代码是在__main_block_func_0函数里面执行的,而age是定义在main函数里面的,两个函数的栈空间都不一样,肯定不能改。如果要改也只能改block结构体里面的age,但是main函数里面的age还是改不了啊。

那如何才能改?
① 使用static修饰:
上面的代码加static修饰“static int age = 20;”,发现可以修改,那为什么使用static修饰就可以改呢?还是查看C++代码:

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

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

            (*age) = 30;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_42e634_mi_0, (*age));
        }

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

        static int age = 20;

        MJBlock block2 = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));


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

    }
    return 0;
}

可以发现,结构体中使用了int *age来保存age的地址,在调用block的时候,内部执行__main_block_func_0函数,__main_block_func_0函数再访问age指针,再通过age指针将age值修改“int *age = __cself->age; (*age) = 30;”。
总结:指针传递,block内部可以修改外部成员变量的值。

② 使用全局变量
这个就更不用解释了,block结构体不会捕获全局变量,拿到全局变量直接改就是了。

有时候我们只是想临时改一下,并不想让变量一直在内存中,(如果使用static修饰变量会一直在内存中,全局变量也会一直在内存中),可以使用__block修饰。

③ 使用__block修饰

__block可以用于解决block内部无法修改auto变量值的问题
__block不能修饰全局变量、静态变量(static)(因为__block的作用就是上句)
编译器会将__block变量包装成一个对象

使用__block修饰“__block int age = 20;”就能在block内部修改外部变量的值,而且不会修改变量的性质(还是auto变量)。

那么为什么__block修饰修饰的可以修改呢?还是看C++代码

struct __Block_byref_age_0 {
  void *__isa; //isa指针(指向类对象)
__Block_byref_age_0 *__forwarding; //自己类型的指针,后面可知道是指向自己
 int __flags;
 int __size; //自己的大小
 int age; //age的值
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; //指向__Block_byref_age_0结构体的指针
  __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;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref

    //先通过age指针拿到__forwarding指针(里面存的就是自己),再通过__forwarding指针拿到自己里面的值,然后修改值为30
            (age->__forwarding->age) = 30;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_ad0be7_mi_0, (age->__forwarding->age));
        }

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

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

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

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        //__block int age = 10;
        __Block_byref_age_0 age = {
            (void*)0,
            (__Block_byref_age_0 *)&age, //自己的地址传给__forwarding(它内部有个指针指向自己)
            0,
            sizeof(__Block_byref_age_0),// 当前结构体多大
            10
        };
        
        //声明block会把__Block_byref_age_0地址传过去
        MJBlock block2 = ((void (*)())&__main_block_impl_0(
                                                           (void *)__main_block_func_0,
                                                           &__main_block_desc_0_DATA,
                                                           (__Block_byref_age_0 *)&age,
                                                           570425344));

        //调用block
        ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
    }
    return 0;
}

首先看__main_block_impl_0函数,定义block结构体的时候第三个参数是(__Block_byref_age_0 *)&age,这是指向__Block_byref_age_0结构体的指针,由于__Block_byref_age_0有个isa,我们可以认为它是个对象,里面保存了age的值,原来代码“__block int age = 10;”代码转成C++代码,就是如下__Block_byref_age_0结构体:

//__block int age = 10;
__Block_byref_age_0 age = {
    (void*)0, //isa 传0
    (__Block_byref_age_0 *)&age, //自己的地址传给__forwarding(它内部有个指针指向自己)
    0,
    sizeof(__Block_byref_age_0),// 当前结构体多大
    10 //age的值为10
};

结构体示意图,如下所示:

iOS-block2-copy操作、对象类型的auto变量、__block_第1张图片
__Block_byref_age_0

调用block的时候,block内部会调用__main_block_func_0函数,可以看出:

__Block_byref_age_0 *age = __cself->age; 
(age->__forwarding->age) = 30;

先通过age指针拿到__forwarding指针(里面存的就是自己),再通过__forwarding指针拿到自己里面的值,然后修改值为30。

总结:使用__block修饰age,会将age包装成__Block_byref_age_0结构体(对象),对象里面存着isa,对象的地址,对象的大小,age的值,然后通过对象里面的__forwarding指针拿到自己,再拿到自己的age值,进行修改。如果没修改外面的变量就不要加__block,因为又包装了一层对象,等用到的时候再加。

问题:执行下面代码会报错吗?

NSMutableArray *arr = [NSMutableArray array];
MJBlock block = ^{
    [arr addObject:@"123"];
};

回答:不会。因为“ [arr addObject:@"123"];”是使用arr而不是修改它的值(例如:arr = nil)。

Demo地址:block的copy操作和__block

你可能感兴趣的:(iOS-block2-copy操作、对象类型的auto变量、__block)