Objective-C 对象模型
首先要了解一下Objective-C中关于类和对象的定义,Cocoa中大部分对象都是NSObject的子类(NSProxy是一个例外),继承了NSObject的方法。NSObject定义如下:
@interface NSObject <NSObject>
{
Class isa;
}
NSObject可见一个对象的内存布局中第一个元素是指向类结构Class的isa指针。Class类结构定义如下:
typedef struct objc_class *Class;
typedef struct objc_object
{
Class isa;
} *id;
Class 是类结构体的别名,而id是一个objc_object对象结构体指针,objc_object内存布局中第一个元素是指向objc_class类结构体的指针。所以id可以指向任何内存布局以objc_class类结构体指针开始的对象。类结构体objc_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
struct objc_protocol_list *protocols
}
objc_class类结构体的各成员介绍如下:
isa:是一个类结构体objc_class的指针。内存以objc_class为开始的数据类型对当做对象来看待,所以说类也是对象,普通的对象叫实例对象而类叫做类对象,类对象的isa指针指向的类叫做metaclass(元类)元类也是一个对象,元类的isa指向rootmetaclass(根元类), 根元类的isa指向本身,这样就成了一个闭环。类对象存储普通成员变量和普通方法,metaclass存储静态成员变量和静态方法,也就是类变量和类方法。
super_class: 指向父类的指针。类对象和元类对象有着同样的继承关系。关于继承和isa的关系如这篇文章Objc Class And Metaclass(Objective-C类和原类)所示。
name: C常量字符串,表示类的名字。可以在运行期,通过这个名称查找到该类例如id objc_getClass(constchar *aClassName)或该类的元类id objc_getMetaClass(const char*aClassName)。
version: 类版本信息。可以在运行期对其进行修改(class_setVersion)或获取(class_getVersion)。
info: 运行期使用的一些表示位。
CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;
CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
CLS_INITIALIZED(0x4L) 表示该类已经被运行期初始化了,这个标识位只被objc_addClass 所设置;
CLS_POSING (0x8L) 表示该类被 pose 成其他的类;(poseclass 在ObjC 2.0中被废弃了);
CLS_MAPPED (0x10L)为ObjC运行期所使用
CLS_FLUSH_CACHE(0x20L) 为ObjC运行期所使用
CLS_GROW_CACHE(0x40L) 为ObjC运行期所使用
CLS_NEED_BIND(0x80L) 为ObjC运行期所使用
CLS_METHOD_ARRAY(0x100L) 该标志位指示 methodlists 是指向一个 objc_method_list 还是一个包含 objc_method_list 指针的数组,methodlists指向指针的数组的意义是方便以Category的形式添加方法。
instance_size: 该类的实例变量大小(包括从父类继承下来的实例变量)。
ivars: 指向 objc_ivar_list 的指针,存储每个实例变量的内存地址,如果该类没有任何实例变量则为 NULL
methodLists: 与 info 的一些标志位有关,CLS_METHOD_ARRAY标识位决定其指向的东西(是指向单个objc_method_list还是一个 objc_method_list 指针数组),如果 info 设置了 CLS_CLASS 则 objc_method_list 存储实例方法,如果设置的是 CLS_META 则存储类方法。
cache:指向objc_cache的指针,用来缓存最近使用的方法,以提高方法调用过程中的查找速度。
protocols: 指向 objc_protocol_list 的指针,存储该类声明要遵守的正式协议。
动态添加属性(isa swizzling)
动态的添加属性主要在Category通过以下两个方法使两个对象关联起来。
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
关联策略policy有:
enum
{
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
具体例子如下:
#import <Foundation/Foundation.h>
#include <objc/runtime.h> static const void *objectName = @"objectName";
@interface NSObject (Test)
@property (nonatomic, strong) NSString *objectName;
@end @implementation NSObject (Test)
- (NSString *)objectName
{
return objc_getAssociatedObject(self, objectName);
}
- (void)setObjectName:(NSString *)name
{
objc_setAssociatedObject(self, objectName, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end int main (int argc, const char * argv[])
{
@autoreleasepool
{
NSObject *object = [[NSObject alloc] init];
[object setObjectName:@"ObjectName"];
NSLog(@"%@",object.objectName);
[object release];
}
return 0;
}
Objective-C 消息模型
关于消息调用的一些定义
Objective-C都是通过发送消息来调用方法的。在objc_ class类结构的方法列表中存储的是Method类型,Method定义如下:
typedef struct objc_method *Method;
typedef struct objc_ method
{
SEL method_name;
char *method_types;
IMP method_imp;
};
SEL和IMP定义如下:
typedef struct objc_selector *SEL;
typedef id (*IMP)(id, SEL, ...);
其中SEL应该为该方法对应的消息名字。IMP是函数指针,指向具体方法实现的地址 。参数id是一个对象,表示是谁调用的方法。剩下的是可变的参数列表。返回值是一个id。
NSObject类中的methodForSelector:方法可以获取消息SEL对应的方法实现IMP的指针。methodForSelector:返回的指针和赋值的变量必须完全一致,包括参数和返回类型。当一个消息要多次发给某个对象的时候,可以使用methodForSelector:来减少动态查找的开销。官方例子如下:
void (*setter)(id, SEL, BOOL);
int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0; i < 1000, i++ )
{
setter(targetList[i], @selector(setFilled:), YES);
}
消息调用过程
消息调用最终都会被转换成objc_msgSend函数调用, 比如一个消息表达式[receiver message]会被转换成objc_msgSend(receiver, selector),如果有参数则为objc_msgSend(receiver, selector, arg1, arg2, …)。objc_msgSend先通过isa指针在类的dispatch table中查找对应selector的函数入口地址IMP,如果没有找到,则沿着classhierarchy(类的继承体系)寻找,直到NSObject类。当找到IMP时就将消息调用和接受的对象的指针、selector和方法指定的参数传递给IMP。最后objc_msgSend把IMP所指向的函数返回值作为返回值返回。
为了加快IMP查找的速度,Runtime System为每个类创建了一个cache, 类结构中看到有一个叫objc_cache *cache 的成员。就是用来缓存selector和对应函数入口地址IMP的映射的。
Runtime System提供的方法查找代码如下,objc-class.mm:
static Method look_up_method(Class cls, SEL sel,BOOL withCache, BOOL withResolver)
{
Method meth = NULL; if (withCache)
{
meth = _cache_getMethod(cls, sel, &_objc_msgForward_internal);
if (meth == (Method)1)
{
// Cache contains forward:: . Stop searching.
return NULL;
}
} if (!meth) meth = _class_getMethod(cls, sel); if (!meth && withResolver) meth = _class_resolveMethod(cls, sel); return meth;
}
1.首先是查找缓存。
2.缓存中没有就在类和类的继承体系结构的方法列表中查找,找到后把Method放入相应查找到的类的缓存中。
3.如果还没找到就进行动态方法决议(接下来讲)。
4.动态方法决议没有解决问题就进行消息转发流程(在动态方法决议之后)
5.最后消息转发失败就会Crash
动态方法决议
Objective-C 提供了一种名为动态方法决议的手段,使得我们可以在运行时动态地为一个 selector 提供实现。我们只要实现 +resolveInstanceMethod: 和/或 +resolveClassMethod: 方法,并在其中为指定的 selector 提供实现即可(通过调用运行时函数 class_addMethod 来添加)。这两个方法都是 NSObject 中的类方法:
+ (BOOL)resolveClassMethod:(SEL)name;
+ (BOOL)resolveInstanceMethod:(SEL)name;
参数 name 是需要被动态决议的 selector;返回值文档中说是表示动态决议成功与否。但在上面的例子中(不涉及消息转发的情况下),如果在该函数内为指定的 selector 提供实现,无论返回 YES 还是 NO,编译运行都是正确的;但如果在该函数内并不真正为 selector 提供实现,无论返回 YES 还是 NO,运行都会 crash,道理很简单,selector 并没有对应的实现,而又没有实现消息转发。resolveInstanceMethod 是为对象方法进行决议,而 resolveClassMethod 是为类方法进行决议。动态方法决议的官方例子如下:
//头文件
#import <Foundation/Foundation.h>
@interface TestObject : NSObject
@end
//实现文件
#include <objc/runtime.h>
void dynamicMethodIMP(id self, SEL _cmd)
{
// implementation ….
}
@implementation Foo
+(BOOL)resolveInstanceMethod:(SEL)name
{
if (name == @selector(MissMethod))
{
class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
+(BOOL)resolveClassMethod:(SEL)name
{
return [super resolveClassMethod:name];
}
@end
关于动态方法决议返回值:在没有提供真正消息的实现,并且提供了消息转发机制的情况下,YES 表示不进行后续的消息转发,返回 NO 则表示要进行后续的消息转发。其实在没有提供真正的动态消息决议的情况下返回YES也会进行消息转发。
动态方法决议源码objc-class.mm:
1.首先判断是进行类方法决议还是进行实例方法决议。
__private_extern__ Method _class_resolveMethod(Class cls, SEL sel)
{
Method meth = NULL; if (_class_isMetaClass(cls))
{
meth = _class_resolveClassMethod(cls, sel);
}
if (!meth)
{
meth = _class_resolveInstanceMethod(cls, sel);
} if (PrintResolving && meth)
{
_objc_inform("RESOLVE: method %c[%s %s] dynamically resolved to %p",
class_isMetaClass(cls) ? '+' : '-',
class_getName(cls), sel_getName(sel),
method_getImplementation(meth));
} return meth;
}
2.实例方法决议(类方法决议类似):
static Method _class_resolveInstanceMethod(Class cls, SEL sel)
{
BOOL resolved;
Method meth = NULL; if (!look_up_method(((id)cls)->isa, SEL_resolveInstanceMethod,YES /*cache*/, NO /*resolver*/))
{
return NULL;
} resolved = ((BOOL(*)(id, SEL, SEL))objc_msgSend)((id)cls, SEL_resolveInstanceMethod, sel); if (resolved)
{
meth = look_up_method(cls, sel, YES/*cache*/, NO/*resolver*/); if (!meth)
{
_objc_inform("+[%s resolveInstanceMethod:%s] returned YES, but "
"no new implementation of %c[%s %s] was found",
class_getName(cls),
sel_getName(sel),
class_isMetaClass(cls) ? '+' : '-',
class_getName(cls),
sel_getName(sel));
return NULL;
}
}
return meth;
}
1.首先是判断是否实现了resolveInstanceMethod,如果没有实现就直接返回NULL。
2.如果实现了就调用resolveInstanceMethod,并获取返回值。
3.如果返回值为YES,表示resolveInstanceMethod提供了selector的实现,再次查找方法列表 ,如果找到了直接返回方法,没有则给出警告,返回NULL。
消息转发
向一个对象发送它不处理的消息是一个错误,不过在报错之前,RuntimeSystem给了接收对象第二次的机会来处理消息。在这种情况下,Runtime System会向对象发一个消息,forwardInvocation:,这个消息只携带一个NSInvocation对象作为参数——这个NSInvocation对象包装了原始消息和相应参数。通过实现forwardInvocation:方法(继承于NSObject),可以给不响应的消息一个默认处理方式。正如方法名一样,通常的处理方式就是转发该消息给另一个对象:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:[anInvocation selector]])
{
[anInvocation invokeWithTarget:someOtherObject];
}
else
{
[super forwardInvocation:anInvocation];
}
}
消息转发只有在方法列表中找不到并且没有提供动态方法决议的前提下才会进行。
Method Swizzling
Method Swizzling可以搞定一件事情,那就是可以不通过继承来重写某个方法。而且还可以调用原来的方法。通常的做法是在category中添加一个方法(当然也可以是一个全新的class)。可以通过method_exchangeImplementations这个运行时方法来交换实现。示例如下:
#import <objc/runtime.h> @interface NSMutableArray (LoggingAddObject)
- (void)logAddObject:(id)aObject;
@end @implementation NSMutableArray (LoggingAddObject) + (void)load
{
Method addobject = class_getInstanceMethod(self, @selector(addObject:));
Method logAddobject = class_getInstanceMethod(self, @selector(logAddObject:));
method_exchangeImplementations(addObject, logAddObject);
} - (void)logAddObject:(id)aobject
{
[self logAddObject:aObject];
NSLog(@"Added object %@ to array %@", aObject, self);
}
@end
我们把方法交换放到了load中,这个方法只会被调用一次,而且是运行时载入。如果指向临时用一下,可以放到别的地方。注意到一个很明显的递归调用logAddObject:。这也是Method Swizzling容易把我们搞混的地方,因为我们已经交换了方法的实现,所以其实调用的是addObject:,如图:
参考链接:
官方运行时代码:http://www.opensource.apple.com/source/objc4/objc4-532/runtime/
官方Runtime指导:Objective-C RuntimeProgramming Guide
罗朝辉的iOS开发专栏:《深入浅出Cocoa系列》
唐巧的技术博客:Objective-C对象模型及应用
无网不剩的博客:(译)Objective-C的动态特性