runtime相关(参考源码objc-runtime或objc4)
结构模型
1、介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
对象:OC中的对象指向的是一个objc_object指针类型,typedef struct objc_object *id;从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表、属性列表、成员变量列表等相关信息的。
1 2 3 4 | /// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; |
类:在OC中的类是用Class来表示的,实际上它指向的是一个objc_class的指针类型,typedef struct objc_class *Class;对应的结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; #endif } |
从结构体中定义的变量可知,OC的Class类型包括如下数据(即:元数据metadata):super_class(父类类对象);name(类对象的名称);version、info(版本和相关信息);instance_size(实例内存大小);ivars(实例变量列表);methodLists(方法列表);cache(缓存);protocols(实现的协议列表);
当然也包括一个isa指针,这说明Class也是一个对象类型,所以我们称之为类对象,这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息。
以下图中可以清楚的了解到OC对象、类、元类之间的关系
从图中可知,最终的基类(NSObject)的元类对象isa指向的是自己本身,从而形成一个闭环。
元类(Meta Class):是一个类对象的类,即:Class的类,这里保存了类方法等相关信息。
我们再看一下类对象中存储的方法、属性、成员变量等信息的结构体
objc_ivar_list:存储了类的成员变量,可以通过object_getIvar或class_copyIvarList获取;另外这两个方法是用来获取类的属性列表的class_getProperty和class_copyPropertyList,属性和成员变量是有区别的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | struct objc_ivar { char * _Nullable ivar_name OBJC2_UNAVAILABLE; char * _Nullable ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; struct objc_ivar_list { int ivar_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE; } |
objc_method_list:存储了类的方法列表,可以通过class_copyMethodList获取。
结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE; struct objc_method_list { struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; } |
objc_protocol_list:储存了类的协议列表,可以通过class_copyProtocolList获取。
结构体如下:
1 2 3 4 5 | struct objc_protocol_list { struct objc_protocol_list * _Nullable next; long count; __unsafe_unretained Protocol * _Nullable list[1]; }; |
2、为什么要设计metaclass
metaclass代表的是类对象的对象,它存储了类的类方法,它的目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则。
具体可以参考这篇文章
3、
class_copyIvarList:获取的是类的成员变量列表,即:@interface{中声明的变量}
class_copyPropertyList:获取的是类的属性列表,即:通过@property声明的属性
4、
class_rw_t:代表的是可读写的内存区,这块区域中存储的数据是可以更改的。
class_ro_t:代表的是只读的内存区,这块区域中存储的数据是不可以更改的。
OC对象中存储的属性、方法、遵循的协议数据其实被存储在这两块儿内存区域的,而我们通过runtime动态修改类的方法时,是修改在class_rw_t区域中存储的方法列表。
参考这篇文章
5、
category的加载是在运行时发生的,加载过程是,把category的实例方法、属性、协议添加到类对象上。把category的类方法、属性、协议添加到metaclass上。
category的load方法执行顺序是根据类的编译顺序决定的,即:xcode中的Build Phases中的Compile Sources中的文件从上到下的顺序加载的。
category并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category添加的methodA会排在原有类的methodA的前面,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category 里的对应方法。
参考这篇文章
6、
category:分类
- 给类添加新的方法
- 不能给类添加成员变量
- 通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和setter方法,同时也不能生成带下划线的成员属性
- 是运行期决定的
注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。
extension:扩展
- 可以给类添加成员变量,但是是私有的
- 可以給类添加方法,但是是私有的
- 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
- 伴随着类的产生而产生,也随着类的消失而消失
- 必须有类的源码才可以给类添加extension,所以对于系统一些类,如nsstring,就无法添加类扩展
不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension。
7、消息转发机制,消息转发机制和其他语言的消息机制优劣对比
消息转发机制:当接收者收到消息后,无法处理该消息时(即:找不到调用的方法SEL),就会启动消息转发机制,流程如下:
第一阶段:咨询接收者,询问它是否可以动态增加这个方法实现
第二阶段:在第一阶段中,接收者无法动态增加这个方法实现,那么系统将询问是否有其他对象可能执行该方法,如果可以,系统将转发给这个对象处理。
第三阶段:在第二阶段中,如果没有其他对象可以处理,那么系统将该消息相关的细节封装成NSInvocation对象,再给接收者最后一次机会,如果这里仍然无法处理,接收者将收到doesNotRecognizeSelector方法调用,此时程序将crash。
具体方法如下:
1 2 3 4 5 6 7 8 9 10 | // 第一阶段 咨询接收者是否可以动态添加方法 + (BOOL)resolveInstanceMethod:(SEL)selector + (BOOL)resolveClassMethod:(SEL)selector //处理的是类方法 // 第二阶段:询问是否有其他对象可以处理 - (id)forwardingTargetForSelector:(SEL)selector // 第三阶段 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector - (void)forwardInvocation:(NSInvocation *)invocation |
参考这篇文章
8、在方法调用的时候,
OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, ...)方法进行调用,这个方法第一个参数是一个消息接收者对象,runtime通过这个对象的isa指针找到这个对象的类对象,从类对象中的cache中查找是否存在SEL对应的IMP,若不存在,则会在 method_list中查找,如果还是没找到,则会到supper_class中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)进行消息转发。
9、
IMP:是方法的实现,即:一段c函数
SEL:是方法名
Method:是objc_method类型指针,它是一个结构体,如下:
1 2 3 4 5 | struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; } |
使用场景:
实现类的swizzle的时候会用到,通过class_getInstanceMethod(class, SEL)来获取类的方法Method,其中用到了SEL作为方法名
调用method_exchangeImplementations(Method1, Method2)进行方法交换
我们还可以给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types),该方法需要我们传递一个方法的实现函数IMP,例如:
1 2 3 | static void funcName(id receiver, SEL cmd, 方法参数...) { // 方法具体的实现 } |
函数第一个参数:方法接收者,第二个参数:调用的方法名SEL,方法对应的参数,这个顺序是固定的。
10、
load:当类被装载的时候被调用,只调用一次
- 调用方式并不是采用runtime的objc_msgSend方式调用的,而是直接采用函数的内存地址直接调用的
- 多个类的load调用顺序,是依赖于compile sources中的文件顺序决定的,根据文件从上到下的顺序调用
- 子类和父类同时实现load的方法时,父类的方法先被调用
- 本类与category的调用顺序是,优先调用本类的(注意:category是在最后被装载的)
- 多个category,每个load都会被调用(这也是load的调用方式不是采用objc_msgSend的方式调用的),同样按照compile sources中的顺序调用的
- load是被动调用的,在类装载时调用的,不需要手动触发调用
注意:当存在继承关系的两个文件时,不管父类文件是否排在子类或其他文件的前面,都是优先调用父类的,然后调用子类的。
例如:compile sources中的文件顺序如下:SubB、SubA、A、B,load的调用顺序是:B、SubB、A、SubA。
分析:SubB是排在compile sources中的第一个,所以应当第一个被调用,但是SubB继承自B,所以按照优先调用父类的原则,B先被调用,然后是SubB,A、SubA。
第二种情况:compile sources中的文件顺序如下:B、SubA、SubB、A,load调用顺序是:B、A、SubA、SubB,这里我给大家画个图梳理一下:
initialize:当类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次
- 调用方式是通过runtime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕
- 子类和父类同时实现initialize,父类的先被调用,然后调用子类的
- 本类与category同时实现initialize,category会覆盖本类的方法,只调用category的initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的)
- initialize是主动调用的,只有当类第一次被用到的时候才会触发
参考这篇文章
内存管理
1、
weak:其实是一个hash表结构,其中的key是所指对象的地址,value是weak的指针数组,weak表示的是弱引用,不会对对象引用计数+1,当引用的对象被释放的时候,其值被自动设置为nil,一般用于解决循环引用的。
weak的实现原理
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
SideTable的结构如下:
1 2 3 4 5 6 7 8 | struct SideTable { // 保证原子操作的自旋锁 spinlock_t slock; // 引用计数的 hash 表 RefcountMap refcnts; // weak 引用全局 hash 表 weak_table_t weak_table; } |
参考这篇文章
2、关联对象的应用?系统如何实现关联对象的?
应用:
- 可以在不改变类的源码的情况下,为类添加实例变量(注意:这里指的实例变量,并不是真正的属于类的实例变量,而是一个关联值变量)
- 结合category使用,为类扩展存储属性。
关联对象实现原理:
关联对象的值实际上是通过AssociationsManager对象负责管理的,这个对象里有个AssociationsHashMap静态表,用来存储对象的关联值的,关于AssociationsHashMap存储的数据结构如下:
AssociationsHashMap:
------添加属性对象的指针地址(key):ObjectAssociationMap(value:所有关联值对象)
ObjectAssociationMap:
------关联值的key:关联值的value
具体runtime的方法实现请参考这篇文章
3、关联对象的如何进行内存管理的?关联对象如何实现weak属性?
内存管理方面是通过在赋值的时候设置一个policy,根据这个policy的类型对设置的对象进行retain/copy等操作。
当policy为OBJC_ASSOCIATION_ASSIGN的时候,设置的关联值将是以weak的方式进行内存管理的。
这个题跟上面的问题差不多,可以参考上面的那篇文章。
4、
自动释放池是一个
AutoreleasePool的释放有如下两种情况:
- 一种是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
- 手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool或者@autoreleasepool{}执行完释放
参考这篇文章
5、
参考这篇文章
6、
- block中的循环引用
- NSTimer的循环引用
- addObserver的循环引用
- delegate的强引用
- 大次数循环内存爆涨
- 非OC对象的内存处理(需手动释放)
参考这篇文章
其他
1、
- 如果是通过method_exchangeImplements()方法实现swizzle的话,需要考虑调用时机,弄不好会出现无效的swizzle
- 在调用method_exchangeImplements函数之前,我们需要确保传入的Method,确保有实现IMP
针对method_exchangeImplements函数的副作用,我们可以结合method_setImplementation实现方法swizzle即可。
参考这篇文章
2、属性修饰符
atomic实际上是为成员变量的setter方法自动添加了一个自旋锁,确保属性的赋值的原子性。
不能保证线程安全,因为atomic只是对setter方法加锁,getter并没有加锁
参考这篇文章
3、iOS 中内省的几个方法有哪些?内部实现原理是什么?
实现内省的方法包括:
- isKindOfClass:Class
- isMemberOfClass:Class
- respondToSelector:selector
- conformsToProtocol:protocol
实现原理:以上方法的实现原理都是运用runtime的相关函数实现的。
参考这篇文章,以及oc类的数据结构
4、
objc_getClass:参数是类名的字符串,返回的就是这个类的类对象;
object_getClass:参数是id类型,它返回的是这个id的isa指针所指向的Class,如果传参是Class,则返回该Class的metaClass
[obj class]:则分两种情况:一是当obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的obj对象中的isa指针;二是当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。
NSNotification相关
相关参考
1、实现原理(结构设计、通知如何存储的、
参考这篇文章
2、通知的发送是同步的,还是异步的?
同步的
3、
通知的接收和发送是在一个线程里
实际上发送通知都是同步的,不存在异步操作。而所谓的异步发送,也就是延迟发送,在合适的实际发送。
实现异步发送:
- 让通知的执行方法异步执行即可
- 通过NSNotificationQueue,将通知添加到队列当中,立即将控制权返回给调用者,在合适的时机发送通知,从而不会阻塞当前的调用
参考这篇文章
4、
NSPostingStyle的值为:
- NSPostWhenIdle和NSPostASAP:异步发送
- NSPostNow:同步发送
响应线程:
默认情况是在主线程中响应的,倘若在调用enqueueNotification将通知添加到队列中时,是在子线程中完成的,那么,响应也会在这个子线程中。
5、
NSNotificationQueue将通知添加到队列中时,其中postringStyle参数就是定义通知调用和runloop状态之间关系。
该参数的三个可选参数:
- NSPostWhenIdle:runloop空闲的时候回调通知方法
- NSPostASAP:runloop在执行timer事件或sources事件完成的时候回调通知方法
- NSPostNow:runloop立即回调通知方法
参考这篇文章
6、如何保证通知接收的线程在主线程?
有以下两种方案
- 使用
addObserverForName: object: queue: usingBlock 方法注册通知,指定在mainqueue 上响应block - 通过在主线程的runloop中添加machPort,设置这个port的delegate,通过这个Port其他线程可以跟主线程通信,在这个port的代理回调中执行的代码肯定在主线程中运行,所以,在这里调用NSNotificationCenter发送通知即可,参考这篇文章
7、页面销毁时不移除通知会崩溃吗?
- iOS9.0之前,会crash,原因:通知中心对观察者的引用是unsafe_unretained,导致当观察者释放的时候,观察者的指针值并不为nil,出现野指针。
- iOS9.0之后,不会crash,原因:通知中心对观察者的引用是weak。
8、多次添加同一个通知会是什么结果?多次移除通知呢?
多次添加同一个通知,会导致发送一次这个通知的时候,响应多次通知回调。
多次移除通知不会产生crash。
9、下面的方式能接收到通知吗?为什么?
1 2 3 4 | // 发送通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1]; // 接收通知 [NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil]; |
不能
需要了解通知中心存储通知观察者的结构了,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 根容器,NSNotificationCenter持有 typedef struct NCTbl { Observation *wildcard; /* 链表结构,保存既没有name也没有object的通知 */ GSIMapTable nameless; /* 存储没有name但是有object的通知 */ GSIMapTable named; /* 存储带有name的通知,不管有没有object */ ... } NCTable; // Observation 存储观察者和响应结构体,基本的存储单元 typedef struct Obs { id observer; /* 观察者,接收通知的对象 */ SEL selector; /* 响应方法 */ struct Obs *next; /* Next item in linked list. */ ... } Observation; |
nameless与named的具体数据结构如下:
如上图所示,当添加通知监听的时候,我们传入了name和object,所以,观察者的存储链表是这样的:
named表:key(name):value->key(object):value(Observation)
因此在发送通知的时候,如果只传入name而并没有传入object,是找不到Observation的,也就不能执行观察者回调
Runloop & KVO
runloop
1、app如何接收到触摸事件的?
- 首先,手机中处理触摸事件的是硬件系统进程 ,当硬件系统进程识别到触摸事件后,会将这个事件进行封装,并通过machPort,将封装的事件发送给当前活跃的APP进程。
- 由于APP的主线程中runloop注册了这个machPort端口,就是用于接收处理这个事件的,所以这里APP收到这个消息后,开始寻找响应链。
- 寻找到响应链后,开始分发事件,它会优先发送给手势集合,来过滤这个事件,一旦手势集合中其中一个手势识别了这个事件,那么这个事件将不会发送给响应链对象。
- 手势没有识别到这个事件,事件将会发送给响应链对象UIResponser。
参考这篇文章
2、为什么只有主线程的
app启动前会调用main函数,具体如下:
1 2 3 4 5 6 7 8 | int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } |
mian函数中调用UIApplicationMain,这里会创建一个主线程,用于UI处理,为了让程序可以一直运行,所以在主线程中开启一个runloop,让主线程常驻。
3、为什么只在主线程刷新UI?
UIKit并不是一个 线程安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。另一方面因为整个程序的起点
参考这篇文章
4、
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
参考这篇文章
5、如何使线程保活?
- 在NSThread执行的方法中添加while(true){},这样是模拟runloop的运行原理,结合GCD的信号量,在{}中处理任务。参考这篇文章
- 采用runloop的方式。参考这篇文章
KVO
1、实现原理。
在给对象A的属性name添加KVO观察者的时候,runtime会动态创建一个类B,这个类B继承自类A,并且重写了父类的属性name的setter方法,在重写的方法中,在给name成员变量赋值的前后,分别通知调用观察者回调。
参考这篇文章
2、如何手动关闭kvo?
- 重写被观察对象的automaticallyNotifiesObserversForKey方法,返回NO
- 重写automaticallyNotifiesObserversOf
,返回NO
注意:关闭kvo后,需要手动在赋值前后添加willChangeValueForKey和didChangeValueForKey,才可以收到观察通知。
参考这篇文章
3、通过KVC修改属性会触发KVO么?
会
4、哪些情况下使用kvo会崩溃,怎么防护崩溃?
- removeObserver一个未注册的keyPath,导致错误:Cannot remove an observer A for the key path "str",because it is not registered as an observer.
解决办法:根据实际情况,增加一个添加keyPath的标记,在dealloc中根据这个标记,删除观察者。
- 添加的观察者已经销毁,但是并未移除这个观察者,当下次这个观察的keyPath发生变化时,kvo中的观察者的引用变成了野指针,导致crash。
解决办法:在观察者即将销毁的时候,先移除这个观察者。
其实还可以将观察者observer委托给另一个类去完成,这个类弱引用被观察者,当这个类销毁的时候,移除观察者对象,参考KVOController。
5、kvo的优缺点?
缺点补充:
- 只能通过重写
-observeValueForKeyPath:ofObject:change:context: 方法来获得通知。 - 不同通过指定selector的方式获取通知。
- 不能通过block的方式获取通知。
参考这篇文章
Block
1、
block的结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct Block_literal_1 { void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 { unsigned long int reserved; // NULL unsigned long int size; // sizeof(struct Block_literal_1) // optional helper functions void (*copy_helper)(void *dst, void *src); // IFF (1<<25) void (*dispose_helper)(void *src); // IFF (1<<25) // required ABI.2010.3.16 const char *signature; // IFF (1<<30) } *descriptor; // imported variables }; |
isa:由此可知,block也是一个对象类型,具体类型包括_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock。
flags:block 的负载信息(引用计数和类型信息),按位存储,也可以获取block版本兼容的相关信息。以下是flags按bit位取与的所有可能值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | enum { // Set to true on blocks that have captures (and thus are not true // global blocks) but are known not to escape for various other // reasons. For backward compatibility with old runtimes, whenever // BLOCK_IS_NOESCAPE is set, BLOCK_IS_GLOBAL is set too. Copying a // non-escaping block returns the original block and releasing such a // block is a no-op, which is exactly how global blocks are handled. BLOCK_IS_NOESCAPE = (1 << 23), BLOCK_HAS_COPY_DISPOSE = (1 << 25), BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code BLOCK_IS_GLOBAL = (1 << 28), BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE BLOCK_HAS_SIGNATURE = (1 << 30), }; |
1 2 3 4 5 6 | switch (flags & (3<<29)) { case (0<<29): 10.6.ABI, no signature field available case (1<<29): 10.6.ABI, no signature field available case (2<<29): ABI.2010.3.16, regular calling convention, presence of signature field case (3<<29): ABI.2010.3.16, stret calling convention, presence of signature field, } |
由此可知:当flags & (3<<29) is BLOCK_HAS_COPY_DISPOSE的时候,才会有copy_helper和dispose_helper函数指针。
invoke:是block具体实现函数指针地址,可以通过此地址直接调用block。
Block_descriptor_1:block的描述文内容,它包括如下:
size:block所占的内存大小
copy_helper:copy函数指针(不同版本不一定存在)
dispose_helper:dispose函数指针(不同版本不一定存在)
signature:block的实现函数的签名(不同版本不一定存在),可以通过此指针获取block的参数内容描述、返回值内容描述等
获取block的方法签名,可以参考这篇文章
2、block是类吗,有哪些类型?
从block的结构体中可知,block同样也有一个isa指针,所以block也是一个类,它的类型包括:
- _NSConcreteGlobalBlock
- _NSConcreteStackBlock
- _NSConcreteMallocBlock
3、一个
没有被__block修饰的int,block体中对这个变量的引用是值拷贝,在block中是不能被修改的。
通过__block修饰的int,block体中对这个变量的引用是指针拷贝,它会生成一个结构体,复制这个变量的指针引用,从而达到可以修改变量的作用。
关于block的变量截获:
block会将block体内引用外部变量的变量进行拷贝,将其拷贝到block的数据结构中,从而可以在block体内访问或修改外部变量。
外部变量未被__block修饰时,block数据结构中捕获的是外部变量的值,通过__block修饰时,则捕获的是对外部变量的指针引用。
注意:block内部访问全局变量时,全局变量不会被捕获到block数据结构中。
举个栗子:
未被__block修饰的情况
1 2 3 4 5 6 7 8 9 | int param = 1; int a = param; // 没用__block修饰的时候,block内部捕获的外部变量 [self updateInt:a]; NSLog(@"----:%@", @(param));// 这里输出:1 // 没用__block修饰的时候,block内部实现如下 - (void)updateInt:(int)a{ a = 2;// 此时对外部变量修改是无效的 } |
被__block修饰的情况
1 2 3 4 5 6 7 8 9 10 | int param = 1; int *a = ¶m; // 用__block修饰的时候,block内部捕获的外部变量,是外部变量的指针 [self updateInt:a]; NSLog(@"----:%@", @(param));// 这里输出:2 // 用__block修饰的时候,block内部实现如下 - (void)updateInt:(int *)a{ *a = 2;// 此时对外部变量修改是有效的 } |
参考这篇文章
4、
- 如果修改的是NSMutableArray的存储内容的话,是不需要添加__block修饰的。
- 如果修改的是NSMutableArray对象的本身,那必须添加__block修饰。
参考block的变量捕获。
5、block怎么进行内存管理的?
block按照内存分布,分三种类型:全局内存中的block、栈内存中的block、堆内存中的block。
在MRC和ARC下block的分布情况不一样
MRC下:
当block内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。
当block内部引用了外部的非全局变量的时候,该block是在栈内存中的。
当栈中的block进行copy操作时,会将block拷贝到堆内存中。
通过__block修饰的变量,不会对其应用计数+1,不会造成循环引用。
ARC下:
当block内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。
当block内部引用了外部的非全局变量的时候,该block是在堆内存中的。
也就是说,ARC下只存在全局block和堆block。
通过__block修饰的变量,在block内部依然会对其引用计数+1,可能会造成循环引用。
通过__weak修饰的变量,在block内部不会对其引用计数+1,不会造成循环引用。
参考这篇文章
6、
在MRC环境中,是不可以的,strong修饰符会对修饰的变量进行retain操作,这样并不会将栈中的block拷贝到堆内存中,而执行的block是在堆内存中,所以用strong修饰的block会导致在执行的时候因为错误的内存地址,导致闪退。
在ARC环境中,是可以的,因为在ARC环境中的block只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作。
7、解决循环引用时为什么要用
__weak修饰的变量,不会出现引用计数+1,也就不会造成block强持有外部变量,这样也就不会出现循环引用的问题了。
但是,我们的block内部执行的代码中,有可能是一个异步操作,或者延迟操作,此时引用的外部变量可能会变成nil,导致意想不到的问题,而我们在block内部通过__strong修饰这个变量时,block会在执行过程中强持有这个变量,此时这个变量也就不会出现nil的情况,当block执行完成后,这个变量也就会随之释放了。
8、
一般情况在ARC环境中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法或函数的参数传递时,编译器不会做copy操作。
- block作为方法或函数的返回值时,编译器会自动完成copy操作。
- 当block赋值给通过strong或copy修饰的id或block类型的成员变量时。
- 当 block 作为参数被传入方法名带有
usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。
9、
首先我们知道,在ARC下,栈区创建的block会自动copy到堆区;而MRC下,就不会自动拷贝了,需要我们手动调用copy函数。
我们再说说block的copy操作,当block从栈区copy到堆区的过程中,也会对block内部访问的外部变量进行处理,它会调用Block_object_assign函数对变量进行处理,根据外部变量是strong还会weak对block内部捕获的变量进行引用计数+1或-1,从而达到强引用或弱引用的作用。
因此
在ARC下,由于block被自动copy到了堆区,从而对外部的对象进行强引用,如果这个对象同样强引用这个block,就会形成循环引用。
在MRC下,由于访问的外部变量是auto修饰的,所以这个block属于栈区的,如果不对block手动进行copy操作,在运行完block的定义代码段后,block就会被释放,而由于没有进行copy操作,所以这个变量也不会经过Block_object_assign处理,也就不会对变量强引用。
简单说就是:
ARC下会对这个对象强引用,MRC下不会。
参考这篇文章