iOS进阶(二)Objective-C底层原理

栏目: Objective-C · 发布时间: 7年前

内容简介: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;
22
#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
22
} OBJC2_UNAVAILABLE;

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

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

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

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

iOS进阶(二)Objective-C底层原理

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];
44
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:set];
[invocation setSelector:selector];
[invocation setArgument:&stuff atIndex:2];
[invocation invoke];
44
NSLog(@"set is : %@", set);

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

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

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

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

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

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

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

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

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

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

8. 调用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指针永远指向其对应的类。用一张图来说明下:

iOS进阶(二)Objective-C底层原理

1-2 KVO.png

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

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

参考文章:

招聘一个靠谱的iOS开发者

唐巧 iOS开发进阶书籍


以上所述就是小编给大家介绍的《iOS进阶(二)Objective-C底层原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hacking Growth

Hacking Growth

Sean Ellis、Morgan Brown / Crown Business / 2017-4-25 / USD 29.00

The definitive playbook by the pioneers of Growth Hacking, one of the hottest business methodologies in Silicon Valley and beyond. It seems hard to believe today, but there was a time when Airbnb w......一起来看看 《Hacking Growth》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具