iOS底层原理 - 窥探Block的本质(一)

通过窥探Block的底层实现,解答以下问题

1.Block底层数据结构是什么,本质是什么
2.Block与其所访问的外部变量的关系
3.Block的内存管理

Block的本质是什么?是函数?代码块?OC对象?

简单起见,我们在main.m文件中写一个没有任何参数访问的简单的Block,通过分析其源码窥探Block的本质
定义一个简单的Block

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock block;
        block = ^{
            NSLog(@"this is block");
        };
    }
    return 0;
}

我们知道OC是基于C/C++实现的,接下来我们通过命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件名main.m文件转化成对应的.cpp文件,窥探Block的本质。

第一步:在.cpp文件中检索int main(int argc, const char * argv[])定位main函数位置,从而找到我们写的代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        MyBlock block;
        block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    }
    return 0;
}

为了方便阅读我们将上述源码中的核心部分((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))中的强制类型转换去掉,可以得到&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA),到此可以清楚的看到这里调用了一个函数__main_block_impl_0(...),传入了两个参数__main_block_func_0__main_block_desc_0_DATA。并将函数返回值的地址赋值给了block。接下来我们分别分析函数及这两个参数。

首先我们看下函数__main_block_impl_0
.cpp文件中进一步检索函数__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;
  }
};

通过上述源码,可以看到__main_block_impl_0(...)是结构体struct __main_block_impl_0的构造函数。也就是说Block的本质就是结构体struct __main_block_impl_0

继续检索传入的参数__main_block_func_0

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6m_csw1z8qs7csb3nvkwyrlbnm80000gp_T_main_d9b536_mi_0);
}

可以看出这就是我们写的NSLog(@"this is block");即block的实现部分。

继续检索__main_block_desc_0_DATA

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

进一步分析其成员变量,其中struct __main_block_desc_0我们上边已经看过了。接下来我们检索成员变量struct __block_impl

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

可与看到一个很熟悉的指针isa指针,我们知道oc对象的基本特点就是有一个isa指针,看到这你可能会猜想block本身是否也是一个oc对象呢?
答案是肯定的block本质的确是一个oc对象,下面我们将通过其他方式验证这一点。我们知道几乎所有的oc对象都继承自NSObject,尝试打印Block的父类。

NSLog(@"block class : %@", [block class]);
NSLog(@"block super class : %@", class_getSuperclass([block class]));
NSLog(@"block super super class : %@", class_getSuperclass(class_getSuperclass([block class])));
NSLog(@"block super super super class : %@", class_getSuperclass(class_getSuperclass(class_getSuperclass([block class]))));

// 输出log
block class : __NSGlobalBlock__
block super class : __NSGlobalBlock
block super super class : NSBlock
block super super super class : NSObject

从上述log可以看出,block本身的确是一个oc对象,且其继承自NSObject

Block对基础数据类型的捕获

定义三种基本类型变量,并在Block中访问他们

int globalInt = 1; // 全局变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock block;
        static int staticInt = 5; // 静态变量
        int autoInt = 10; // 自动变量(本质上是auto int autoInt = 10;省略了关键字auto,通常我们定义的都是这种变量)
        block = ^{
            NSLog(@"staticInt = %d", staticInt);
            NSLog(@"autoInt = %d", autoInt);
            NSLog(@"globalInt = %d", globalInt);
        };
        block();
    }
    return 0;
}

通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件名得到对应的.cpp文件。
.cpp文件中检索int main(int argc, const char * argv[])定位main函数

int globalInt = 1;
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        MyBlock block;
        static int staticInt = 5;
        int autoInt = 10;
        block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staticInt, autoInt));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

观察上述源码,可以看到在构建block的结构体struct __main_block_impl_0时,比之前多传了两个参数&staticIntautoInt,但是并没有传globalInt,即:static变量传了地址(地址传递),局部auto变量传了值(值传递),全局变量没有传递。
接下来我们去struct __main_block_impl_0中看下这些参数的处理

Block对基础数据类型的捕获.png

观察上述代码,可以看到结构体struct __main_block_impl_0中多了两个成员变量int *staticInt;int autoInt;,并且构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staticInt, int _autoInt, int flags=0) : staticInt(_staticInt), autoInt(_autoInt)将外边传过来的_staticInt_autoInt分别赋值给了int *staticInt;int autoInt;也就是说Block分别对静态变量和自动变量进行了地址捕获和值捕获

在看下__main_block_func_0__main_block_desc_0

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *staticInt = __cself->staticInt; // 取结构体中的int型指针变量staticInt给局部int型指针变量staticInt
  int autoInt = __cself->autoInt; // 取结构体中int型变量autoInt给局部int型变量autoInt

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_6m_csw1z8qs7csb3nvkwyrlbnm80000gp_T_main_7c19b4_mi_0, (*staticInt)); // 打印局部int型指针staticInt指向值
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_6m_csw1z8qs7csb3nvkwyrlbnm80000gp_T_main_7c19b4_mi_1, autoInt);//打印局部变量autoInt的值
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_6m_csw1z8qs7csb3nvkwyrlbnm80000gp_T_main_7c19b4_mi_2, globalInt);//打印全局变量globalInt的值
        }

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_desc_0没有变化

小结

  • Block不会捕获全局变量
  • Block会对静态变量进行地址捕获
  • Block会对自动变量进行值捕获

那么Block为什么这么做呢,认真思考下就会发现这是一个很合乎情理的设计,对于全局变量由于它是全局的,本身就可以在任意地方访问,所以Block自然不需要捕获它,就像函数中访问全局变量不需要将其当参数传进去一样;对于局部静态变量,首先我们想象一种使用场景,Block及局部静态变量是在一个普通函数中的,并且Block作为函数返回值使用,在这种情况下,因为出了函数我们就无法访问到局部变量了,所以首先Block肯定要对这个静态局部变量进行捕获,再者我们知道静态变量不会随着大括号即函数的结束而销毁,所以Block只需要对其进行地址捕获即可。对于普通的自动局部变量,由于其会被销毁,所以需要对其进行值捕获

Block对OC对象的捕获

为了演示,我们先创建一个类LJPerson

@interface LJPerson : NSObject

@property (copy, nonatomic) NSString    *name;
@property (assign, nonatomic) int       age;

@end

和基础变量一样,我们也创建三种不同的LJPerson变量,并在Block中访问他们

typedef void(^MyBlock)(void);

LJPerson *globalPerson; // 全局变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock block;
        
        globalPerson = [[LJPerson alloc] init];
        globalPerson.name = @"globalPerson";
        globalPerson.age = 1;
        
        static LJPerson *staticPerson; // 静态变量
        staticPerson = [[LJPerson alloc] init];
        staticPerson.name = @"staticPerson";
        staticPerson.age = 10;
        
        LJPerson *autoPerson = [[LJPerson alloc] init]; // 自动变量
        autoPerson.name = @"autoPerson";
        autoPerson.age = 20;
        
        block = ^{
            NSLog(@"globalPerson = %@", globalPerson.name);
            NSLog(@"staticPerson = %@", staticPerson.name);
            NSLog(@"autoPerson = %@", autoPerson.name);
        };
        block();
    }
    return 0;
}

同样的生成对应的.cpp文件,并检索main函数int main(int argc, const char * argv[])和结构体struct __main_block_impl_0

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

        globalPerson = ((LJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LJPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)globalPerson, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_6m_csw1z8qs7csb3nvkwyrlbnm80000gp_T_main_d7ed57_mi_0);
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)globalPerson, sel_registerName("setAge:"), 1);

        static LJPerson *staticPerson;
        staticPerson = ((LJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LJPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)staticPerson, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_6m_csw1z8qs7csb3nvkwyrlbnm80000gp_T_main_d7ed57_mi_1);
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)staticPerson, sel_registerName("setAge:"), 10);

        LJPerson *autoPerson = ((LJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LJPerson"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)autoPerson, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_6m_csw1z8qs7csb3nvkwyrlbnm80000gp_T_main_d7ed57_mi_2);
        ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)autoPerson, sel_registerName("setAge:"), 20);

        block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &staticPerson, autoPerson, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  LJPerson **staticPerson; // 地址捕获
  LJPerson *autoPerson; // 值捕获
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LJPerson **_staticPerson, LJPerson *_autoPerson, int flags=0) : staticPerson(_staticPerson), autoPerson(_autoPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

观察上述源码,可知Block对OC变量的捕获机制和基础数据类型一样。即:Block对变量的捕获方式只取决于其是全局变量、静态变量、还是自动变量。
那么Block对OC类型变量的处理与基础数据类型变量的处理究竟有什么不同呢?
检索Block的成员变量struct __main_block_desc_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};

对比之前的源码,可以发现这里多了两个函数copydispose
观察结构体对象__main_block_desc_0_DATA的入参,可知copydispose函数分别对应参数__main_block_copy_0__main_block_dispose_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->staticPerson, (void*)src->staticPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->autoPerson, (void*)src->autoPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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

观察函数两个函数,可以看到__main_block_copy_0内部分别对staticPersonautoPerson对象各调用了一次_Block_object_assign方法,__main_block_dispose_0内部分别对staticPersonautoPerson各调用了一次_Block_object_dispose方法。也就是说Block除了捕获OC对象的外,还会对OC对象做内存管理。

小尾巴

到此我们已经解决了开篇中的第1、2个问题,并且知道了Block会对OC对象做内存管理,那么在后续的文章里我们将继续探索Block是如何进行内存管理的以及当我们用__weak__block修饰变量时究竟是在做些什么

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

推荐阅读更多精彩内容