美文网首页
iOS底层原理 - 窥探Block的本质(一)

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

作者: julieQY7 | 来源:发表于2022-02-22 17:10 被阅读0次

通过窥探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修饰变量时究竟是在做些什么

相关文章

网友评论

      本文标题:iOS底层原理 - 窥探Block的本质(一)

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