Objective-C对象模型及应用

本文主要介绍Objective-C对象模型的实现细节,以及Objective-C语言对象模型中对isa swizzling和method swizzling的支持。但愿本文能加深你对Objective-C对象的理解。html

ISA指针

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

在XCode中按Shift + Command + O, 而后输入NSObject.h和objc.h,能够打开NSObject的定义头文件,经过头文件咱们能够看到,NSObject就是一个包含isa指针的结构体,以下图所示:git

 

按照面向对象语言的设计原则,全部事物都应该是对象(严格来讲Objective-C并无彻底作到这一点,由于它有象int, double这样的简单变量类型)。在Objective-C语言中,每个类实际上也是一个对象。每个类也有一个名为isa的指针。每个类也能够接受消息,例如[NSObject alloc],就是向NSObject这个类发送名为alloc消息。github

在XCode中按Shift + Command + O, 而后输入runtime.h,能够打开Class的定义头文件,经过头文件咱们能够看到,Class也是一个包含isa指针的结构体,以下图所示。(图中除了isa外还有其它成员变量,但那是为了兼容非2.0版的Objective-C的遗留逻辑,你们能够忽略它。)编程

由于类也是一个对象,那它也必须是另外一个类的实列,这个类就是元类(metaclass)。元类保存了类方法的列表。当一个类方法被调用时,元类会首先查找它自己是否有该类方法的实现,若是没有,则该元类会向它的父类查找该方法,直到一直找到继承链的头。安全

元类(metaclass)也是一个对象,那么元类的isa指针又指向哪里呢?为了设计上的完整,全部的元类的isa指针都会指向一个根元类(root metaclass)。根元类(root metaclass)自己的isa指针指向本身,这样就行成了一个闭环。上面提到,一个对象可以接收的消息列表是保存在它所对应的类中的。在实际编程中,咱们几乎不会遇到向元类发消息的状况,那它的isa指针在实际上不多用到。不过这么设计保证了面向对象的干净,即全部事物都是对象,都有isa指针。服务器

咱们再来看看继承关系,因为类方法的定义是保存在元类(metaclass)中,而方法调用的规则是,若是该类没有一个方法的实现,则向它的父类继续查找。因此,为了保证父类的类方法能够在子类中能够被调用,因此子类的元类会继承父类的元类,换而言之,类对象和元类对象有着一样的继承关系。网络

我很想把关系说清楚一些,可是这块儿确实有点绕,下面这张图或许可以让你们对isa和继承的关系清楚一些app

该图中,最让人困惑的莫过于Root Class了。在实现中,Root Class是指NSObject,咱们能够从图中看出:编程语言

  1. NSObject类包括它的对象实例方法。

  2. NSObject的元类包括它的类方法,例如alloc方法。

  3. NSObject的元类继承自NSObject类。

  4. 一个NSObject的类中的方法同时也会被NSObject的子类在查找方法时找到。

类的成员变量

若是把类的实例当作一个C语言的结构体(struct),上面说的isa指针就是这个结构体的第一个成员变量,而类的其它成员变量依次排列在结构体中。排列顺序以下图所示(图片来自《iOS 6 Programming Pushing the Limits》):

为了验证该说法,咱们在XCode中新建一个工程,在main.m中运行以下代码:

#import <UIKit/UIKit.h>

@interface Father : NSObject {
   int _father;
}

@end

@implementation Father

@end

@interface Child : Father {
   int _child;
}

@end

@implementation Child

@end


int main(int argc, char * argv[])
{

 Child * child = [[Child alloc] init];
 @autoreleasepool {
     // ...
 }
}

咱们将断点下在 @autoreleasepool 处,而后在Console中输入p *child,则能够看到Xcode输出以下内容,这与咱们上面的说法一致。

(lldb) p *child
(Child) $0 = {
 (Father) Father = {
   (NSObject) NSObject = {
     (Class) isa = Child
   }
   (int) _father = 0
 }
 (int) _child = 0
}

可变与不可变

由于对象在内存中的排布能够当作一个结构体,该结构体的大小并不能动态变化。因此没法在运行时动态给对象增长成员变量。

相对的,对象的方法定义都保存在类的可变区域中。Objective-C 2.0并未在头文件中将实现暴露出来,但在Objective-C 1.0中,咱们能够看到方法的定义列表是一个名为 methodLists的指针的指针(以下图所示)。经过修改该指针指向的指针的值,就能够实现动态地为某一个类增长成员方法。这也是Category实现的原理。同时也说明了为何Category只可为对象增长成员方法,却不能增长成员变量。

须要特别说明一下,经过objc_setAssociatedObject 和 objc_getAssociatedObject方法能够变相地给对象增长成员变量,但因为实现机制不同,因此并非真正改变了对象的内存结构。

除了对象的方法能够动态修改,由于isa自己也只是一个指针,因此咱们也能够在运行时动态地修改isa指针的值,达到替换对象整个行为的目的。不过该应用场景较少。

系统相关API及应用

isa 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个方法的实现时使用。

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

以上3个方法的源码在这里,感兴趣的同窗能够读一读。

使用示例

咱们在开发客户端的笔记功能时,须要使用系统的UIImagePickerController。可是,咱们发现,在iOS6.0.2系统下,系统提供的UIImagePickerController在iPad横屏下有转屏的Bug,形成其方向错误。具体的Bug详情能够见这里

为了修复该Bug,咱们须要替换UIImagePickerController的以下2个方法

- (BOOL)shouldAutorotate;
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation;

咱们先实现了一个名为ImagePickerReplaceMethodsHolder的类,用于定义替换后的方法和实现。以下所示:

// ImagePickerReplaceMethodsHolder.h
@interface ImagePickerReplaceMethodsHolder : NSObject

- (BOOL)shouldAutorotate;
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation;

@end

// ImagePickerReplaceMethodsHolder.m
@implementation ImagePickerReplaceMethodsHolder

- (BOOL)shouldAutorotate {
   return NO;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
   return UIInterfaceOrientationPortrait;
}


@end

开源界的使用

有少许不明真相的同窗觉得苹果在审核时会拒绝App使用以上API,这实际上是对苹果的误解。使用如上API是安全的。另外,开源界也对以上方法都适当的使用。例如:

  • 著名的网络库AFNetworking。AFNetworking网络库(v1.x版本)使用了class_replaceMethod方法(AFHTTPRequestOperation.m文件第105行)

  • Nimbus。Nimbus是著名的工具类库,它在其core模块中提供了NIRuntimeClassModifications.h文件,用于提供上述API的封装。

  • 国内的大众点评iOS客户端。该客户端使用了他们本身开发的基于Wax修改而来的WaxPatch,WaxPatch能够实现经过服务器更新来动态修改客户端的逻辑。而WaxPatch主要是修改了wax中的wax_instance.m文件,在其中加入了class_replaceMethod来替换原始实现,从而实现修改客户端的原有行为。

总结

经过本文,咱们了解到了Objective-C语言的对象模型,以及Objective-C语言对象模型中对isa swizzling和method swizzling的支持。本文也经过具体的实例代码和开源项目,让咱们对该对象模型提供的动态性有了更加深入的认识。

相关文章
相关标签/搜索