Objective-C 对象模型

首先要了解一下Objective-C中关于类和对象的定义,Cocoa中大部分对象都是NSObject的子类(NSProxy是一个例外),继承了NSObject的方法。NSObject定义如下:

@interface NSObject <NSObject>
{
Class isa;
}

NSObject可见一个对象的内存布局中第一个元素是指向类结构Class的isa指针。Class类结构定义如下:

typedef struct objc_class *Class;
typedef struct objc_object
{
Class isa;
} *id;

Class 是类结构体的别名,而id是一个objc_object对象结构体指针,objc_object内存布局中第一个元素是指向objc_class类结构体的指针。所以id可以指向任何内存布局以objc_class类结构体指针开始的对象。类结构体objc_class定义具体如下:

struct objc_class
{
Class isa;
Class super_class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars
struct objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
}

objc_class类结构体的各成员介绍如下:

isa:是一个类结构体objc_class的指针。内存以objc_class为开始的数据类型对当做对象来看待,所以说类也是对象,普通的对象叫实例对象而类叫做类对象,类对象的isa指针指向的类叫做metaclass(元类)元类也是一个对象,元类的isa指向rootmetaclass(根元类), 根元类的isa指向本身,这样就成了一个闭环。类对象存储普通成员变量和普通方法,metaclass存储静态成员变量和静态方法,也就是类变量和类方法。

super_class: 指向父类的指针。类对象和元类对象有着同样的继承关系。关于继承和isa的关系如这篇文章Objc Class And Metaclass(Objective-C类和原类)所示。

name: C常量字符串,表示类的名字。可以在运行期,通过这个名称查找到该类例如id objc_getClass(constchar *aClassName)或该类的元类id objc_getMetaClass(const char*aClassName)。

version: 类版本信息。可以在运行期对其进行修改(class_setVersion)或获取(class_getVersion)。

info: 运行期使用的一些表示位。

CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;

CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;

CLS_INITIALIZED(0x4L) 表示该类已经被运行期初始化了,这个标识位只被objc_addClass 所设置;

CLS_POSING (0x8L) 表示该类被 pose 成其他的类;(poseclass 在ObjC 2.0中被废弃了);

CLS_MAPPED (0x10L)为ObjC运行期所使用

CLS_FLUSH_CACHE(0x20L) 为ObjC运行期所使用

CLS_GROW_CACHE(0x40L) 为ObjC运行期所使用

CLS_NEED_BIND(0x80L) 为ObjC运行期所使用

CLS_METHOD_ARRAY(0x100L) 该标志位指示 methodlists 是指向一个 objc_method_list 还是一个包含 objc_method_list 指针的数组,methodlists指向指针的数组的意义是方便以Category的形式添加方法。

instance_size: 该类的实例变量大小(包括从父类继承下来的实例变量)。

ivars: 指向 objc_ivar_list 的指针,存储每个实例变量的内存地址,如果该类没有任何实例变量则为 NULL

methodLists: 与 info 的一些标志位有关,CLS_METHOD_ARRAY标识位决定其指向的东西(是指向单个objc_method_list还是一个 objc_method_list 指针数组),如果 info 设置了 CLS_CLASS 则 objc_method_list  存储实例方法,如果设置的是 CLS_META 则存储类方法。

cache:指向objc_cache的指针,用来缓存最近使用的方法,以提高方法调用过程中的查找速度。

protocols: 指向 objc_protocol_list 的指针,存储该类声明要遵守的正式协议。

动态添加属性(isa swizzling)

动态的添加属性主要在Category通过以下两个方法使两个对象关联起来。

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);

关联策略policy有:

enum
{
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

具体例子如下:

#import <Foundation/Foundation.h>
#include <objc/runtime.h> static const void *objectName = @"objectName";
@interface NSObject (Test)
@property (nonatomic, strong) NSString *objectName;
@end @implementation NSObject (Test)
- (NSString *)objectName
{
return objc_getAssociatedObject(self, objectName);
}
- (void)setObjectName:(NSString *)name
{
objc_setAssociatedObject(self, objectName, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end int main (int argc, const char * argv[])
{
@autoreleasepool
{
NSObject *object = [[NSObject alloc] init];
[object setObjectName:@"ObjectName"];
NSLog(@"%@",object.objectName);
[object release];
}
return 0;
}

Objective-C 消息模型

关于消息调用的一些定义

Objective-C都是通过发送消息来调用方法的。在objc_ class类结构的方法列表中存储的是Method类型,Method定义如下:

typedef struct objc_method *Method;
typedef struct objc_ method
{
SEL method_name;
char *method_types;
IMP method_imp;
};

SEL和IMP定义如下:

typedef struct objc_selector   *SEL;
typedef id (*IMP)(id, SEL, ...);

其中SEL应该为该方法对应的消息名字。IMP是函数指针,指向具体方法实现的地址 。参数id是一个对象,表示是谁调用的方法。剩下的是可变的参数列表。返回值是一个id。

NSObject类中的methodForSelector:方法可以获取消息SEL对应的方法实现IMP的指针。methodForSelector:返回的指针和赋值的变量必须完全一致,包括参数和返回类型。当一个消息要多次发给某个对象的时候,可以使用methodForSelector:来减少动态查找的开销。官方例子如下:

void (*setter)(id, SEL, BOOL);
int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0; i < 1000, i++ )
{
setter(targetList[i], @selector(setFilled:), YES);
}

消息调用过程

消息调用最终都会被转换成objc_msgSend函数调用, 比如一个消息表达式[receiver message]会被转换成objc_msgSend(receiver, selector),如果有参数则为objc_msgSend(receiver, selector, arg1, arg2, …)。objc_msgSend先通过isa指针在类的dispatch table中查找对应selector的函数入口地址IMP,如果没有找到,则沿着classhierarchy(类的继承体系)寻找,直到NSObject类。当找到IMP时就将消息调用和接受的对象的指针、selector和方法指定的参数传递给IMP。最后objc_msgSend把IMP所指向的函数返回值作为返回值返回。

为了加快IMP查找的速度,Runtime System为每个类创建了一个cache, 类结构中看到有一个叫objc_cache *cache 的成员。就是用来缓存selector和对应函数入口地址IMP的映射的。

Runtime System提供的方法查找代码如下,objc-class.mm:

static Method look_up_method(Class cls, SEL sel,BOOL withCache, BOOL withResolver)
{
Method meth = NULL; if (withCache)
{
meth = _cache_getMethod(cls, sel, &_objc_msgForward_internal);
if (meth == (Method)1)
{
// Cache contains forward:: . Stop searching.
return NULL;
}
} if (!meth) meth = _class_getMethod(cls, sel); if (!meth && withResolver) meth = _class_resolveMethod(cls, sel); return meth;
}

1.首先是查找缓存。

2.缓存中没有就在类和类的继承体系结构的方法列表中查找,找到后把Method放入相应查找到的类的缓存中。

3.如果还没找到就进行动态方法决议(接下来讲)。

4.动态方法决议没有解决问题就进行消息转发流程(在动态方法决议之后)

5.最后消息转发失败就会Crash

动态方法决议

Objective-C 提供了一种名为动态方法决议的手段,使得我们可以在运行时动态地为一个 selector 提供实现。我们只要实现 +resolveInstanceMethod: 和/或 +resolveClassMethod: 方法,并在其中为指定的 selector 提供实现即可(通过调用运行时函数 class_addMethod 来添加)。这两个方法都是 NSObject 中的类方法:

+ (BOOL)resolveClassMethod:(SEL)name;
+ (BOOL)resolveInstanceMethod:(SEL)name;

参数 name 是需要被动态决议的 selector;返回值文档中说是表示动态决议成功与否。但在上面的例子中(不涉及消息转发的情况下),如果在该函数内为指定的 selector 提供实现,无论返回 YES 还是 NO,编译运行都是正确的;但如果在该函数内并不真正为 selector 提供实现,无论返回 YES 还是 NO,运行都会 crash,道理很简单,selector 并没有对应的实现,而又没有实现消息转发。resolveInstanceMethod 是为对象方法进行决议,而 resolveClassMethod 是为类方法进行决议。动态方法决议的官方例子如下:

//头文件
#import <Foundation/Foundation.h>
@interface TestObject : NSObject
@end
//实现文件
#include <objc/runtime.h>
void dynamicMethodIMP(id self, SEL _cmd)
{
// implementation ….
}
@implementation Foo
+(BOOL)resolveInstanceMethod:(SEL)name
{
if (name == @selector(MissMethod))
{
class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
+(BOOL)resolveClassMethod:(SEL)name
{
return [super resolveClassMethod:name];
}
@end

关于动态方法决议返回值:在没有提供真正消息的实现,并且提供了消息转发机制的情况下,YES 表示不进行后续的消息转发,返回  NO 则表示要进行后续的消息转发。其实在没有提供真正的动态消息决议的情况下返回YES也会进行消息转发。

动态方法决议源码objc-class.mm:

1.首先判断是进行类方法决议还是进行实例方法决议。

__private_extern__ Method _class_resolveMethod(Class cls, SEL sel)
{
Method meth = NULL; if (_class_isMetaClass(cls))
{
meth = _class_resolveClassMethod(cls, sel);
}
if (!meth)
{
meth = _class_resolveInstanceMethod(cls, sel);
} if (PrintResolving && meth)
{
_objc_inform("RESOLVE: method %c[%s %s] dynamically resolved to %p",
class_isMetaClass(cls) ? '+' : '-',
class_getName(cls), sel_getName(sel),
method_getImplementation(meth));
} return meth;
}

2.实例方法决议(类方法决议类似):

static Method _class_resolveInstanceMethod(Class cls, SEL sel)
{
BOOL resolved;
Method meth = NULL; if (!look_up_method(((id)cls)->isa, SEL_resolveInstanceMethod,YES /*cache*/, NO /*resolver*/))
{
return NULL;
} resolved = ((BOOL(*)(id, SEL, SEL))objc_msgSend)((id)cls, SEL_resolveInstanceMethod, sel); if (resolved)
{
meth = look_up_method(cls, sel, YES/*cache*/, NO/*resolver*/); if (!meth)
{
_objc_inform("+[%s resolveInstanceMethod:%s] returned YES, but "
"no new implementation of %c[%s %s] was found",
class_getName(cls),
sel_getName(sel),
class_isMetaClass(cls) ? '+' : '-',
class_getName(cls),
sel_getName(sel));
return NULL;
}
}
return meth;
}

1.首先是判断是否实现了resolveInstanceMethod,如果没有实现就直接返回NULL。

2.如果实现了就调用resolveInstanceMethod,并获取返回值。

3.如果返回值为YES,表示resolveInstanceMethod提供了selector的实现,再次查找方法列表 ,如果找到了直接返回方法,没有则给出警告,返回NULL。

消息转发

向一个对象发送它不处理的消息是一个错误,不过在报错之前,RuntimeSystem给了接收对象第二次的机会来处理消息。在这种情况下,Runtime System会向对象发一个消息,forwardInvocation:,这个消息只携带一个NSInvocation对象作为参数——这个NSInvocation对象包装了原始消息和相应参数。通过实现forwardInvocation:方法(继承于NSObject),可以给不响应的消息一个默认处理方式。正如方法名一样,通常的处理方式就是转发该消息给另一个对象:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:[anInvocation selector]])
{
[anInvocation invokeWithTarget:someOtherObject];
}
else
{
[super forwardInvocation:anInvocation];
}
}

消息转发只有在方法列表中找不到并且没有提供动态方法决议的前提下才会进行。

Method Swizzling

Method Swizzling可以搞定一件事情,那就是可以不通过继承来重写某个方法。而且还可以调用原来的方法。通常的做法是在category中添加一个方法(当然也可以是一个全新的class)。可以通过method_exchangeImplementations这个运行时方法来交换实现。示例如下:

#import  <objc/runtime.h>

@interface NSMutableArray (LoggingAddObject)
- (void)logAddObject:(id)aObject;
@end @implementation NSMutableArray (LoggingAddObject) + (void)load
{
Method addobject = class_getInstanceMethod(self, @selector(addObject:));
Method logAddobject = class_getInstanceMethod(self, @selector(logAddObject:));
method_exchangeImplementations(addObject, logAddObject);
} - (void)logAddObject:(id)aobject
{
[self logAddObject:aObject];
NSLog(@"Added object %@ to array %@", aObject, self);
}
@end

我们把方法交换放到了load中,这个方法只会被调用一次,而且是运行时载入。如果指向临时用一下,可以放到别的地方。注意到一个很明显的递归调用logAddObject:。这也是Method Swizzling容易把我们搞混的地方,因为我们已经交换了方法的实现,所以其实调用的是addObject:,如图:

参考链接:

官方运行时代码:http://www.opensource.apple.com/source/objc4/objc4-532/runtime/

官方Runtime指导:Objective-C RuntimeProgramming Guide

罗朝辉的iOS开发专栏:《深入浅出Cocoa系列》

唐巧的技术博客:Objective-C对象模型及应用

无网不剩的博客:(译)Objective-C的动态特性

Objective-C 对象和消息模型的更多相关文章

  1. 简析android消息模型

    android总结系列 一.消息系统构成要素和基本原理 l  消息队列 l  发送消息 l  消息读取 l  消息分发 l  消息循环线程 消息系统必须要依赖一个消息循环线程来轮询自己的消息队列,如果 ...

  2. ActiveMQ( 一) 同步,异步,阻塞 JMS 消息模型

    同步请求:浏览器 向服务器 发送一个登录请求,如果服务器 没有及时响应,则浏览器则会一直等待状态,直至服务器响应或者超时. 异步请求:浏览器 向服务器 发送一个登录请求,不管服务器是否立即响应,浏览器 ...

  3. akka设计模式系列-消息模型(续)

    在之前的akka设计模式系列-消息模型中,我们介绍了akka的消息设计方案,但随着实践的深入,发现了一些问题,这里重新梳理一下设计方法,避免之前的错误.不当的观点给大家带来误解. 命令和事件 我们仍然 ...

  4. 【读书笔记】iOS-ARC-不要向已经释放的对象发送消息

    一,在AppDelegate.m中写入如下代码: - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOpti ...

  5. effective OC2.0 52阅读笔记(二 对象、消息、运行期)

    第二章:对象.消息.运行期 6 理解属性这一概念 总结:OC解决硬编码偏移量问题的做法,一种方案是把实例变量当做一种存储偏移量所用的特殊变量,交由类对象保管,偏移量会在运行期查找,叫做稳固的“应用程序 ...

  6. Kafka消息模型

    一.消息传递模型 传统的消息队列最少提供两种消息模型,一种P2P,一种PUB/SUB,而Kafka并没有这么做,巧妙的,它提供了一个消费者组的概念,一个消息可以被多个消费者组消费,但是只能被一个消费者 ...

  7. 函数调用和给对象发消息(Runtime理解)

    在写代码的时候这个差距其实是不打看的出得,很多时候也就无所谓叫什么,很多人为了便于理解,干脆就叫函数调用.这个其实应该是oc的一个特色,消息发送.具体的类typedef struct objc_cla ...

  8. Java对象的内存模型(一)

    前言 新人一枚,刚刚入门编程不久,各方面都在学习当中,博文有什么错误的地方,希望我们可以多多交流! 最近,在开发App后台过程中,需要将项目部署到云服务器上.而云服务器的内存大小却只有1G.要如何做到 ...

  9. RabbitMQ消息模型概览(简明教程)

    小菜最近用到RabbitMQ,由于之前了解过其他消息中间件,算是有些基础,所以随手从网上搜了几篇文章,准备大概了解下RabbitMQ的消息模型,没想到网上文章千篇一律,写一大堆内容,就是说不明白到底怎 ...

随机推荐

  1. Python.Books

    Flask 1. Flask Web Development Miguel Grinberg April 2014 2. Flask Framework Cookbook Shalabh Aggarw ...

  2. Can't find bundle for base name test.properties, locale zh_CN

    这个问题花了我好长时间,网上搜了一个答案:http://verran.iteye.com/blog/44357 是将properties文件放在新建的文件夹下,如config,然后将config加入到 ...

  3. sscanf 解析字符串

    test.txt中内容如下所示: eth0||192.168.0.2-192.168.0.150 eth2||192.168.0.2-192.168.0.150 想要将其中的ip地址等解析出来: #i ...

  4. 超全面!UI设计师如何适配2018新款iPhone

    北京时间9月13日凌晨1点,苹果在美国加利福尼亚州的Apple Park园区召开了2018年苹果秋季新品发布会. 很多人对这次科技界的春晚充满了期待,除了那些让人“剁手”的新品,设计师关注的还有新手机 ...

  5. Servlet 3.0 规范(二)注解驱动和异步请求

    Servlet 3.0 规范(二)注解驱动和异步请求 在 Servlet 3.0 时支持注解启动,不再需要 web.xml 配制文件. 一.Servlet 3.0 组件 Servlet 容器的组件大致 ...

  6. Metasploit学习

    阶段一:初步渗透 GO! msfconsole 相关漏洞 msf > search platform: windows xp sp3 查看某个漏洞后,查看漏洞详细信息 msf > info ...

  7. mybatis学习 十一 缓存

    1. 应用程序和数据库交互的过程是一个相对比较耗时的过程2. 缓存存在的意义:让应用程序减少对数据库的访问,提升程序运行效率3. MyBatis 中默认 SqlSession 缓存(一级缓存)开启 同 ...

  8. Linux---CentOS 定时运行脚本配置

    很多时候我们有希望服务器定时去运行一个脚本来触发一个操作,比如使用七牛的工具上传,如果同步文件里面有新增加一个文件,这个时候我们可以提供定时脚本去完成我们需要的同步命令(七牛的qrsbox工具是自动会 ...

  9. 2019.02.09 bzoj4487: [Jsoi2015]染色问题(容斥原理)

    传送门 题意简述: 用ccc中颜色给一个n∗mn*mn∗m的方格染色,每个格子可涂可不涂,问最后每行每列都涂过色且ccc中颜色都出现过的方案数. 思路: 令fi,j,kf_{i,j,k}fi,j,k​ ...

  10. 2019.01.22 hdu5195 DZY Loves Topological Sorting(贪心+线段树)

    传送门 题意简述:给出一张DAGDAGDAG,要求删去不超过kkk条边问最后拓扑序的最大字典序是多少. 思路:贪心帮当前不超过删边上限且权值最大的点删边,用线段树维护一下每个点的入度来支持查询即可. ...