本文参考自 http://blog.devtang.com/2013/10/15/objective-c-object-model/ 以及 http://ios.jobbole.com/81657/ 。 纯粹是对文章内容的整理和整合,供自己以后查阅,版权归原作者所有。
isa 指针
什么数据结构才能称之为对象?
每个对象都有类。这是面向对象的基本概念,但是在Objective-C中,它对数据结构也一样。含有一个指针且该指针可以正确指向类的数据结构,都可以被视作为对象。
在Objective-C中,对象的类是isa指针决定的。isa指针指向对象所属的类。
实际上,Objective-C中对象最基本的定义是这样的:
这说的是:任何带有以指针开始并指向类结构的结构都可以被视作objc_object。
我们还可以看到,Class 也是一个包含 isa 指针的结构体。(图中除了 isa 外还有其它成员变量,但那是为了兼容非 2.0 版的 Objective-C 的遗留逻辑,大家可以忽略它。)
Objective-C中对象最重要的特点是你可以发送消息给它们:
[@"stringValue" writeToFile:@"/file.txt" atomically:YES encoding:NSUTF8StringEncoding error:NULL];
这能工作是因为Objective-C对象(这儿是NSCFString)在发送消息时,运行时库会追寻着对象的isa指针得到了对象所属的类(这儿是NSCFString类)。这个类包含了能应用于这个类的所有实例方法和指向超类的指针以便可以找到父类的实例方法。运行时库检查这个类和其超类的方法列表,找到一个匹配这条消息的方法(在上面的代码里,是NSString类的writeToFile:atomically:encoding:error方法)。运行时库基于那个方法调用函数(IMP)。重点就是类要定义这个你发送给对象的消息。
什么是元类(meta class)?
你可以发送消息给一个类:
NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];
在这个示例里,defaultStringEncoding被发送给了 NSString类。
因此Objective-C中每个类本身(Class)也是一个对象。如上面图Class所展示的,这意味着类结构必须以一个isa指针开始,从而可以和objc_object在二进制层面兼容。为了调用类里的方法,类的isa指针必须指向包含这些类方法的类结构体。这个类结构体就是元类 (metaclass)。
简单说就是:
- 当你给对象发送消息时,消息是在寻找这个对象的类的方法列表。
- 当你给类发消息时,消息是在寻找这个类的元类的方法列表。
元类是必不可少的,因为它存储了类的类方法。每个类都必须有独一无二的元类,因为每个类都有独一无二的类方法。每个对象的isa所指的是一个元类的实例。那么这个实例所属的类是如何定义的呢?这就引出了:
元类的类是什么?
元类,就像之前的类一样,它也是一个对象。你也可以调用它的方法。自然的,这就意味着他必须也有一个类。
如类结构图所示,所有的元类都使用根元类(继承体系中处于顶端的类的元类)作为他们的类。这就意味着所有NSObject的子类(大多数类)的元类都会以NSObject的元类作为他们的类。
根据这个规则,所有的元类使用根元类作为他们的类,根元类的元类则就是它自己。也就是说基类的元类的isa指针指向他自己。
验证
下面的代码在运行时创建了一个NSError的子类,并且添加了一个方法:
Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
objc_registerClassPair(newClass);
ReportFunction函数就是添加的实例方法,具体实现如下
void ReportFunction(id self, SEL _cmd)
{
NSLog(@"This object is %p.", self);
NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
Class currentClass = [self class];
for (int i = 1; i < 5; i++)
{
NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
currentClass = object_getClass(currentClass);
}
NSLog(@"NSObject's class is %p", [NSObject class]);
NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
}
表面上看来,这相当简单。在运行时创建一个类只需要3个步骤:
- 为”class pair”分配内存 (使用objc_allocateClassPair).
- 添加方法或成员变量到有需要的类里 (我已经使用class_addMethod添加了一个方法).
- 注册类以便它能使用 (使用objc_registerClassPair).
这里解释一下 SEL和IMP
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
其中,Apple源码里并没有给出objc_selector的定义,这里用例子来说明:
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo
{
NSLog(@"IMP: -[NSObject(Sark) foo]");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
SEL sel = @selector(foo);
NSLog(@"%s", (char *)sel);
NSLog(@"%p", sel);
const char *selName = [@"foo" UTF8String];
SEL sel2 = sel_registerName(selName);
NSLog(@"%s", (char *)sel2);
NSLog(@"%p", sel2);
}
return 0;
}
输出结果:
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
因此可以发现,Objective-C在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID。只要方法名称相同,那么它们的ID就是相同的。
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么它的SEL就是一样的。每一个方法都对应着一个SEL。编译器会根据每个方法的方法名为那个方法生成唯一的SEL。这些SEL组成了一个Set集合,当我们在这个集合中查找某个方法时,只需要去找这个方法对应的SEL即可。而SEL本质是一个字符串,所以直接比较它们的地址即可。
那么什么是IMP呢?
看其定义, IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。
那么什么是方法列表呢?
方法列表就是在图objc_object里,objc_class结构中的成员 struct objc_method_list **methodLists.
重点参考自:http://chun.tips/blog/2014/11/06/bao-gen-wen-di-objective[nil]c-runtime(3)[nil]-xiao-xi-he-category/
运行ReportFunction,我们需要创建一个动态实例来创建类调用report方法:
id instanceOfNewClass = [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
[instanceOfNewClass performSelector:@selector(report)];
[instanceOfNewClass release];
这里没有声明report方法,但我使用performSelector:调用它,所以编译器不会给出警告。函数使用object_getClass跟踪isa指针,因为isa指针是类的保护成员(你不能直接接收其他对象的isa指针)。ReportFunction不使用类方法,因为在类对象里调用类方法不能返回元类,它会再次返回这个类(因此[NSString class]会返回NSString类而不是NSString元类).
ReportFunction函数会沿着isa进行检索,来告诉我们class,meta-class以及meta-class的class是什么样的情况:
This object is 0x10010c810.
Class is RuntimeErrorSubclass, and super is NSError.
Following the isa pointer 1 times gives 0x10010c600
Following the isa pointer 2 times gives 0x10010c630
Following the isa pointer 3 times gives 0x7fff71038480
Following the isa pointer 4 times gives 0x7fff71038480
NSObject's class is 0x7fff710384a8
NSObject's meta class is 0x7fff71038480
观察isa到达过的地址的值:
- 对象的地址是 0x10010c810
- 类的地址是 0x10010c600
- 元类的地址是 0x10010c630
- 根元类(NSObject的元类)的地址是 0x7fff71038480
- NSObject元类的类是它本身.
这些地址的值并不重要,重要的是它们说明了文中讨论的从类到meta-class到NSObject的meta-class的整个流程。
系统相关API及应用
ias swizzling的应用
系统提供的 KVO 的实现,就利用了动态地修改 isa 指针的值的技术。
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the [class] method to determine the class of an object instance.
Method Swizzling API 说明
Objective-C 提供了以下 API 来动态替换类方法或实例方法的实现:
class_replaceMethod 替换类方法的定义
method_exchangeImplementations 交换 2 个方法的实现
method_setImplementation 设置 1 个方法的实现
这 3 个方法有一些细微的差别,给大家介绍如下:
- class_replaceMethod
在苹果的文档(如下图所示)中能看到,它有两种不同的行为。当类中没有想替换的原方法时,该方法会调用class_addMethod
来为该类增加一个新方法,也因为如此,class_replaceMethod在调用时需要传入types参数,而method_exchangeImplementations和method_setImplementation却不需要。
- method_exchangeImplementations 的内部实现相当于调用了 2 次method_setImplementation方法,从苹果的文档中能清晰地了解到(如下图所示)
从以上的区别我们可以总结出这 3 个 API 的使用场景:
- class_replaceMethod, 当需要替换的方法可能有不存在的情况时,可以考虑使用该方法。
- method_exchangeImplementations,当需要交换 2 个方法的实现时使用。(常用于使用自定义的类的方法来hack掉iOS SDK提供的方法)
- method_setImplementation 最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。