上篇文章:Runtime在工作中的运用
1.objc在向一个对象发送消息时,发生了什么?
objc在向一个对象发送消息时,runtime会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果一直到根类还没找到,转向拦截调用,走消息转发机制,一旦找到 ,就去执行它的实现
IMP
。
详解:请看Runtime在工作中的运用 第二章Runtime消息机制;
2.objc中向一个nil对象发送消息将会发生什么?
如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会崩溃。
详解: 如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil);
如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0;
如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;
如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。
3.objc中向一个对象发送消息[obj foo]和objc_msgSend()
函数之间有什么关系?
在objc编译时,[obj foo] 会被转意为:
objc_msgSend(obj, @selector(foo));
。
详解:请看Runtime在工作中的运用 第二章Runtime消息机制;
4.什么时候会报unrecognized selector的异常?
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,会进入消息转发阶段,如果消息三次转发流程仍未实现,则程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。
详解:请看Runtime在工作中的运用 第三章Runtime方法调用流程;
5.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量;
能向运行时创建的类中添加实例变量;
1.因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用.所以不能向存在的类中添加实例变量。
2.运行时创建的类是可以添加实例变量,调用class_addIvar函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
6.给类添加一个属性后,在类结构体里哪些元素会发生变化?
instance_size :实例的内存大小;objc_ivar_list *ivars:属性列表
7.一个objc对象的isa的指针指向什么?有什么作用?
指向他的类对象,从而可以找到对象上的方法
详解:下图很好的描述了对象,类,元类之间的关系:
图中实线是 super_class指针,虚线是isa指针。
- Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
- 每个Class都有一个isa指针指向唯一的Meta class
- Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
- 每个Meta class的isa指针都指向Root class (meta)。
8.[self class] 与 [super class]
下面的代码输出什么?
- @implementation Son : Father
- - (id)init
- {
- self = [super init];
- if (self) {
- NSLog(@"%@", NSStringFromClass([self class]));
- NSLog(@"%@", NSStringFromClass([super class]));
- }
- return self;
- }
- @end
- 复制代码
NSStringFromClass([self class]) = Son NSStringFromClass([super class]) = Son
详解:这个题目主要是考察关于 Objective-C 中对 self 和 super 的理解。
self 是类的隐藏参数,指向当前调用方法的这个类的实例;
super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者。不同点在于:super 会告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。
当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。
在调用[super class]
的时候,runtime会去调用objc_msgSendSuper
方法,而不是objc_msgSend
;
- OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
-
- /// Specifies the superclass of an instance.
- struct objc_super {
- /// Specifies an instance of a class.
- __unsafe_unretained id receiver;
-
- /// Specifies the particular superclass of the instance to message.
- #if !defined(__cplusplus) && !__OBJC2__
- /* For compatibility with old objc-runtime.h header */
- __unsafe_unretained Class class;
- #else
- __unsafe_unretained Class super_class;
- #endif
- /* super_class is the first class to search */
- };
- 复制代码
在objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是当前类的父类super_class。
objc_msgSendSuper的工作原理应该是这样的: 从objc_super结构体指向的superClass父类的方法列表开始查找selector,找到后以objc->receiver去调用父类的这个selector。注意,最后的调用者是objc->receiver,而不是super_class!
那么objc_msgSendSuper最后就转变成:
- // 注意这里是从父类开始msgSend,而不是从本类开始
- objc_msgSend(objc_super->receiver, @selector(class))
-
- /// Specifies an instance of a class. 这是类的一个实例
- __unsafe_unretained id receiver;
-
-
- // 由于是实例调用,所以是减号方法
- - (Class)class {
- return object_getClass(self);
- }
- 复制代码
由于找到了父类NSObject里面的class方法的IMP,又因为传入的入参objc_super->receiver = self。self就是son,调用class,所以父类的方法class执行IMP之后,输出还是son,最后输出两个都一样,都是输出son。
9.runtime如何通过selector找到对应的IMP地址?
每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
10._objc_msgForward函数是做什么的,直接调用它将会发生什么?
_objc_msgForward
是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发。
详解:_objc_msgForward
在进行消息转发的过程中会涉及以下这几个方法:
resolveInstanceMethod:
方法 (或resolveClassMethod:
)。forwardingTargetForSelector:
方法methodSignatureForSelector:
方法forwardInvocation:
方法doesNotRecognizeSelector:
方法
具体请见:请看Runtime在工作中的运用 第三章Runtime方法调用流程;
11. runtime如何实现weak变量的自动置nil?知道SideTable吗?
runtime 对注册的类会进行布局,对于 weak 修饰的对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
更细一点的回答:
1.初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2.添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3.释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
SideTable结构体是负责管理类的引用计数表和weak表,
详解:参考自《Objective-C高级编程》一书 1.初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
- {
- NSObject *obj = [[NSObject alloc] init];
- id __weak obj1 = obj;
- }
- 复制代码
当我们初始化一个weak变量时,runtime会调用 NSObject.mm 中的objc_initWeak函数。
- // 编译器的模拟代码
- id obj1;
- objc_initWeak(&obj1, obj);
- /*obj引用计数变为0,变量作用域结束*/
- objc_destroyWeak(&obj1);
- 复制代码
通过objc_initWeak
函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak
函数释放该变量(obj1)。
2.添加引用时:objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
objc_initWeak
函数将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak
函数。
- obj1 = 0;
- obj_storeWeak(&obj1, obj);
- 复制代码
也就是说:
weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)
然后obj_destroyWeak
函数将0(nil)作为参数,调用objc_storeWeak
函数。
- objc_storeWeak(&obj1, 0);
- 复制代码
前面的源代码与下列源代码相同。
- // 编译器的模拟代码
- id obj1;
- obj1 = 0;
- objc_storeWeak(&obj1, obj);
- /* ... obj的引用计数变为0,被置nil ... */
- objc_storeWeak(&obj1, 0);
- 复制代码
objc_storeWeak
函数把第二个参数的赋值对象(obj)的内存地址作为键值,将第一个参数__weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。如果第二个参数(obj)为0(nil),那么把变量(obj1)的地址从weak表中删除。
由于一个对象可同时赋值给多个附有__weak修饰符的变量中,所以对于一个键值,可注册多个变量的地址。
可以把objc_storeWeak(&a, b)
理解为:objc_storeWeak(value, key)
,并且当key变nil,将value置nil。在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。
3.释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:
1.调用objc_release
2.因为对象的引用计数为0,所以执行dealloc
3.在dealloc中,调用了_objc_rootDealloc函数
4.在_objc_rootDealloc中,调用了object_dispose函数
5.调用objc_destructInstance
6.最后调用objc_clear_deallocating
对象被释放时调用的objc_clear_deallocating函数:
1.从weak表中获取废弃对象的地址为键值的记录
2.将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
3.将weak表中该记录删除
4.从引用计数表中删除废弃对象的地址为键值的记录
总结:
其实Weak表是一个hash(哈希)表,Key是weak所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。
12.isKindOfClass 与 isMemberOfClass
下面代码输出什么?
- @interface Sark : NSObject
- @end
- @implementation Sark
- @end
- int main(int argc, const char * argv[]) {
- @autoreleasepool {
- BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
- BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
- BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
- BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
- NSLog(@"%d %d %d %d", res1, res2, res3, res4);
- }
- return 0;
- }
- 复制代码
1000
详解:
在isKindOfClass
中有一个循环,先判断class
是否等于meta class
,不等就继续循环判断是否等于meta class
的super class
,不等再继续取super class
,如此循环下去。
[NSObject class]
执行完之后调用isKindOfClass
,第一次判断先判断NSObject
和 NSObject
的meta class
是否相等,之前讲到meta class
的时候放了一张很详细的图,从图上我们也可以看出,NSObject
的meta class
与本身不等。接着第二次循环判断NSObject
与meta class
的superclass
是否相等。还是从那张图上面我们可以看到:Root class(meta)
的superclass
就是 Root class(class)
,也就是NSObject本身。所以第二次循环相等,于是第一行res1输出应该为YES。
同理,[Sark class]
执行完之后调用isKindOfClass
,第一次for循环,Sark的Meta Class
与[Sark class]
不等,第二次for循环,Sark Meta Class
的super class
指向的是 NSObject Meta Class
, 和Sark Class
不相等。第三次for循环,NSObject Meta Class
的super class
指向的是NSObject Class
,和 Sark Class
不相等。第四次循环,NSObject Class
的super class
指向 nil, 和 Sark Class
不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO。
isMemberOfClass
的源码实现是拿到自己的isa指针和自己比较,是否相等。 第二行isa 指向 NSObject
的 Meta Class
,所以和 NSObject Class
不相等。第四行,isa指向Sark的Meta Class
,和Sark Class
也不等,所以第二行res2和第四行res4都输出NO。
13.使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放。
详解:
- 1、调用 -release :引用计数变为零
- 对象正在被销毁,生命周期即将结束.
- 不能再有新的 __weak 弱引用,否则将指向 nil.
- 调用 [self dealloc]
-
- 2、 父类调用 -dealloc
- 继承关系中最直接继承的父类再调用 -dealloc
- 如果是 MRC 代码 则会手动释放实例变量们(iVars)
- 继承关系中每一层的父类 都再调用 -dealloc
-
- >3、NSObject 调 -dealloc
- 只做一件事:调用 Objective-C runtime 中object_dispose() 方法
-
- >4. 调用 object_dispose()
- 为 C++ 的实例变量们(iVars)调用 destructors
- 为 ARC 状态下的 实例变量们(iVars) 调用 -release
- 解除所有使用 runtime Associate方法关联的对象
- 解除所有 __weak 引用
- 调用 free()
- 复制代码
14. 什么是method swizzling(俗称黑魔法)
简单说就是进行方法交换
详解:请看Runtime在工作中的运用 第五章Runtime方法交换;
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。
换方法的几种实现方式
- 利用 method_exchangeImplementations 交换两个方法的实现
- 利用 class_replaceMethod 替换方法的实现
- 利用 method_setImplementation 来直接设置某个方法的IMP
15.Compile Error / Runtime Crash / NSLog…?
下面的代码会?Compile Error
/ Runtime Crash
/ NSLog…
?
- @interface NSObject (Sark)
- + (void)foo;
- - (void)foo;
- @end
-
- @implementation NSObject (Sark)
- - (void)foo {
- NSLog(@"IMP: -[NSObject (Sark) foo]");
- }
- @end
-
- // 测试代码
- [NSObject foo];
- [[NSObject new] performSelector:@selector(foo)];
- 复制代码
IMP: -[NSObject(Sark) foo] ,全都正常输出,编译和运行都没有问题。
详解:
这道题和上一道题很相似,第二个调用肯定没有问题,第一个调用后会从元类中查找方法,然而方法并不在元类中,所以找元类的superclass
。方法定义在是NSObject
的Category
,由于NSObject
的对象模型比较特殊,元类的superclass
是类对象,所以从类对象中找到了方法并调用。
感谢:
霜神、iOS程序犭袁、sunnyxx