Objective-C对象模型及应用

OCT 15TH, 2013

前言

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

本文主要介绍Objective-C对象模型的实现细节,以及Objective-C语言对象模型中对isa swizzlingmethod 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,我们可以从图中看出:

  1. NSObject类包括它的对象实例方法。
  2. NSObject的元类包括它的类方法,例如alloc方法。
  3. NSObject的元类继承自NSObject类。
  4. 一个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_exchangeImplementationsmethod_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 swizzlingmethod swizzling的支持。本文也通过具体的实例代码和开源项目,让我们对该对象模型提供的动态性有了更加深刻的认识。

后记

文章发表后,一些同行指出在ARM64的CPU下,isa的内部结构有变化。这点我是知道的,不过希望以后再撰文讨论。感兴趣的同学可以查看苹果今年WWDC2013的视频:《Session 404 Advanced in Objective-C》。

参考链接

  • https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html
  • http://www.sealiesoftware.com/blog/archive/2009/04/14/objc_explain_Classes_and_metaclasses.html
  • http://www.devalot.com/articles/2011/11/objc-object-model.html
  • http://www.cocoawithlove.com/2010/01/what-is-meta-class-in-objective-c.html
  • http://www.sealiesoftware.com/blog/archive/2009/04/14/objc_explain_Classes_and_metaclasses.html
  • gunstep的实现源码
  • http://algorithm.com.au/downloads/talks/objective-c-internals/objective-c-internals.pdf
  • http://opensource.apple.com/source/objc4/objc4-532/runtime/
  • https://github.com/AFNetworking/AFNetworking
  • https://github.com/jverkoey/nimbus
  • https://github.com/mmin18/WaxPatch

转载于:https://www.cnblogs.com/yanghuhu/p/3375585.html

Objective-C对象模型及应用相关推荐

  1. Objective C内存管理之理解autorelease------面试题

    Objective C内存管理之理解autorelease Autorelease实际上只是把对release的调用延迟了,对于每一个Autorelease,系统只是把该Object放入了当前的Aut ...

  2. 【C++】C++对象模型:对象内存布局详解(C#实例)

    C++对象模型:对象内存布局详解 0.前言 C++对象的内存布局.虚表指针.虚基类指针解的探讨,参考. 1.何为C++对象模型? 引用<深度探索C++对象模型>这本书中的话: 有两个概念可 ...

  3. python对象模型 ruby_使用JRuby/Jython实现Ruby/Python的互操作性?

    不,那不行.至少不是你想的那样.在 Jython和JRuby之间的互操作性与CPython和YARV之间的工作方式相同:它们都运行在同一个平台上,因此可以使用该平台彼此通信.在 在CPython和YA ...

  4. Sharepoint学习笔记—ECMAScript对象模型系列-- 8、组与用户操作(一)

    这里总结一下关于使用ECMAscript对象模型来操作Goup与User的常用情况,因为内容较多,所以拆分为两个部分,这部分主要内容如下:      1.取得当前Sharepoint网站所有的Grou ...

  5. BOM 浏览器对象模型和DOM 文档对象模型

    浏览器对象模型BOM 1. 浏览器对象模型介绍 BOM(Browser Object Model) 是指浏览器对象模型,是用于描述这种对象与对象之间层次关系的模型,浏览器对象模型提供了独立于内容的.可 ...

  6. 对象模型创建SharePoint2010多选字段SPFieldMultiChoice

    在使用页面方式创建SharePoint 2010的选项(Choice)字段时,选项字段的显示方式有3种:下拉列表.单选按钮.多选.但是如果使用对象模型创建时,下拉列表和单选按钮只能使用SPFieldC ...

  7. Kubernetes对象模型

    在之前的文章已经讲到了很多Kubernets对象,包括pod,service,deployment等等.Kubernets对象是一种持久化,表示集群状态的实体.它是一种声明式的意图的记录,一般使用ya ...

  8. MAVEN项目对象模型,原来找Bug也不用如此费时费事。

    解释之前,提个小问题 假如你正在Eclipse下开发两个Java项目,姑且把它们称为A.B,其中A项目中的一些功能依赖于B项目中的某些类,那么如何维系这种依赖关系的呢? 很简单,这不就是跟我们之前写程 ...

  9. JVM内存结构、内存模型 、对象模型那些事

    作者:Hollis,阿里资深攻城狮 来自:Hollis Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点.而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚.比如本文我 ...

  10. ORACLE8对象模型

    ORACLE8对于对象模型采取较为现实和谨慎的态度,使用了对象/关系模型,即在完全支持传统关系模型的基础上,为对象机制提供了有限的支持.ORACLE8不仅能够处理传统的表结构信息,而且能够管理由C++ ...

最新文章

  1. Intellij IDEA神器常用技巧七-超好用插件推荐
  2. SpringCloud接入EDAS——服务发现篇
  3. 一、查看MVC4还是MVC5
  4. 反思Code Review的注意点与目的
  5. 《网络对抗》Exp5 MSF基础应用
  6. 打印为带边框的表格_会这些Excel打印技巧的人,2秒搞定别人大半天的工作!
  7. FFmpeg 内存H264流发布rtmp
  8. Spark清华镜像下载
  9. c语言比matlab慢很多,为什么我的Python脚本与Matlab相比速度太慢?
  10. 关于屏幕分辨率适配的教程
  11. ffmpeg代码实现往视频文件里面叠加文字
  12. MATLAB导入txt和excel文件技巧汇总
  13. docker提交腾讯云标准模式
  14. IT行业里的热门技术 | 热门IT技术项目分享 | 详细介绍一下机器人技术
  15. java设计功能怎么实现代码_Java中的门面设计模式及如何用代码实现
  16. vue下载excel表格模板和导入excel表格数据
  17. QST《Linux基础》学习笔记
  18. PyCharm中import显示灰色解决方案
  19. 计算机考研803学校,803计算机考研大纲
  20. EasyRecovery2022数据免费恢复文件

热门文章

  1. 比特币交易的脚本如何执行
  2. html+css响应式布局
  3. mysql新手创建数据库_用MySQL创建数据库和数据库表(新手必看)
  4. linux值centos7安装docker
  5. Java NIO和IO的区别
  6. LayaAir UI 组件 # RadioGroup 单选框按钮组
  7. python源代码文件_Python代码编译与反编译
  8. PrintService类打印
  9. java之Calendar类
  10. lightslider-支持移动触摸的轻量级jQuery幻灯片插件