iOS进阶:Objective-C底层原理

这篇读书笔记主要介绍了Objective-C底层的一些东西,比如Objective-C对象模型、objc_msgSend消息发送原理、方法混写(Method Swizzling)和ISA混写(ISA Swizzling)。

Objective-C对象模型

我们都知道Objective-C是一门动态性语言,这种动态性的核心是objc提供的Objective-C运行时,比如objc_msgSend就是一个核心函数,每次使用[object message]语法都会调用它。我们先来了解下Objective-C对象模型。

Objective-C是一门面向对象的编程语言,每一个对象都是一个类的实例,在Objective-C中,每一个对象都有一个名为isa的指针,指向该对象的类。每一个类描述了一系列它的实例的特点,包括成员变量的列表、成员函数的列表等。每一个对象都可以接受到消息,而对象能够接受到的消息列表保存在它所对应的类中。

注意:

每一个对象都有一个isa指针,这个指针指向的是它的类。

类中包括成员变量、成员函数列表等。

在Xcode中打开objc.h文件,会看到如下代码:

/// Represents an instance of a class.

struct objc_object {

Class isa  OBJC_ISA_AVAILABILITY;

};

通过注释我们看到objc_object代表一个对象的实例,在对象实例中我们看到了isa指针,验证了我们刚才说的话。

根据面向对象的设计原则,所有事物都应该是对象,所以在Objective-C中,每一个类实际上也是一个对象,每一个类也有一个名为isa的指针,每一个类也可以接收消息,例如代码[NSObject alloc],就是向NSObject这个类发送名为alloc的消息。

在Xcode中打开runtime.h文件,会看到如下代码:

struct objc_class {

Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__

Class super_class                                        OBJC2_UNAVAILABLE;

const char *name                                         OBJC2_UNAVAILABLE;

long version                                             OBJC2_UNAVAILABLE;

long info                                                OBJC2_UNAVAILABLE;

long instance_size                                       OBJC2_UNAVAILABLE;

struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;

struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;

struct objc_cache *cache                                 OBJC2_UNAVAILABLE;

struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

#endif

} OBJC2_UNAVAILABLE;

objc_class代表一个类,从上面代码中可以看出类中有一个isa指针的。前面说到isa指针会执行它的类,那类中的isa指针指向什么呢?因为类也是一个对象,所以它也必须是另一个类的实例,这个类就是元类(metaclass),所以isa指针指向的是它的元类。元类保存了类的方法列表。当一个类的方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,这样可以一直找到继承链的头。

如前面所说元类也是一个对象,那么元类的isa指针指向谁呢?Objective-C为了设计上的完整,所有的元类的isa指针都会指向一个根元类(root metaclass),根元类的isa指针指向自己,这样就形成一个闭环。上面说到,一个对象能够接收的消息列表是保存在它所对应的类中的。在实际编程中,我们几乎不会遇到向元类发消息的情况,那它的isa指针在实际上很少用到。

再来看看继承关系,由于类方法的定义是保存在元类中,而方法调用的规则是,如果该类没有一个方法的实现,则向它的父类继续查找。所以,为了保证父类的类方法在子类中可以被调用,所有子类的元类都会继承父类的元类,简单来说就是类对象和元类对象有着同样的继承关系。

最后用一张图对对象模型做一个总结,如下图:

1-1 对象模型.png

objc_msgSend

Objective-C运行时的核心就在于消息分派器objc_msgSend,消息分派器把选择器映射为函数指针,并调用被引用的函数。 要想理解objc_msgSend的背后原理,先来理解下NSInvocation这个类。

NSInvocation是命令模式的一种传统实现,它把一个目标、一个选择器、一个方法签名和所有的参数都塞进一个对象里,这个对象可以先存储起来,以备将来调用。当NSInvocation被调用时,它会发送信息,Objective-C运行时会找到正确的方法实现来执行。我们通过一个例子来理解下NSInvocation的作用,比如[NSObject alloc],此时会发送一个alloc消息,这条消息都包含什么内容呢?它怎么找到alloc的实现方法呢?这些都是通过NSInvocation来完成的,它包含了消息要传递的内容,也告诉了该怎么找到对应的方法实现。

解释一下什么是方法实现?一个方法实现(IMP)是一个指向具有如下签名的C函数的函数指针,注意是指针。

id function(id self, SEL _cmd, ...)

NSInvocation包含了一个目标和选择器,目标是一个可接受的对象,选择器则是被发送的消息。比如[NSObject alloc],目标就是NSObject,选择器就是alloc。一个选择器大致是一个方法的名称,之所以说是大致是因为选择器不必精确映射到方法。比如[NSString length]和[NSData length]会映射到不同方法的实现,但他们拥有相同的选择器。

NSInvocation还包含一个方法签名(NSMethodSignature),它封装了一个方法的返回类型和参数类型,记住它不包括方法名称,只有返回类型和参数类型。你可以手动创建一个方法签名,如下:

NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"@@:*"];

但是应该尽可能少使用signatureWithObjCTypes:方法,获得方法签名常用的方法是为它请求一个类或实例,比如可以使用methodSignatureForSelector:方法从实例中请求实例方法签名,或者从类中请求类方法签名。也可以使用instanceMethodSignatureForSelector:方法从一个类中获取实例方法签名。两个方法有点绕口,我们通过一个例子来看下区别:

SEL initSEL = @selector(init);

SEL allocSEL = @selector(alloc);

// 从NSString类中获取实例方法(init)的方法签名

NSMethodSignature *initSig = [NSString instanceMethodSignatureForSelector:initSEL];

// 从test实例中获取实例方法(init)的方法签名

initSig = [@"test" methodSignatureForSelector:initSEL];

// 从NSString类中获取类方法签名

NSMethodSignature *allocSig = [NSString methodSignatureForSelector:allocSEL];

最后,NSInvocation还包含了所有的参数。至此,对于[NSString length]和[NSData length]就可以通过NSInvocation对象包含的信息,找到它们分别对应的方法实现。我们来看一个具体的例子,如下:

NSMutableSet *set = [NSMutableSet set];

NSString *stuff = @"stuff";

SEL selector = @selector(addObject:);

NSMethodSignature *sig = [set methodSignatureForSelector:selector];

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];

[invocation setTarget:set];

[invocation setSelector:selector];

[invocation setArgument:&stuff atIndex:2];

[invocation invoke];

NSLog(@"set is : %@", set);

注意,第一个参数被置于索引2处,索引0是目标(self),索引1是选择器(_cmd),NSInvocation会自动设置它们。另外,必须把参数指针传递给参数,而不能传递参数本身。

接下来重点要介绍下消息传递是如何工作的?

在Objective-C中调用方法最终会翻译成调用方法实现的函数指针,并传递给这个方法实现一个对象指针、一个选择器和一组函数参数。每个Objective-C消息表达式都会转化为对objc_msgSend的调用,看下objc_msgSend的工作方式:

检查接受对象是否为nil,如果是nil,调用nil处理程序。

检查缓存中是不是已经有方法实现了,有的话,直接调用。

比较请求的选择器和类中定义的选择器,如果找到了,调用方法实现。

比较请求的选择器和父类中定义的选择器,然后是父类的父类,以此类推,如果找到了选择器,调用方法实现。

调用resolveInstanceMethod:(或resolveClassMethod)。如果它返回YES,那么重新开始。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。

调用forwardingTargetForSelector:,如果返回非nil,那就把消息发送到返回的对象上,这里不要返回self,否则会形成死循环的。

调用methodSignatureForSelector:,如果返回非nil,创建一个NSInvocation并传给forwardInvocation:。

调用doesNotRecognizeSelector:,默认的实现是抛出异常。

先看下第5步,首先可以想到的就是用resolveInstanceMethod:和resolveClassMethod:在运行时提供实现,这通常是@dynamic合成属性的处理方式。简单来说,就是需要自己实现属性的getter和setter方法,通过resolveInstanceMethod:方法来把setter方法和getter方法和属性绑定在一起。

如果第5步返回NO的话,系统接着会首先尝试一次快速转发,也就是调用forwardingTargetForSelector:,看其能否返回一个对象,如果有对象返回,就转发给返回的对象。快速转发的原理其实就是先从缓存里找下是否存在对应的选择器。

如果快速转发返回nil的话,接下来就进行普通的转发,调用forwardInvocation进行普通的转发。

objc_msgSend还有几个相关的函数:objc_msgSend_fpret、objc_msgSendSuper、objc_msgSend_stret、objc_msgSendSuper_stret。

SendSuper格式的函数很明显是把消息发送给父类,而带stret的在返回结构体时处理大部分情况。在Intel处理器上返回浮点数时,带fpret的函数处理大部分情况。

方法混写(Method Swizzling)与ISA混写(Isa Swizzling)

在Objective-C中,混写(Swizzling)是指透明地把一个东西换成另一个,我们可以利用Objective-C中的运行时来实现混写。我们先看下方法混写,Objective-C提供了以下API来动态替换类方法或实例方法的实现:

class_replaceMethod替换类方法的定义。

method_exchangeImplementations交换两个方法的实现。

method_setImplementation设置一个方法的实现。

我们来看下三者的区别:

class_replaceMethod,当需要替换的方法有可能不存在时,可以考虑使用该方法。

method_exchangeImplementations,当需要交换两个方法的实现时使用。

method_setImplementation是最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。

系统中提供的KVO使用到了isa混写,具体是怎么实现的呢?当你观察一个对象时,一个新的类会被自动创建,这个新类继承自该对象的原本的类,并且重写了被观察属性setter方法。重写setter方法会负责在调用原setter方法之前和之后,通知所有观察对象:值的更改。最后通过isa混写,把这个对象的isa指针指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

注意一点:把isa指针指向新创建的子类,被观察的对象就变成了新创建子类的对象实例,这是由于isa指针永远指向其对应的类。用一张图来说明下:

1-2 KVO.png

键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:。在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变之后,didChangeValueForKey:会被调用,继而obserValueForKey:ofObject:change:context:也会被调用。可以手动实现这些调用,但很少有人这么做。一般我们希望能控制回调的调用时机时才会这么做。

使用KVO的一个明显的优势就是零开销观察的优势,如果给定的实例没有观察者,那么KVO不会有任何消耗,因为根本没有KVO代码。而即使没有观察者,对于委托方法和NSNotification还得工作。

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

推荐阅读更多精彩内容