Objective-C:探索block(二)

Objective-C:探索block(一)中简单地说了下block的基本上面貌,包括它的语法和底层定义,了解block的底层定义对在项目开发中正确使用block极为重要。
本篇文章要探索block使用中不可避免的几个方面

  1. 基本类型变量与对象的截取
  2. __block修饰符
  3. block存储域
  4. copy的使用
  5. 相互引用问题

一. 基本数据类型变量与对象的截取

1.block截获基本数据类型变量

在这小节会解答:为什么给定义在代码体外面的局部变量重新赋值会引起编译错误
下面是block截取基本数据类型的示例代码:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int aInteger = 3;
        char *str = "hello world";
        void (^myprint)(void) = ^void(void){
            printf("%s-%d\n",str,aInteger);
        };
        myprint();
    }
    return 0;
}

注意:由于控制台标准输出有缓存,当有行刷新标志或者行缓存已满时,系统才会把缓存数据输出到控制台,Xcode8编译printf打印函数结束加上\n换行符,后台才有打印输出

示例代码中,代码块myprint的代码体中使用了变量aInteger,str,使用终端命令反编译:clang -rewrite-objc main.m, 可以在main.cpp文件的底部看到代码块myprint的底层定义,主要有:

//1.存储与block实现相关的信息
struct __block_impl {
  void *isa;  //指向block结构体实例所在的内存区域
  int Flags;  //系统默认值为0
  int Reserved; //构造方法里没看到赋值,用来存储block保留内存空间大小
  void *FuncPtr; //指向block代码体实现的函数指针,block的调用关键就它来寻址了
};
//2.此结构体记录block的描述信息,它在定义时顺便初始化了个实例__main_block_desc_0_DATA
static struct __main_block_desc_0 {
  size_t reserved; //指明block在内存中要保留一块内存空间的大小,这块内存区暂没用途。
  size_t Block_size;//指明block的结构体实例的大小:sizeof(struct __main_block_impl_0) 
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

//3.代码块myprint的真身,就是一个结构体
struct __main_block_impl_0 {
  struct __block_impl impl; //存储与block实现(代码体)相关的信息
  struct __main_block_desc_0* Desc;//存储block的描述信息
  char *str;  //截获到的str变量
  int aInteger; //截获到的aInteger变量
  //结构体的构造方法
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_str, int _aInteger, int flags=0) : str(_str), aInteger(_aInteger) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//4.block代码体的实现函数,就是一个C函数,可以通过函数指针来调用,传入的参数为block的结构体实例本身
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//编译器在编译过程中会在block代码体的实现函数中,声明与截获的外部局部变量名称相同的局部变量,并进行赋值
   char *str = __cself->str; // cself为block的结构体实例本身
   int aInteger = __cself->aInteger; // 
   
   printf("%s%d\n",str,aInteger);
}

//程序入口,主函数
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    //这两个变量虽然与block的结构体实例的成员变量名称相同,但根本是不同,所以在block代码体的实现函数里给它们赋值,会编译报错:Variable is not assignable,因为超出了变量的作用域
        int aInteger = 3;
        char *str = "hello world";
   
   /*-----------重点-----------*/
       //1.void (*myprint)(void):声明一个返回类型为空,参数为空,名称叫myprint的函数指针,它指向了block的构造方法,构造方法将创建一个block结构体实例
        void (*myprint)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, aInteger));

        //2.(void (*)(__block_impl *))这是一个返回值为void,参数类型为__block_impl *的指针类型,用来修饰实例myprint的成员变量FuncPtr(即myprint->FuncPtr), ((__block_impl *)myprint)为FuncPtr的参数
        ((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);
    }

//类型转换简化后,1和2相当于下面:
struct __main_block_impl_0 tmp = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA,str,aInteger);
struct __main_block_impl_0 *myprint = &tmp;
(*myprint->impl.FuncPtr)(myprint);
  /*-----------重点-----------*/

    return 0;
}

显然,代码块myprint经过编译后,代码块myprint底层结构体会新增成员变量aInteger,str并在构造函数中进行初始化。同时可以看到代码块myprint的实现函数static void __main_block_func_0(struct __main_block_impl_0 *__cself)中重新声明了相同名称的变量int aInteger和char *str,此时的aInteget,str与声明在主函数的局部变量是两个完全没关系的不同变量,它们不在同一作用域(同一函数)并且编译器有它自己的一套编译规则(1.支持截获变量的瞬间值,2.不支持在代码体内给外部的局部变量赋值,因为不允许在block的实现函数里给main函数的局部变量赋值,尽管两个变量名称相同),这也解释为什么给定义在代码体外面的局部变量重新赋值会引起编译错误。想要给aInteger,str重新赋值,可以把它们声明为全局变量或静态变量来解决作用域问题,但一般不选择这样做,而是在它们前面加上修饰符__block,这样就可以通过编译(将在第二小节探索)。

2.block截获对象

下面是block截取对象的oc源代码:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            NSMutableArray *array = [[NSMutableArray alloc] init];
            void(^myprint)(void)=^{
                printf("数组Count=%ld\n",array.count);
            };
            myprint();
        }
    }
    return 0;
}

示例中,myprint截获了可变数组array,以下是转换成C++的代码:

//代码块myprint的结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *array; //截获的array
  //代码块myprint结构体的构造方法
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock; //isa指向结构体实例本身,实例分配在栈区
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//myprint代码体对应的实现函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableArray *array = __cself->array; 
  //array.count反编译成runtime的消息发送objc_msgSend(arry,@selector(count));
   printf("数组Count=%ld\n",((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
 }
 
//拷贝操作函数,
//1.这个函数用来拷贝myprint结构体实例的array对象
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
//废弃操作函数,废弃myprint结构体实例的array对象,相当对象realse
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

//存储代码块myprint的结构体的描述信息并实例化一个实例__main_block_desc_0_DATA
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; 
        {
 //(NSMutableArray *(*)(id, SEL))把objc_msgSend(id,SEL)转成返回值为NSMutableArray *,参数为id,SEL的函数指针。
 //简化如下:
 NSMutableArray *aZone = objc_msgSend(objc_getClass("NSMutableArray"),sel_registerName("alloc"));
 NSMutableArray *array = objc_msgSend(aZone,sel_registerName("alloc"));
 // 
             NSMutableArray *array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
             
            void(*myprint)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
            ((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);

        }

    }
    return 0;
}

由此可知,编译器对block截获对象与截获变量的处理基本一样:在block的结构体中生成一个与截获的变量名称相同的成员变量。与截获基本数据类型变量不同的是,截获对象时,编译器会生成block_copyblock_dispose 函数用于对象的内存管理。

二. __block修饰符

1.编译器对__block的处理

我们知道在block代码体中不能给截获的变量重新赋值,但可以用__block来修饰被截获的变量,再进行重新赋值,示例如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
           __block int aInteger = 1;
            void(^myprint)(void)=^{
                aInteger = 3;
                printf("aInteger=%d\n",aInteger);
            };
            myprint();
    }
    return 0;
}

为了给block截获的变量重新赋值,可使用__block修饰符,下面是编译器对加了__block修饰符的变量的处理,与上面反编译后的代码相差不大:

//编译器会为被__block修饰的变量生成一个结构体类型,
struct __Block_byref_aInteger_0 {
  void *__isa; 
__Block_byref_aInteger_0 *__forwarding; //__forwarding指向__block变量结构体实例本身
 int __flags;
 int __size;
 int aInteger; //与外部的局部变量名称相同的成员变量
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_aInteger_0 *aInteger; // 截获的__block变量aInteger
  //block结构体的构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_aInteger_0 *_aInteger, int flags=0) : aInteger(_aInteger->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;  
    impl.Flags = flags;
    impl.FuncPtr = fp;  //函数指针指向block代码体的实现函数
    Desc = desc;
  }
};
//block代码体的实现函数,以block的结构实例本身作为参数传递
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//结构体aInteger的__forwarding指向自身,绕了一步再取成员变量int aInteger
  __Block_byref_aInteger_0 *aInteger = __cself->aInteger; 
  (aInteger->__forwarding->aInteger) = 3;
  printf("aInteger=%d\n",(aInteger->__forwarding->aInteger));
 }
//block变量的copy函数用于对象的内存管理  (拷贝对象,引用计数+1)         
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->aInteger, (void*)src->aInteger, 8/*BLOCK_FIELD_IS_BYREF*/);
}
//block变量的dispose函数用于对象的内存管理 (废弃对象)
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->aInteger, 8/*BLOCK_FIELD_IS_BYREF*/);}

//存储block的描述信息,在声明同时实例化一个__main_block_desc_0_DATA实例
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; 
        {
        //实例化aInteger结构体
            __attribute__((__blocks__(byref))) __Block_byref_aInteger_0 aInteger = {
(void*)0 //void *isa
(__Block_byref_aInteger_0 *)&aInteger, //__forwardind
 0, 
  sizeof(__Block_byref_aInteger_0), 
  1
  };

//函数指针myprint指向myprint代码体的构造函数即指向了结构体实例            
  void(*myprint)(void)=((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_aInteger_0 *)&aInteger, 570425344));

//下面去掉类型转换:(myprint->FuncPtr)(myprint),即结构体实例myprint引用成员变量FuncPtr指针,来调用block代码体的实现函数__main_block_func_0
((void (*)(__block_impl *))((__block_impl *)myprint)->FuncPtr)((__block_impl *)myprint);

        }
    }
    return 0;
}

2.对象的内容可以进行操作

不能对没有__block修饰符修饰的变量进行重新赋值,但截获的变量如果是对象类型的话,是可以对其内容进行操作的,例如:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:2];
        void(^myprint)(void)=^{
               [mArray addObject:@"Hello"];
            printf("mArray.count=%ld\n",mArray.count);
            };
        myprint();
    }
    return 0;
}

三. block存储域

下面是内存分配的一些说明:
一:声明在函数内部的局部变量,系统自动分配到栈上,且在函数执行结束后,被释放。
二:由程序员调用alloc,malloc,new,copy等方法初始化的变量,会分配到堆上,程序员可手动调用free来释放。否则,等程序结束后,由系统释放回收
三:一些静态变量,全局变量,常量会在编译阶段分配在数据 区(data区)
四:操作指令会分配到文本区(text区)
以下是一个应用在内存中进程空间的布局(线程thread共享进程的内存空间)


这里写图片描述

1.分配在栈上的block

声明在函数内部的block代码体会分配在栈上。
在上面的c++代码中,block的结构体实例构造方法会给impl.isa赋值为 &_NSConcreteStackBlock,我们知道isa指针是指向了block结构体实例自身,即&_NSConcreteStackBlock指向了block结构体实例在内存中的起址地址,NSConcreteStackBlock,见文知义,Stack是栈的意思,可以推测系统将block的结构体实例分配在栈上。

2.分配在堆上的block

当block代码体赋值给拥有strong,__strong,copy修饰的变量时,block会在堆上生成一份副本,并将副本赋值给变量,block结构体实例的impl.isa=&_NSConcreteMallocBlock,isa指针指向堆上的实例

3.分配在数据区(全局)的block

在全局地方声明的block代码体(在@interface外面声明),会分配在数据区,impl.isa=&_NSConcreteGlobalBlock 指向数据区。

四.copy的使用

1.为什么要用copy呢?

一般情况下,block的代码体会声明在某个方法内而极少声明在@interface外面(可理解为全局的地方,因数在全局地方block截获不了有用的变量),我们知道,在方法内声明的(不是由all,new初始化的)会分配到栈上,并且会在方法执行完结时被释放,这样就极容易引用了被释放的对象,从而抛出内存读取错误的异常,所以会为block声明一个copy属性,使它指向堆上的对象以确保其生命周期。

在项目开发,为某个类声明一个block属性时,通常会这样写:

typdef void (^Myblock)(void)
@interface MyClass
@property (copy,nonatomic) Myblock oneBlock;
...
{
    self.oneBlock=^{ NSLog(@"Hello world!"); };
}

当为属性指明copy属性时,^{ NSLog(@"Hello world!"); } 的结构体实例将被从栈上copy一份副本到堆上,且_oneBlock指向了堆上的副本。其实,声明onBlock属性也可以这样写 @property (strong,nonatomic) Myblock oneBlock; 或者@property (nonatomic) Myblock oneBlock; 因为属性strong作用相当于修饰符__strong(__strong是对象的隐式说明),编译器会为__strong的对象分配到堆上作内存管理。但如果非要作死,写成@property (weak,nonatomic) Myblock oneBlock; ,就会报错了bad_address_access。

2.不用手动copy的情况

在一些情况下,出于内存管理的原因,编译器会自动帮我们copy, 不我们自己去copy,情况如下:
一:block作为函数的返回值被传递时,如

-(OneBlock)someMethod{
  OneBlock aBlock = ^{...};
  return aBlock; //不需要写成return [aBlock copy];
}

二:Cocoa框架方法或GCD的API方法中含有usingBlock来传递block时
三:将block赋值给带有__strong修饰符的id类型的类或__block说明符的变量。

五.相互引用问题

由于block会截获出现在代码体的外部对象(定义在代码体{}外部的对象),并在block的结构体内声明一个与之名称相同的成员变量并在实例化时指向并截获的外部对象,如果被截获的对象又拥有或间接拥有(持有)该block,就会形成闭环,引起相互引用的问题,不能被有效释放。

1.相互引用

引起循环引用的示例如下:

#import "MyClass.h"
typedef void(^Myprint)(void);
@interface MyClass ()
{
    Myprint _myprint;
}
@end
@implementation MyClass
-(void)myPrintMethod{
    _myprint = ^{
        NSLog(@"%@",self); //编译器警告:Capturing 'self'strongly in this block is likely to lead to a retain cycle
    };
}
@end

2.间接相互引用

#import "MyClass.h"
typedef void(^Myprint)(void);
@interface MyClass ()
{
    Myprint _myprint;
}
@property(strong,nonatomic) NSString *str;

@end
@implementation MyClass
-(void)myPrintMethod{
    _myprint = ^{
        NSLog(@"%@",_str);//编译器警告:Capturing 'self'strongly in this block is likely to lead to a retain cycle
    };
}
@end

3.__weak解决相互引用

形成相互引用的两个对象,不能被有效释放,dealloc方法也不会执行。


这里写图片描述

A对象要释放,首先要释放B,B要释放,那么A对象就要先释放,这样会形成死锁,导致A,B对象都不能释放。__weak修饰符或__unsafe_unretained修饰符可解决循环引用问题,示例如下:

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

推荐阅读更多精彩内容