在上一章里面,笔者向大家介绍了在Objective-C里面的几个非常重要的概念, 简单的说就是SEL,Class和IMP。我们知道Objective-C是C语言的扩展,有了这3个概念还有我们以前讲过的继承和封装的概念,Objective-C发生了翻天覆地的变化,既兼容C语言的高效特性又实现了面向对象的功能。
Objective-C从本质上来说,还是C语言的。那么内部究竟是怎样实现SEL,Class和IMP,还有封装和继承的?为了解答这个问题,笔者决定在本章向大家概要的介绍一下Objective-C的最主要的一个类,NSObject。
不过说实在话,如果同学们觉得本章的内容比较晦涩难懂的话,不阅读本章的内容丝毫不会对写程序产生任何不良的影响,但是如果掌握了本章的内容的话,对加深对Objective-C的理解,对于今后笔者将要讲述的内容而言,将会是一个极大的促进。
6.1,本章程序的执行结果
在本章里面,我们将要继续使用我们在前面几章已经构筑好的类Cattle和Bull。由于在现在的Xcode版本里面,把一些重要的东西比如说Class的原型定义都放到了LIB文件里面,所以这些东西的具体的定义,对于我们来说是不可见的。
我们首先把第4章的代码打开,然后打开“Cattle.h” 文件,把鼠标移动到“NSObject”上面,单击鼠标右键,在弹出菜单里面选择“Jump to Definition”。然后会弹出一个小菜单,我们选择“interface NSObject” 。我们可以看到如下代码
Class isa;
我们知道了,所谓的NSObject里面只有一个变量,就是Class类型的isa。isa的英文的意思就是is a pointer的意思。也就是说NSObject里面只有一个实例变量isa。好的,我们需要知道Class是一个什么东西,我们把鼠标移动到“Class”上面,单击鼠标右键,在弹出菜单里面选择“Jump to Definition”,我们看到了如下的代码:
typedef struct objc_object {
Class isa;
} *id;
...
我们在这里知道了,Class实际上是一个objc_class的指针类型,我们把鼠标移动到“objc_class”上面,单击鼠标右键,在弹出菜单里面选择“Jump to Definition”,发现我们还是在这个窗口里面,Xcode并没有把我们带到objc_class的定义去,所以我们无从知道objc_class内部究竟是一个什么样的东西。
笔者顺便提一下,大家也许注意到了id的定义,id实际上是objc_object结构的一个指针,里面只有一个元素那就是Class。那么根据上面我们看到的,所谓的id就是objc_class的指针的指针。让我们回忆一下下面的代码:
这句话是在初始化和实例话cattle对象,这个过程,实际上可以理解为,runtime为我们初始化好了Class的指针,并且把这个指针返回给我们。我们初始化对象完成了之后,实际上我们得到的对象就是一个指向这个对象的Class指针。
让我们在回过头来说说这个神秘的Class,我们无法在Xcode里面看到Class也就是objc_class的定义。庆幸的是这部分的定义是GCC代码,是开源的。笔者下载了开源的代码之后,把开源的代码作了一些小小的调整,然后把Class的定义等等放到了我们的工程文件里面去,通过类型转化之后,我们终于可以看到Class,SEL,还有isa等等过去对我们来说比较“神秘”的东西的真正面目。
我们在前面几章里面在每一个章的第一节里面都要介绍一下本章程序执行结果的屏幕拷贝,本章也是一样,但是本章的执行结果非常简单。因为对于本章而言重点应该是放在对NSObject机制的理解上。
图6-1,本章程序运行结果
大家看到本章程序的运行结果的屏幕拷贝的时候,也许会觉得很无趣,因为单单从结果画面,我们没有发现任何令人感到很有兴趣的东西,相反,都是同学们已经很熟悉的一些老面孔。但是本章所要讲述的东西也许是同学们在其他语言里面从来没有遇到过的东西,这些东西将会令人感到新鲜和激动。
6.2,实现步骤
第一步,按照我们在第2章所述的方法,新建一个项目,项目的名字叫做06-NSObject。如果你是第一次看本篇文章,请到这里参看第二章的内容。
第二步,按照我们在第4章的4.2节的第二,三,四步所述的方法,把在第4章已经使用过的“Cattle.h”,“Cattle.m”,“Bull.h”还有“Bull.m” 导入本章的项目里面。如果你没有第4章的代码,请到这里下载。如果你没有阅读第4章的内容,请参看这里。
第三步,把鼠标移动到项目浏览器上面的“Source”上面,然后在弹出的菜单上面选择“Add”,然后在子菜单里面选择“New File”,然后在新建文件对话框的左侧最下面选择“Other”,然后在右侧窗口选择“Empty File”,选择“Next”,在“New File”对话框里面的“File Name”栏内输入“MyNSObject.h”。然后输入(或者是拷贝也可以,因为这是C的代码,如果你很熟悉C语言的话,可以拷贝一下节省时间)如下代码:
typedef const struct objc_selector
{
void *sel_id;
const char *sel_types;
} *MySEL;
typedef struct my_objc_object {
struct my_objc_class* class_pointer;
} *myId;
typedef myId (*MyIMP)(myId, MySEL, );
typedef char *STR; /* String alias */
typedef struct my_objc_class *MetaClass;
typedef struct my_objc_class *MyClass;
struct my_objc_class {
MetaClass class_pointer;
struct my_objc_class* super_class;
const char* name;
long version;
unsigned long info;
long instance_size;
struct objc_ivar_list* ivars;
struct objc_method_list* methods;
struct sarray * dtable;
struct my_objc_class* subclass_list;
struct my_objc_class* sibling_class;
struct objc_protocol_list *protocols;
void* gc_object_type;
};
typedef struct objc_protocol {
struct my_objc_class* class_pointer;
char *protocol_name;
struct objc_protocol_list *protocol_list;
struct objc_method_description_list *instance_methods, *class_methods;
} Protocol;
typedef void* retval_t;
typedef void(*apply_t)(void);
typedef union arglist {
char *arg_ptr;
char arg_regs[sizeof (char*)];
} *arglist_t;
typedef struct objc_ivar* Ivar_t;
typedef struct objc_ivar_list {
int ivar_count;
struct objc_ivar {
const char* ivar_name;
const char* ivar_type;
int ivar_offset;
} ivar_list[1];
} IvarList, *IvarList_t;
typedef struct objc_method {
MySEL method_name;
const char* method_types;
MyIMP method_imp;
} Method, *Method_t;
typedef struct objc_method_list {
struct objc_method_list* method_next;
int method_count;
Method method_list[1];
} MethodList, *MethodList_t;
struct objc_protocol_list {
struct objc_protocol_list *next;
size_t count;
Protocol *list[1];
};
第四步,打开06-NSObject.m文件,输入如下代码并且保存
#import "Cattle.h"
#import "Bull.h"
#import "MyNSObject.h"
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
id cattle = [Cattle new];
id redBull = [Bull new];
SEL setLegsCount_SEL = @selector(setLegsCount:);
IMP cattle_setLegsCount_IMP = [cattle methodForSelector:setLegsCount_SEL];
IMP redBull_setLegsCount_IMP = [redBull methodForSelector:setLegsCount_SEL];
[cattle setLegsCount:4];
[redBull setLegsCount:4];
[redBull setSkinColor:@"red"];
Class cattle_class = cattle->isa;
MyClass my_cattle_class = cattle->isa;
SEL say = @selector(saySomething);
IMP cattle_sayFunc = [cattle methodForSelector:say];
cattle_sayFunc(cattle, say);
Class redBull_class = redBull->isa;
MyClass my_redBull_class = redBull->isa;
IMP redBull_sayFunc = [redBull methodForSelector:say];
redBull_sayFunc(redBull, say);
[pool drain];
return 0;
}
第五步,在06-NSObject.m文件的窗口的“[pool drain];”代码的左侧单击一下窗口的边框,确认一下是否出现一个蓝色的小棒棒,如果有的话那么断点被选择好了。如图6-2所示
图6-2,选择执行断点
第六步,选择Xcode上面的菜单的“Run”,然后选择“Debuger” ,在Debuger窗口里面选择“Build and Go”。
好的,大家就停在这里,不要做其他的操作,我们把程序中断在程序几乎执行到最后的断点上,我们将要通过Debuger来看看Objective-C内部究竟发生了什么样的奇妙的魔法。
注意在从编译到执行的过程当中,会出现一些警告。由于本章程序指示用来阐述一些NSObject内部的东西,所以请忽略掉这些警告。当然,我们在写自己的程序的时候,编译产生的警告一般是不能被忽略的。
6.3,超类方法的调用
我们现在打开“06-NSObject.m”文件,发现下面的代码:
IMP cattle_setLegsCount_IMP = [cattle methodForSelector:setLegsCount_SEL];
IMP redBull_setLegsCount_IMP = [redBull methodForSelector:setLegsCount_SEL];
这一段代码,对同学们来说不是什么新鲜的内容了,我们在第5章里面已经讲过,这个是SEL和IMP的概念。我们在这里取得了cattle对象和redBull对象的setLegsCount:的函数指针。
如果大家现在已经不在Debuger里面的话,那么请选择Xcode菜单里面的,“Run”然后选择“Debuger” 。
我们注意到在Debuger里面,cattle_setLegsCount_IMP的地址和redBull_setLegsCount_IMP是完全一样的,如图6-3所示:
图6-3,cattle_setLegsCount_IMP和redBull_setLegsCount_IMP的地址。
注意由于环境和执行的时候的内存情况不同,所以同学们的电脑上显示的地址的数值可能和图6-3的数值不一样。
cattle_setLegsCount_IMP和redBull_setLegsCount_IMP的地址完全一样,说明他们使用的是相同的代码段。这种结果是怎样产生的呢?大家请打开“MyNSObject.h”,参照下列代码:
MetaClass class_pointer;
struct my_objc_class* super_class;
const char* name;
long version;
unsigned long info;
long instance_size;
struct objc_ivar_list* ivars;
struct objc_method_list* methods;
struct sarray * dtable;
struct my_objc_class* subclass_list;
struct my_objc_class* sibling_class;
struct objc_protocol_list *protocols;
void* gc_object_type;
};
笔者在这里把开源代码的名字的定义加上了“my_”前缀,仅仅是为了区分一下。“MyNSObject.h”里面的代码问题很多,笔者从来没有也不会在实际的代码里面使用这段代码,使用这些代码的主要目的是为了向大家讲解概念,请大家忽略掉代码里面的种种问题。
我们注意到这里的methods变量,里面包存的就是类的方法名字(SEL)定义,方法的指针地址(IMP)。当我们执行
的时候,runtime会通过dtable这个数组,快速的查找到我们需要的函数指针,查找函数的定义如下:
objc_msg_lookup(id receiver, SEL op)
{
if(receiver)
return sarray_get(receiver->class_pointer->dtable, (sidx)op);
else
return nil_method;
好的,现在我们的cattle_setLegsCount_IMP没有问题了,那么redBull_setLegsCount_IMP怎么办?在Bull类里面我们并没有定义实例方法setLegsCount:,所以在Bull的Class里面,runtime难道找不到setLegsCount:么?答案是,是的runtime直接找不到,因为我们在Bull类里面根本就没有定义setLegsCount:。
但是,从结果上来看很明显runtime聪明的找到了setLegsCount:的地址,runtime是怎样找到的?答案就在:
在自己的类里面没有找到的话,runtime会去Bull类的超类cattle里面去寻找,庆幸的是它成功的在cattle类里面runtime找到了setLegsCount:的执行地址入口,所以我们得到了redBull_setLegsCount_IMP。 redBull_setLegsCount_IMP和cattle_setLegsCount_IMP都是在Cattle类里面定义的,所以他们的代码的地址也是完全一样的。
我们现在假设,如果runtime在cattle里面也找不到setLegsCount:呢?没有关系,cattle里面也有超类的,那就是NSObject。所以runtime会去NSObject里面寻找。当然,NSObject不会神奇到可以预测我们要定义setLegsCount:所以runtime是找不到的。
在这个时候,runtime 并没有放弃最后的努力,再没有找到对应的方法的时候,runtime会向对象发送一个forwardInvocation:的消息,并且把原始的消息以及消息的参数打成一个NSInvocation的一个对象里面,作为forwardInvocation:的唯一的参数。 forwardInvocation:本身是在NSObject里面定义的,如果你需要重载这个函数的话,那么任何试图向你的类发送一个没有定义的消息的话,你都可以在forwardInvocation:里面捕捉到,并且把消息送到某一个安全的地方,从而避免了系统报错。
笔者没有在本章代码中重写forwardInvocation:,但是在重写forwardInvocation:的时候一定要注意避免消息的循环发送。比如说,同学们在A类对象的forwardInvocation里面,把A类不能响应的消息以及消息的参数发给B类的对象;同时在B类的forwardInvocation里面把B类不能响应的消息发给A类的时候,容易形成死循环。当然一个人写代码的时候不容易出现这个问题,当你在一个工作小组里面做的时候,如果你重写forwardInvocation:的时候,需要和小组的其他人达成共识,从而避免循环调用。
6.4,重载方法的调用
让我们继续关注“06-NSObject.m”文件,请大家参考一下下面的代码:
2 MyClass my_cattle_class = cattle->isa;
3 SEL say = @selector(saySomething);
4 IMP cattle_sayFunc = [cattle methodForSelector:say];
5 cattle_sayFunc(cattle, say);
6
7 Class redBull_class = redBull->isa;
8 MyClass my_redBull_class = redBull->isa;
9
10 IMP redBull_sayFunc = [redBull methodForSelector:say];
11 redBull_sayFunc(redBull, say);
本节的内容和6.3节的内容比较类似,关于代码部分笔者认为就不需要解释了,如果同学们有所不熟悉的话,可以参考一下第5章的内容。
在我们的Cattle类和Bull类里面,都有saySometing这个实例方法。我们知道只要方法的定义相同,那么它们的SEL是完全一样的。我们根据一个SEL say,在cattle和redBull对象里面找到了他们的函数指针。根据6.3节的讲述,我们知道当runtime接收到寻找方法的时候,会首先在这个类里面寻找,寻找到了之后寻找的过程也就结束了,同时把这个方法的IMP返回给我们。所以,在上面的代码里面的cattle_sayFunc和redBull_sayFunc应该是不一样的,如图6-4所示:
图6-4, cattle_sayFunc和redBull_sayFunc的地址
6.5,超类和子类中的Class
在类进行内存分配的时候,对于一个类而言,runtime需要找到这个类的超类,然后把超类的Class的指针的地址赋值给isa里面的super_class。所以,我们的cattle里面的Class应该和redBull里面的Class里面的super_class应该是完全相同的,请参照图6-5:
图6-5, cattle里面的Class和redBull里面的Class里面的super_class
6.6,实例变量的内存分配的位置
我们先来回忆一下对象是怎样被创建的。创建对象的时候,类的内容需要被调入到内存当中我们称之为内存分配(Allocation),然后需要把实体变量进行初始化(Initialization),当这些步骤都结束了之后,我们的类就被实例化了,我们把实例化完成的类叫做对象(Object)。
对于内存分配的过程,runtime需要知道分配多少内存还有各个实例变量的位置。我们回到“MyNSObject.h”,参照如下代码:
2 typedef struct objc_ivar_list {
3 int ivar_count;
4 struct objc_ivar {
5 const char* ivar_name;
6 const char* ivar_type;
7 int ivar_offset;
8 } ivar_list[1];
9 } IvarList, *IvarList_t;
我们仔细看看第5行的ivar_name,顾名思义这个是实例变量的名字,第6行的ivar_type是实例变量的类型,第7行的ivar_offset,这个就是位置的定义。runtime从类的isa里面取得了这些信息之后就知道了如何去分配内存。我们来看看图6-6:
图6-6,实例变量在内存中的位置
在cattle里面,我们看到了第一个实例变量是isa,第二个就是我们定义的legsCount。其中isa是超类的变量,legsCount是Cattle类的变量。我们可以看出来,总是把超类的变量放在前头,然后是子类的变量。
那么对于redBull而言是什么样子呢?我们来看看图6-7
图6-7,redBull里面的实例变量的位置
我们通过图6-7可以发现redBull的Class里面的skinColor的位置偏移是8,很明显,runtime为isa和legsCount预留了2个位置。
6.7本章总结
非常感谢大家!
在本章里面,笔者通过一个小小的“把戏”为同学们揭开了NSObject的神秘的面纱。本章的内容,虽然对理解Objective-C不是必需的,但是对以后的章节的内容的理解会有一个非常好的辅助作用,希望同学们花费一点点心思和时间阅读一下。
另外,笔者需要再次强调一下,由于笔者没有得到官方的正式的文档说明,所以不能保证本章的内容是完整而且准确的,本章内容仅供大家参考和娱乐,希望大家谅解。
相关推荐
### NSObject扩展:深入理解与应用 在Objective-C中,`NSObject`是所有类的基类,它提供了许多基本的功能和属性。对`NSObject`进行扩展(Category)是一种常见的编程技巧,用于向现有类添加额外的功能,而无需修改...
2. **KVC和KVO**:`NSObject`实现了KVC,使得我们可以通过键值访问对象的属性,即使这些属性没有公开的getter和setter。KVO则允许我们在属性改变时接收到通知,实现数据绑定。这两个特性是基于Objective-C的动态性,...
在这个项目中,"OC简易通信录实现"涉及了多个关键知识点,包括数据模型设计、用户界面构建、数据存储和检索、以及基本的事件处理。下面将对这些知识点进行详细解释。 首先,我们需要建立数据模型来表示联系人。在OC...
本教程将深入探讨如何使用OC来实现一个基础的通讯录功能,涵盖了增、删、改、查以及显示联系人列表等核心操作。 首先,通讯录的核心数据结构是联系人模型。我们可以创建一个名为`Contact`的类,它包含姓名(name)...
NSObject脑图
在Objective-C(OC)中,协议(@protocol)主要用于定义对象间交互的接口,它们类似于接口,但不提供默认实现。然而,在Swift中,我们可以为协议提供默认实现,使得遵循该协议的类无需手动实现所有方法。这篇教程将...
利用runtime对NSObject进行分类扩展,解决字典转Model的问题 原理:http://www.jianshu.com/p/71454166c397 github:https://github.com/cccgoodboy/CCModel 喜欢请给个star 谢谢!
"NSObject+RunTimeUtilit"这个项目显然就是针对Runtime特性的一个实用工具包,通过扩展NSObject类来提供便捷的方法来操作私有属性。 首先,我们要理解什么是Objective-C的Runtime。Objective-C的运行时系统是其动态...
通过以上步骤,我们就成功地将PYTheme集成到了Swift项目中,实现了基于`NSObject`分类的主题更换功能。这个框架不仅提供了简洁的API,还确保了主题切换的高效性和一致性,极大地提高了开发效率和用户体验。在实际...
本篇将详细介绍OC代理机制的原理,并提供简单的实现步骤和代码实例,适合初学者入门学习。 ### 1. 代理机制原理 代理模式的核心是协议(Protocol),它定义了一组方法,这些方法必须由代理对象来实现。协议类似于...
- Swift的类需要继承自`NSObject`,或者实现一个`@objc`协议,才能在OC中创建实例。 - OC可以通过`NSClassFromString`动态创建Swift类的实例,并通过`performSelector:`方法调用方法。 - OC可以使用`@selector`...
在iOS开发中,为了实现游戏逻辑和界面的高效配合,开发者往往需要在C++和OC之间进行灵活的调用。本文将深入探讨C++和Objective-C互相调用的原理与实践。 首先,理解C++和Objective-C之间的差异是至关重要的。C++是...
- Swift的枚举和结构体默认不是桥接的,如果需要在OC中使用,需要添加`@objc`并实现`Equatable`和`Hashable`协议。 - OC的枚举可以通过`NS_ENUM`或`NS_OPTIONS`在Swift中使用。 9. **Category** - Swift不支持...
IOS 开发学习总结 Objective-C 面向对象之——成员变量、模拟类变量、单例模式 Objective-C 中的变量根据定义位置的不同,可以分为三大类:成员变量、局部变量和全局变量。成员变量是 Objective-C 中的实例变量,不...
在处理未知或未实现的消息时,`NSObject`提供了两个关键的方法:`methodSignatureForSelector:` 和 `forwardInvocation:`。这两个方法是Objective-C运行时系统的重要组成部分,对于理解和扩展动态语言的特性至关重要...
可以使用Runtime的`NSObject`的`associatedObject`机制,将定时器与对象关联,然后在对象即将被释放时(如`dealloc`方法中)移除定时器。 最后,我们来看看Method Swizzling,这是Runtime中最常用也最具威力的技术...
**封装**是面向对象编程的核心特性之一,它涉及到将数据(属性)和操作这些数据的方法(函数)捆绑到一个独立的单元——对象中。在OC中,我们可以使用`@interface`和`@implementation`来定义和实现类,通过`@...
3. **Category的实现**:Category可以为已有的类添加方法,其实现依赖Runtime。Category的方法会被添加到类的方法列表中,可以在运行时动态调用。 五、Runtime在实际开发中的应用 - **AOP(面向切面编程)**:利用...
本教程将深入探讨如何利用Runtime来实现NSCoding的自动归档和解档,从而避免手动实现每个需要归档的对象的encodeWithCoder:和initWithCoder:方法。 首先,让我们了解NSCoding协议。该协议定义了两个方法:`- (void)...
二、Runtime 主要功能 1. 动态类型:Objective-C 的消息发送机制依赖于Runtime来解析消息。当发送消息时,Runtime会根据接收者对象的isa指针找到对应的类,然后查找并调用相应的方法。 2. KVC(Key-Value Coding):...