Objective-C对象模型及应用

唐巧 2013-10-16 09:54:25
分享我新写的一篇博客文章《Objective-C对象模型及应用》,原文链接:http://blog.devtang.com/blog/2013/10/15/objective-c-object-model/

前言

原创文章,转载请注明出自唐巧的技术博客。

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

ISA指针

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

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



按照面向对象语言的设计原则,所有事物都应该是对象(严格来说Objective-C并没有完全做到这一点,因为它有象int, double这样的简单变量类型)。在Objective-C语言中,每一个类实际上也是一个对象。每一个类也有一个名为isa的指针。每一个类也可以接受消息,例如[NSObject alloc],就是向NSObject这个类发送名为alloc消息。

在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和继承的关系清楚一些(该图片来自这里)



该图中,最让人困惑的莫过于Root Class了。在实现中,Root Class是指NSObject,我们可以从图中看出:

NSObject类包括它的对象实例方法。
NSObject的元类包括它的类方法,例如alloc方法。
NSObject的元类继承自NSObject类。
一个NSObject的类中的方法同时也会被NSObject的子类在查找方法时找到。
类的成员变量

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



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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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输出如下内容,这与我们上面的说法一致。

1
2
3
4
5
6
7
8
9
10
(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.

类似的,使用isa swizzling的技术的还有系统提供的Key-Value Coding(KVC)。 (谢谢大家指出错误,KVC并没有使用到isa swizzling)

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 最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。
使用示例

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

为了修复该Bug,我们需要替换UIImagePickerController的如下2个方法

1
2
- (BOOL)shouldAutorotate;
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation;
我们先实现了一个名为ImagePickerReplaceMethodsHolder的类,用于定义替换后的方法和实现。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ImagePickerReplaceMethodsHolder.h
@interface ImagePickerReplaceMethodsHolder : NSObject

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

@end

// ImagePickerReplaceMethodsHolder.m
@implementation ImagePickerReplaceMethodsHolder

- (BOOL)shouldAutorotate {
return NO;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}


@end
然后,我们在调用处,判断当前的iOS版本,对于[iOS6.0, iOS6.1)之间的版本,我们将UIImagePickerController的有问题的方法替换。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self hackForImagePicker];
});
}

+ (void)hackForImagePicker {
// fix bug of image picker under iOS 6.0
// http://stackoverflow.com/questions/12522491/crash-on-presenting-uiimagepickercontroller-under-ios-6-0
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"6.0")
&& SYSTEM_VERSION_LESS_THAN(@"6.1")) {
Method oldMethod1 = class_getInstanceMethod([UIImagePickerController class], @selector(shouldAutorotate));
Method newMethod1 = class_getInstanceMethod([ImagePickerReplaceMethodsHolder class], @selector(shouldAutorotate));
method_setImplementation(oldMethod1, method_getImplementation(newMethod1));

Method oldMethod2 = class_getInstanceMethod([UIImagePickerController class], @selector(preferredInterfaceOrientationForPresentation));
Method newMethod2 = class_getInstanceMethod([ImagePickerReplaceMethodsHolder class], @selector(preferredInterfaceOrientationForPresentation));
method_setImplementation(oldMethod2, method_getImplementation(newMethod2));
}
}
通过如上代码,我们就针对iOS特定版本的有问题的系统库函数打了Patch,使问题得到解决。

开源界的使用

有少量不明真相的同学以为苹果在审核时会拒绝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的支持。本文也通过具体的实例代码和开源项目,让我们对该对象模型提供的动态性有了更加深刻的认识。

后记

文章发表后,一些同行指出在ARM64的CPU下,isa的内部结构有变化。这点我是知道的,不过希望以后再撰文讨论。感兴趣的同学可以查看苹果今年WWDC2013的视频:《Session 404 Advanced in Objective-C》。
...全文
257 1 打赏 收藏 转发到动态 举报
写回复
用AI写文章
1 条回复
切换为时间正序
请发表友善的回复…
发表回复
小杨在玩iOS 2013-10-29
  • 打赏
  • 举报
回复
好深奥的样子
OBJECTIVE-C编程之道 IOS设计模式解析电子书+源代码 《Objective-C编程之道:iOS设计模式解析》是基于iOS的软件开发指南。书中应用GoF的经典设计模式,介绍了如何在代码中应用创建型模式、结构型模式和行为模式,如何设计模式以巩固应用程序,并通过设计模式实例介绍MVC在CocoaTouch框架中的工作方式。   《Objective-C编程之道:iOS设计模式解析》适用于那些已经具备Objective-C基础、想利用设计模式来提高软件开发效率的中高级iOS开发人员。 第一部分 设计模式初体验 第1章 你好,设计模式 1.1 这是一本什么书 1.2 开始前的准备 1.3 预备知识 1.4 似曾相识的设计 1.5 设计模式的起源——模型、视图和控制器 1.5.1 在模型对象中封装数据和基本行为 1.5.2 使用视图对象向用户展示信息 1.5.3 用控制器对象联系起模型和视图 1.5.4 作为复合设计模式的MVC 1.6 影响设计的几个问题 1.6.1 针对接口编程,而不是针对实现编程 1.6.2 @protocol与抽象基类 1.6.3 对象组合与类继承 1.7 本书用到的对象和类 1.7.1 类图 1.7.2 对象图 1.8 本书如何安排模式的讲解 1.9 总结 第2章 案例分析:设计一个应用程序 2.1 想法的概念化 2.2 界面外观的设计 2.3 架构设计 2.3.1 视图管理 2.3.2 如何表现涂鸦 2.3.3 如何表现保存的涂鸦图 2.3.4 用户操作 2.4 所用设计模式的回顾 2.5 总结 第二部分 对象创建 第3章 原型 3.1 何为原型模式 3.2 何时使用原型模式 3.3 浅复制与深复制 3.4 使用Cocoa Touch框架中的对象复制 3.5 为Mark聚合体实现复制方法 3.6 将复制的Mark用作“图样模板” 3.7 总结 第4章 工厂方法 4.1 何为工厂方法模式 4.2 何时使用工厂方法 4.3 为何这是创建对象的安全方法 4.4 在TouchPainter中生成不同画布 4.5 在Cocoa Touch框架中应用工厂方法 4.6 总结 第5章 抽象工厂 5.1 把抽象工厂应用到TouchPainter应用程序 5.2 在Cocoa Touch框架中使用抽象工厂 5.3 总结 第6章 生成器 6.1 何为生成器模式 6.2 何时使用生成器模式 6.3 构建追逐游戏中的角色 6.4 总结 第7章 单例 7.1 何为单例模式 7.2 何时使用单例模式 7.3 在Objective-C中实现单例模式 7.4 子类化Singleton 7.5 线程安全 7.6 在Cocoa Touch框架中使用单例模式 7.6.1 使用UIApplication类 7.6.2 使用UIAccelerometer类 7.6.3 使用NSFileManager类 7.7 总结 第三部分 接口适配 第8章 适配器 8.1 何为适配器模式 8.2 何时使用适配器模式 8.3 委托 8.4 用Objective-C协议实现适配器模式 8.5 用Objective-C的块在iOS 4中实现适配器模式 8.5.1 块引用的声明 8.5.2 块的创建 8.5.3 把块用作适配器 8.6 总结 第9章 桥接 9.1 何为桥接模式 9.2 何时使用桥接模式 9.3 创建iOS版虚拟仿真器 9.4 总结 第10章 外观 10.1 何为外观模式 10.2 何时使用外观模式 10.3 为子系统的一组接口提供简化的接口 10.4 在TouchPainter应用程序中使用外观模式 10.5 总结 第四部分 对象去耦 第11章 中介者 11.1 何为中介者模式 11.2 何时使用中介者模式 11.3 管理TouchPainter应用程序中的视图迁移 11.3.1 修改迁移逻辑的困难 11.3.2 集中管理UI交通 11.3.3 在Interface Builder中使用CoordinatingController 11.4 总结 第12章 观察者 12.1 何为观察者模式 12.2 何时使用观察者模式 12.3 在模型-视图-控制器中使用观察者模式 12.4 在Cocoa Touch框架中使用观察者模式 12.4.1 通知 12.4.2 键-值观察 12.5 在TouchPainter中更新CanvasView上的线条 12.6 总结 第五部分 抽象集合 第13章 组合 13.1 何为组合模式 13.2 何时使用组合模式 13.3 理解TouchPainter中Mark的使用 13.4 在Cocoa Touch框架中使用组合模式 13.5 总结 第14章 迭代器 14.1 何为迭代器模式 14.2 何时使用迭代器模式 14.3 在Cocoa Touch框架中使用迭代器模式 14.3.1 NSEnumerator 14.3.2 基于块的枚举 14.3.3 快速枚举 14.3.4 内部枚举 14.4 遍历Scribble的顶点 14.5 总结 第六部分 行为扩展 第15章 访问者 15.1 何为访问者模式 15.2 何时使用访问者模式 15.3 用访问者绘制TouchPainter中的Mark 15.4 访问者的其他用途 15.5 能不能用范畴代替访问者模式 15.6 总结 第16章 装饰 16.1 何为装饰模式 16.2 何时使用装饰模式 16.3 改变对象的“外表”和“内容” 16.4 为UIImage创建图像滤镜 16.4.1 通过真正的子类实现装饰 16.4.2 通过范畴实现装饰 16.5 总结 第17章 责任链 17.1 何为责任链模式 17.2 何时使用责任链模式 17.3 在RPG游戏中使用责任链模式 17.4 总结 第七部分 算法封装 第18章 模板方法 18.1 何为模板方法模式 18.2 何时使用模板方法 18.3 利用模板方法制作三明治 18.4 保证模板方法正常工作 18.5 向模板方法增加额外的步骤 18.6 在Cocoa Touch框架中使用模板方法 18.6.1 UIView类中的定制绘图 18.6.2 Cocoa Touch框架中的其他模板方法实现 18.7 总结 第19章 策略 19.1 何为策略模式 19.2 何时使用策略模式 19.3 在UITextField中应用验证策略 19.4 总结 第20章 命令 20.1 何为命令模式 20.2 何时使用命令模式 20.3 在Cocoa Touch框架中使用命令模式 20.3.1 NSInvocation对象 20.3.2 NSUndoManager 20.4 在TouchPainter中实现撤销与恢复 20.4.1 使用NSUndoManager实现绘图与撤销绘图 20.4.2 自制绘图与撤销绘图的基础设施 20.4.3 允许用户触发撤销与恢复 20.5 命令还能做什么 20.6 总结 第八部分 性能与对象访问 第21章 享元 21.1 何为享元模式 21.2 何时使用享元模式 21.3 创建百花池 21.4 总结 第22章 代理 22.1 何为代理模式 22.2 何时使用代理模式 22.3 用虚拟代理懒加载图像 22.4 在Cocoa Touch框架中使用代理模式 22.5 总结 第九部分 对象状态 第23章 备忘录 23.1 何为备忘录模式 23.2 何时使用备忘录模式 23.3 在TouchPainter中使用备忘录模式 23.3.1 涂鸦图的保存 23.3.2 涂鸦图的恢复 23.3.3 ScribbleMemento的设计与实现 23.4 Cocoa Touch框架中的备忘录模式 23.5 总结
此书介绍: 关于Swift Swift是一种新的编程语言,用于iOS和OS X的应用程序,建立在 最佳的C和Objective-C中,没有C的兼容性的限制。迅速 采用安全的编程模式,并增加了现代的功能,使 编程更简单,更灵活,更有趣。斯威夫特的洗涮,通过支持 成熟和备受宠爱的可可和可可触摸框架,是一个 机会重新构想如何的软件开发工作。 Swift一直是苹果公司多年的酝酿。苹果公司奠定了Swift的基础 推进我们现有的编译器,调试器和框架的基础设施。我们 简化存储管理,自动引用计数( ARC) 。我们的 框架堆栈,建立在基金会和可可的坚实基础,一直 现代化和整个标准化。 Objective-C语言本身已经进化到 支撑块,收集文字和模块,使框架采用 现代语言技术不中断。由于这个基础, 我们现在可以引入苹果软件的未来一种新的语言 发展。 Swift适合熟悉Objective-C语言的开发人员。它采用的可读性 Objective-C中的命名参数和Objective-C的动态对象的权力 模型。它提供了无缝接入现有的Cocoa框架和混合和 匹配的互操作性与Objective-C代码。从这个共同建设 地面上,Swift引入了许多新的特性和统一的程序和 语言的面向对象的部分。 Swift是友好的新的程序。它是第一个获得工业质量体系 编程语言,如表情和愉快的脚本 语言。它支持操场,一个创新的功能,它允许 程序员尝试斯威夫特代码并立即看到结果, 没有建立和运行一个应用程序的开销。 斯威夫特结合了最好的现代语言思维与智慧 更宽的苹果工程师文化。编译器是针对性能进行了优化, 和语言的发展进行了优化,在不影响 无论是。它的设计从“你好,世界”扩展到整个操作系统。 这一切都使得雨燕的开发者和苹果健全的未来投资。 Swift是一个很好的方式来写的iOS和OS X的应用程序,并且将继续发展 新的特性和功能。我们的目标雨燕是雄心勃勃。我们不能 等着看你用它创造什么

29,027

社区成员

发帖
与我相关
我的任务
社区描述
主要讨论与iOS相关的软件和技术
社区管理员
  • iOS
  • 大熊猫侯佩
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧