iOS中block的底层实现及注意事项

block作为iOS开发过程中很常用,我们可以把block想象成一个封装了一系列方法的函数,可以用于作为参数或者返回值,但是其底层实现及其注意事项,大家可能并不是都能讲的很清楚,下面我简单讲诉下我对其理解,希望对大家有所帮组。

block的本质

block的本质其实也是一个对象,为什么这么说呢?因为如果你使用clang进行转换oc语言中的block,你会发现block的结构体也是有isa指针的,只不过这个结构体还包含了函数的调用以及该函数执行的参数环境,下面我将clang -rewrite-objc的前后结果附上,大家进行比较也就理解了。

//这是oc代码
void(^blockName)(NSString *arg) = ^(NSString *arg) {
        NSLog(@"%@",arg);
    };
    blockName(@"arg");
//这是重写后c++底层实现
//这是block的定义
void(*blockName)(NSString *arg) = ((void (*)(NSString *))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//这是block的调用
    ((void (*)(__block_impl *, NSString *))((__block_impl *)blockName)->FuncPtr)((__block_impl *)blockName, (NSString *)&__NSConstantStringImpl__var_folders_lq_g7l9xm350p9fn1h8p3dwts2m0000gn_T_main_9b951d_mi_1);

不难发现,在调用的时候blockName是被__block_impl *修饰的,所以可以知道该block的类型是__block_impl *,所以我们再找到__block_impl *的定义,

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

可以看到__block_impl结构体的第一个成员也是isa指针,所以说其实block的本质也是一个对象。

block的内部结构

通过简单的分析我们知道了block的本质是一个对象,那它是怎么工作的呢?我们继续在刚刚的例子上进行近一步的分析。
 首先我们观察block的定义部分,发现我们oc代码定义的block被定义成了__main_block_impl_0,我们可以找到该结构体的实现

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的定义的第一个成员就是刚刚我们找到的__block_impl,除此之外,我们可以在block的定义部分发现传人的第一个参数为(void *)__main_block_func_0,该指针的实现部分是

static void __main_block_func_0(struct __main_block_impl_0 *__cself, NSString *arg) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_lq_g7l9xm350p9fn1h8p3dwts2m0000gn_T_main_9b951d_mi_0,arg);
    }

将这些串接起来,我们可以知道block结构体中FuncPtr是用来存储block的实现方法的。


上面的例子中,block是不引入外部参数的,但上面例子很容易让我们理解block本质结下来我们会由浅入深的分别介绍在引入参数的不同情况

//这是oc代码
        NSInteger i = 10;
        void(^blockName)(void) = ^() {
            NSLog(@"wenpq....%ld",i);
        };
        i = 20;
        blockName();
//这是c++代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

对比之前不引入参数的情况,可以发现新的block中增加了一个i,并且在运行后,我们发现输出的是10,所以可以知道在这种情况下是数值的传递,而并非是地址传递。


下面将i变量定义成static,看在底层block的结构体有什么变化

//这是oc代码
        static NSInteger i = 10;
        void(^blockName)(void) = ^() {
            NSLog(@"wenpq....%ld",i);
        };
        i = 20;
        blockName();
//这是c++代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSInteger *i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger *_i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

对比之前auto类型的变量,static类型的变量传递进来的是个指针,所以大家也一定猜到了运行结果,运行结果在这里是20,这个时候有同学就会疑问,为什么有的时候是值传递,有的时候是地址传递,我这里发表下自己的理解,因为作为局部变量,这两个变量的存储空间是不一样的,非静态的局部变量是存在栈中,它随时都有可能会被回收,所以block只取捕获它的值,而不记录它的地址是比较合理的,而静态变量是一直存在内存中的,所以使用最新的值。


上面的变量无论是静态的还是auto的都是局部变量,那么全局变量是否有什么不同

//这是oc代码
static NSInteger i = 10;
int main(int argc, char * argv[]) {
    @autoreleasepool {
        void(^blockName)(void) = ^() {
            NSLog(@"wenpq....%ld",i);
        };
        i = 20;
        blockName();
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
//这是c++代码
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函数中如果访问了全局变量,是不会捕获的,其实这种情况很好理解的,既然都应是全局的变量,任何对象就没必要再去记录该变量,都能直接访问的。

block类型说明

在上面的例子中,我们可以发现block的构造函数的isa指针都指向_NSConcreteStackBlock所在地址,如果大家感兴趣可以使用[obj class]打印出来看看block的真实类型以及其父类是什么类型,这里直接打印出结果

        void(^blockName)(void) = ^() {
            NSLog(@"wenpq....%ld",i);
        };
        NSLog(@"%@\n%@\n%@\n%@\n%@",blockName,[blockName class],[[blockName class] superclass],[[[blockName class] superclass] superclass],[[[[blockName class] superclass] superclass] superclass]);
//打印结果
<__NSGlobalBlock__: 0x10f2ab170>
__NSGlobalBlock__
__NSGlobalBlock
NSBlock
NSObject

block函数中引入参数不同,在运行时可以将其分为全局、栈、堆block,下面分别举例说明这三类block是如何划分的。

全局block

不访问局部auto变量的block为全局变量,全局block因为不访问auto变量,所以在实际开发中很少使用,因为完全可以用一个方法代替。

static NSInteger i = 10;
NSInteger j = 20;
int main(int argc, char * argv[]) {
    @autoreleasepool {
        static NSInteger k = 30;
        void(^blockName)(void) = ^() {
            NSLog(@"wenpq....%ld....%ld....%ld",i,j,k);
        };
        NSLog(@"wenpq...result...%@",[blockName class]);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
//打印结果
wenpq...result...__NSGlobalBlock__

栈block

栈block是之前mrc环境时,block内部引用了auto变量,因为block时存放在栈上数据,所以释放时机是由系统决定的,于是这种类型的block使用起来很不安全,可能造成意想不到的问题。所以要把栈block的内存放到堆内存中,由程序来决定其生命周期。

mrc 环境

        NSInteger m = 40;
        void(^block2)(void) = ^() {
            NSLog(@"wenpq.....%ld",m);
        };
        NSLog(@"wenpq....result...%@",[block2 class]);
//打印结果
wenpq....result...__NSStackBlock__

堆block

在MRC环境下受用copy操作后,栈block就可以成为堆block

        NSInteger m = 40;
        void(^block2)(void) = ^() {
            NSLog(@"wenpq.....%ld",m);
        };
        NSLog(@"wenpq....result...%@",[[block2 copy] class]);
//打印结果
wenpq....result...__NSMallocBlock__

以上对栈block和堆block的分析都是在MRC的环境下进行分析,但是现在的工程大都是ARC的形式,所以下面讲诉下ARC环境block的类型,ARC环境在以下情况会自动将栈block进行copy操作,转换成堆block:

  1. block作为返回值;
  2. block被强指针指向;
  3. GCD中的回调block也都是在堆中;

block的强弱引用问题

下面讲诉内容都以ARC环境展开分析

之前举的例子都是用整型作为block的引入数据,下面我们用对象类型来分析block的强弱引用

强引用

//oc代码
        PQPerson *person = [[PQPerson alloc] init];
        person.name = @"zhangsan";
        void(^block)(void) = ^(){
            NSLog(@"wenpq....%@",person.name);
        };
        NSLog(@"come_here");
//c++代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  PQPerson *person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, PQPerson *_person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以发现在这个例子中,block结构体存储的person对象是被强引用的。

弱引用

//oc代码
PQPerson *person = [[PQPerson alloc] init];
        person.name = @"zhangsan";
        __weak typeof(person) weakPerson = person;
        void(^block)(void) = ^(){
            NSLog(@"wenpq....%@",weakPerson.name);
        };
        NSLog(@"come_here");
        return 0;
//c++代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  PQPerson *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, PQPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

后续

关于block的循环引用问题,会在讲解weak时一同讲解

以上对block都是个人工作中的总结,可能其中有些理解错误或者有偏差,如果大家发现以上讲解有问题或者疑问,欢迎在评论区提出,谢谢阅读!

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

推荐阅读更多精彩内容