Objective-C 对象模型及应用整理

本文参考自 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

这说的是:任何带有以指针开始并指向类结构的结构都可以被视作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个步骤:

  1. 为”class pair”分配内存 (使用objc_allocateClassPair).
  2. 添加方法或成员变量到有需要的类里 (我已经使用class_addMethod添加了一个方法).
  3. 注册类以便它能使用 (使用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 最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容