OC动态语言特性以及僵尸调试模式原理
OC的消息转发流程
1:在C++中调用一个对象不具有的方法将导致崩溃,但在OC中却并不一定。如果向一OC对象发送其不能响应的消息,则会触发OC的消息转发流程:
第一步:动态方法解析,询问能否动态添加一个方法:
+(BOOL)resolveInstanceMethod:(SEL)selector {
NSString *selectorString = NSStringFromSelector(selector);
if(***) {
class_addMethod(self,selector,(IMP)autoDictionarySetter,”v@:@”);
}
}
第二步:询问是否具有备援接收者,询问能否把这条消息转给其他接收者来处理:
- (id)forwardingTargetForSelector:(SEL)selector
第三步:完整的消息转发,消息重定向:
- (void)forwardInvocation:(NSInvocation *)invocation {
id target = [realObject1 methodSignatureForSelector:[invocation selector]] ? realObject1 : realObject2;
[invocation invokeWithTarget:target];
}
理解objc_msgSend
1:在对象上调用方法是Objective-C中经常使用的功能。用OC的术语来说,这叫做“传递消息”(pass a message)。消息有“名称”(name)或“选择子”(selector),可以接受参数,而且可能还有返回值。由于OC是在C的基础上开发的语言,现比较与C语言的函数调用方式的不同。
C语言使用“静态绑定”(static binding),也就是说,在编译器就能决定运行时所应调用的函数。如下代码:
#import <stdio.h>
void printHello() {
printf(“Hello,word!\n”);
}
void printGoodbye() {
printf(“Goodbye,word!\n”);
}
void doTheThing(int type) {
if(type == 0) {
printHello();
} else {
printGoobye();
}
return 0;
}
如果不考虑“內联”(inline),那么编译器在编译代码的时候就已经知道程序中有printHello、printGoodbye两个函数,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。若是将刚才代码写成如下形式
void doTheThing(int type) {
void (*fnc)();
if(type == 0) {
fnc = printHello;
} else {
fnc = printGoodbyte;
}
fnc();
return 0;
}
这就是使用“动态绑定”(dynamic binding)了,调用的函数直到运行期才能确定。OC正式采用了类似的思想。在OC中如果向某个对象传递消息,在底层所有的方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全与运行期决定,甚至在程序运行期间改变,这些特性使得OC成为一门真正的动态语言。
以以下代码为例:
id returnVlaue = [someObject messageName:parameter];
在本例中someObject称为“接收者”(receiver),messageName叫做“选择子”(selector)。选择子与参数称为“消息”(message)。编译器看到此消息后会把它转成标准的C语言函数调用,所调用函数原型如下:
void objc_msgSend(id self,SEL cmd,….); 一参数可变函数(variadic function)
上例:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parmeter);
上面的函数是如何最终完成函数调用的:为了完成此操作,该方法回到接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就调用实现代码,若找不到则沿着继承体系向上查找,等找到合适的方法之后再跳转。如果最后还是找不到,则执行“消息转发”(message forwarding)操作。
刚才提到,objc_msgSend等函数一旦找到应该调用的方法实现之后,就会“跳转过去”,OC对的的每个方法都可以视为简单的C函数,其原型如下:
<return_type> Class_selector(id self,SEL _cms,…)
每个类中都有一张表格,其中的指针都会指向这种函数,而选择子的名称就是查找时所用的“key”。objc_msgSend正式通过这张表找到对应方法的。
可以看出此函数原型与objc_msgSend一致,这也不是巧合,而是为了利用“尾调用优化”(tail-call optimization)技术,令“跳至方法实现”这一操作变的更简单些。
类对象概念:
1:在C++中类被看成一种数据类型,而OC中类不仅仅被看成一种数据类型,还代表着一个类对象。消息的接受者究竟是何物?是对象本身么?是对象本身么?运行期系统如何知道某个对象的类型呢?
typedef struct objc_object {
Class isa;
} *id;
typedef struct objc_class *Class;
struct objc_Class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
stuck objc_protocol_list *protocils;
}
此结构体存放类的“元数据”(methadata),此结构体首个变量也是isa指针,这说明Class本身也是OC对象。
其isa指向另一个类,叫做“元类”(methaclass),用来表述对象本身所具备的元数据。“类方法”也就定义于此处。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。
通过这张布局关系图即可执行“类型信息查询”。可以查出对象是否响应某个选择子,是否遵循某项协议,确认其“类继承体系等”。
OC中的类别(分类)(Category)概念
分类能够做到的事情主要是:即使在你不知道一个类的源码情况下,向这个类添加扩展的方法。
#import “UIViewController.h”
@interface UIViewController(CustomView)
-(void)extMethod;
@end
使用分类为类添加方法(Add Methods to Classes)。
通过在interface中声明一个额外的方法并且在implementation 中定义相同名字的方法即可。分类的名字(也就是括号括起来的CustomView)表示的是:对于声明于其他地方的这个类(UIViewController),在此处添加的方法是额外的,而不是表示这是一个新的类。但是你不可以通过分类为一个类添加额外的成员变量。
僵尸对象模式
启用这项调试功能之后,远行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到复写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
给僵尸对象发消息后,控制台会打印消息,而应用程序则会终止。打印出来的消息就像这样:
-[CFString respondsToSelector:]:message sent to deallocated instance 0xffab45….
那么僵尸对象的工作原理是什么:
系统在即将回收对象的时候如果发现环境变量启用了僵尸对象功能,那么将执行一个附加步骤。这一步就是把对象转化成僵尸对象,而不彻底回收。
如有如下代码,采用手动引用计数MRC(manual reference counter):
#import<Foundation/Foundation.h>
#import<objc/runtime.h>
@interface EOCClass : NSObject
@end
@implementation EOCClass
@end
void PrintClassInfo(id obj) {
Class cls = object_getClass(obj);
Class superCls = class_getSuperclass(cls);
NSLog(@“=== %s : %s ===”,class_getName(cls),class_getName(superCls);
}
int main(int argc, char *argv[]) {
EOCClass *obj = [[EOCClass alloc] init];
NSLog(@“Before release:”);
PrintClassInfo(obj);
[obj release];
NSLog(@“After release:”);
PrintClassInfo(obj);
}
输出:
Before release:
=== EOCClass:NSObject ===
After release:
=== _NSZombie_EOCClass:nil ===
对象由EOCClass变成了_NSZombie_EOCClass。
我们的代码中没有定义过这个类,那么这个类来自哪里呢,如果说编译器没看到一种可能变成僵尸的对象,就创建一个对应的僵尸类,就太低效率了。
_NSZombie_EOCClass实际上是在运行期间生成的,当首次碰到EOCClass类的对象变成僵尸对象时,就会创建一个类。僵尸类只是充当一个标记,协助调试罢了。
下面是一块为代码,演示僵尸类如何被创建的,以及僵尸类如何把待回收的对象转换成僵尸对象。
// Obtain the class of the object being deallocated
Class cls = object_getClass(self);
// Get the class’s name
const char *clsName = class_getName(cls);
// Prepend _NSZombie_ to the class name
const char *zombieClsName = “_NSZombie_” + clsName;
// See if the specific zombie class exists
Class zombieCls = objc_lookUpClass(zombieClsName);
// If the specific zombie class does’t exits.
// Then it needs to be created
if (!zombieCls) {
// Obtain the template zombie class called _NSZombie_
Class baseZombieCls = objc_lookUpClass(“_NSZombie_”);
// Duplicate the base zombie class,where the new class’s name is the prepended string from above,它把整个_NSZombie_类结构拷贝一份,并赋予其新的名字。
zombieCls = objc_dublicateClass(baseZombieCls,zombieClsName,0);
}
// Perform normal destruction of the object being deallocated
object_destructInstance(self);
// Set the class of the object being deallocated to the zombie class
// 修改了对象的isa指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容极其接收者的消息,然后终止应用程序。
objc_setClass(self,zombieCls);
// The class of ’self’ in now _NSZombie_OriginalClass
这个过程实在NSObject的dealloc方法所做的事。运行期系统如果发现NSZombieEnable环境变量已设置,那么就在dealloc方法中执行上述代码。执行到最后,对象所属的类已经变成_NSZombie_+原类名了。
代码的关键之处在于:对象所占内存没有(通过调用free())方法释放,因此,这块内存不可复用。虽然内存泄漏了,但这只是调试手段。
僵尸类的作用会在消息转发例程中体现出来。此类没有超类,只有一个isa实例变量。这个轻量级的类没有实现任何方法,所以发给他的全部消息都要经过“完整的消息转发机制”。