动态语言

OC是一门不折不扣的动态语言,所以它的很多机制都是动态运行时决定的。这点和C语言不一样,C语言是静态绑定,也就是编译后所有的一切都已经决定了。这一点和C语言的函数指针有些类似,很多时候函数指针在编译的时候并不知道会指向哪个函数,所以此时就是动态绑定。

举几个OC动态类型的例子,最为直接的就是id类型了、还有关联对象、动态绑定、消息转发、方法调配、这些技术都是动态类型很好的证明

OC对象结构

在介绍动动态性之前,我们先来看看OC对象的一些结构。

#import<objc/runtime>这是OC运行时函数库,里面定义了很多结构体。

首先看对象的结构:

typdef struct objc_object {
Class isa;
} *id;

对象结构中非常简单,只有一个isa指针,isa指针后面我们会介绍。

接下来我们看类的结构体

struct objc_class {
Class isa #if !__OBJC2__
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
#endif }
typedef struct objc_class *Class;

可以看到很多信息都在Class中定义着,里面信息如下

字段 含义
isa isa指针
super_class 父类指针
name 类名
version 类的版本信息,默认为0
info 供运行期使用的一些位标识
instance_size 实例的大小
ivars 实例变量列表
methodLists 方法列表
cache 指向最近调用的方法,用于优化调用方法的速度
protocols 协议列表

接下来我们逐个介绍一下:

isa指针和super_class

在OC中,严格意义上讲是没有类这种概念的,每一个类都是一个对象,只不过类对象是一个单例。

isa指针存在于每一个对象中,类普通实例的isa指针指向类,类的isa指针指向它的元类(类方法全部都在元类中存放)。元类的isa指针指向根元类,也就是NSObject的isa所指向的元类。

super_class只有类和元类才有,它们分别指向自己的父类和父元类,而为了让NSObject成为所有类的根类,让NSObject的元类的父类指针也指向了NSObject。

这样说可能也不是很好理解,看下面这张图应该就很快理解了。

等下我们说到消息传递的时候还会在说到isa和super_class。

ivars属性列表

struct objc_ivar_list {
int ivar_count
#ifdef __LP64__
int space
#endif
/* variable length structure */
struct objc_ivar ivar_list[]
}

space作用还不太清楚...求指教啊。

下面是实例变量结构

struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}

可以看到有一个ivar_offset,这个是实例变量在编译时的偏移量,是由编译时决定的。

methodLists方法列表

struct objc_method_list {
struct objc_method_list *obsolete int method_count
#ifdef __LP64__
int space
#endif
/* variable length structure */
struct objc_method method_list[]
}

该结构有方法链表和方法总数。

struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

里面有函数名,返回值类型和函数实现,接下来看看SEL和IMP定义

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

可以看到SEL是objc_selector,(*IMP)(id,SEL,...)是id,我没有找到objc_selector的结构所以这里也没法说什么...

cache缓存列表

typedef struct objc_cache *Cache                            

#define CACHE_BUCKET_NAME(B)  ((B)->method_name)
#define CACHE_BUCKET_IMP(B) ((B)->method_imp)
#define CACHE_BUCKET_VALID(B) (B)
#ifndef __LP64__
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
#else
#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>3)) & (mask))
#endif
struct objc_cache {
unsigned int mask /* total = mask + 1 */
unsigned int occupied
Method buckets[]
};

缓存列表里面包含了已缓存的方法,用于快速的调用,不需要在去方法列表里面查询了。

protocol协议列表

struct objc_protocol_list {
struct objc_protocol_list *next;
long count;
Protocol *list[];
};

协议列表包含了协议数量和协议的指针。

id

id类型实际上就是一个objc_object的typedef,是一个对象实例,而且还是一个指针,所以用id来定义对象的时候就不需要加*号了。

id类型往往需要我们使用”自省“机制来保证使用安全,所谓自省其实就是看看这个对象是不是某个类的实例,或者是不是其子类。

自省用一下两个方法:

isKindOfClass:(Class)class          判断是不是其类族对象

isMemberOfClass:(Class)class     判断是不是类本身对象

关联对象

关联对象在我之前的博客中已经有介绍了,这里就不再说了。

OC消息机制和消息转发

别的语言调用函数,OC则叫做发送消息,这是因为所有OC的方法调用实际上底层都是通过

objc_msgSend(id self , SEL cmd,...)来发送的。

该函数的作用就是传递给一个对象某个方法,后面的不定参数列表是方法所需要的参数。

这里说一下OC的消息传递机制,首先对一个对象发送消息,它会先检查自己的类中有没有该方法,如果没有就找他的父类中有没有,如果还没有则会进行消息转发。

在看例子之前先说一下,Xcode6貌似默认行为不让我们使用objc_msgSend了,所以需要先设置一下

把这一项设置为No就可以了。

这里看个例子,

        EqualObject *object1 = [EqualObject new];
EqualObject *object2 = [EqualObject new];
object1.name = @"xiaoming";
object2.name = @"xiaoming"; BOOL isEqual = objc_msgSend(object1,@selector(isEqualToEqualObject:),object2); if(isEqual)
{
NSLog(@"equal");
}

EqualObject是我们自己实现的类,它有一个判断是否相等的方法isEqualToEqualObject:,如果name相等就人为两个对象相等。

这里我们直接传递消息,不通过OC语法,运行程序可以看到equal被打印了出来。

然后我们再看看消息转发机制。

当该对象包括其父类都没有这个方法的时候会启动,消息转发机制分为两大阶段。

第一阶段先看对象所属类是否有能力动态添加方法,已处理这个位置的选择子,这叫做动态解析(dynamic method resolution)。

第二阶段设计“完整的消息转发机制”。如果运行期系统已经把第一阶段执行完了,那么接受者自己就没法再以动态新增方法的手段来处理与消息相关的方法调用。这又分为两个小步。

首先,请接受者看看有没有其他对象能处理这条消息。若有,则在运行时转给那个对象,于是消息转发过程结束。若没有“备用的接受者”,则启动完成的消息转发机制,运行起系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接受者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析

遇到无法解析的信息后,首先将调用其所属类的下列类方法:
+(BOOL)resolveInstanceMethod:(SEL)selector

该方法参数就是未知的选择子,返回BOOL类型那个,表示这个类是否能新增一个实力方法已处理这个选择子。假如是类方法,那么会调用

+(BOOL)resolveClassMethod:(SEL)selector

使用这种方法的前提是相关的实现已经写好了,只等运行时动态的插入就行,比如CoreData中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。

下面我们看个例子

void showMessage(id self, SEL _cmd, id value)
{
if([value isKindOfClass:[NSString class]])
{
NSLog(@"%@",(NSString *)value);
}
} +(BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *selString = NSStringFromSelector(sel);
if([selString isEqualToString:@"showMessage:"])
{
class_addMethod(self, sel, (IMP)showMessage, "v@:@");
return YES;
}
else
{
return [super resolveInstanceMethod:sel];
}
}

向刚才EqualObject添加以上实现带代码,然后在客户端调用:

        objc_msgSend(object1,@selector(showMessage:),@"Hello");

能够看到程序并没有报错,而且还打印出了Hello!

备援接受者

当没有使用动态方法解析后,还是出发备用接受者,该步骤会触发该方法

-(id)forwardingTargetForSelector:(SEL)selector

方法参数是未知的选择子,如果找到备用对象返回对象,否则返回nil。

可以利用该方法来模拟多重继承机制(实际为组合),因为外部看不到,所以感觉上就像是本身处理该消息。

下面看一个例子:

把刚才添加的代码注释掉,添加一个新类OtherObject,添加showMessage方法,然后在EqualObject中添加以下代码

-(id)forwardingTargetForSelector:(SEL)aSelector
{
NSString *selectString = NSStringFromSelector(aSelector);
if([selectString isEqualToString:@"showMessage:"])
{
return other;
}
return nil;
}

会发现Hello依旧出现了!,而且如果你在OtherObject中的showMessage方法中打上断点,会发现方法执行到了OtherObject中...

完整的消息转发

如果消息没有转发,那么回来到这一步,首先创建NSInvocation对象,然后把未处理的信息细节全部都封装于其中。

此对象包含选择子、目标(target)和参数。在触发NSInvocation对象时,“消息派发系统”会把消息派给目标。

此步骤会调用以下方法:

-(void)forwardInvocation:(NSInvocation *)invocation

这个方法实现简单,只要改变调用目标,使消息在新目标上得以调用即可。但是这样和备用接受者实现就一样了,所以一般都不会这样写。

比较有用的实现是再出发消息前,先以某种方式改变消息内容,比如追加另一个参数,或是改装选择子。

如果发现不该由该类执行,那么需要调用超类的该方法,继承体系中每个类都有机会处理此调用请求,如果到NSObject还不能处理会调用doesNotRecognizerSelector抛出异常。

实现上面那个方法的时候需要同时实现。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

需要先对NSInvocation签名然后才能使用NSInvocation

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig;
sig = [other methodSignatureForSelector:aSelector];
return sig;
} -(void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation invokeWithTarget:other];
}

方法调配

在运行时,我们还可以使用方法调配技术来改变SEL指向的IMP,比如说目前方法名和对应IMP如下

方法 IMP
methodA IMPA
methodB IMPB

但是当我们使用方法调配后,就可以出现一下情况

方法 IMP
methodA IMPB
methodB IMPA

其实我们修改的本质是方发表的映射,修改了选择子的指向

该技术主要用到的方法如下

void method_exchangeImplementations(Method m1, Method m2)

该函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

Method class_getInstanceMethod(Class aClass , SEL aSelector)

现在我们向EqualObject中添加methodA和methodB

-(void)methodA
{
NSLog(@"methodA");
} -(void)methodB
{
NSLog(@"methodB");
}

然后客户端这样写

        EqualObject *object1 = [EqualObject new];
Method methodA = class_getInstanceMethod([EqualObject class], @selector(methodA));
Method methodB = class_getInstanceMethod([EqualObject class], @selector(methodB));
method_exchangeImplementations(methodA, methodB); [object1 methodA];

运行后就会打印出methodB。

利用该技术进行黑盒调试

要进行黑盒调试,主要用到的就是该技术和category

下面来看看具体该如何编写:

首先添加一个EqualObject的category,然后添加一个新方法plusMethodA

-(void)plusMethodA
{
[self plusMethodA];
NSLog(@"this is plus version");
}

这里看着像是会无限递归,但是实际上plusMethodA选择子已经指向了methodA的IMP,所以并不会出现无限调用的情况。

客户端代码

        EqualObject *object1 = [EqualObject new];
Method methodA = class_getInstanceMethod([EqualObject class], @selector(methodA));
Method methodB = class_getInstanceMethod([EqualObject class], @selector(plusMethodA));
method_exchangeImplementations(methodA, methodB); [object1 methodA]

输出结果为

-- ::01.672 Equal[:] methodA
-- ::01.673 Equal[:] this is plus version

可以看到我们没有继承一个类就做到了扩展某个方法,用于调试打印一些输出信息会很有用!

常用Runtime总结

关联对象:

设置一个关联对象

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

获取关联对象

void objc_getAssociatedObject(id object, void *key)

删除该对象所有的关联对象

void objc_removeAssociatedObjects(id object)

消息传递

向某个对象/父类 发送消息

objc_msgSend(Super)

方法调配

交换两个方法的实现

void method_exchangeImplementation(Method m1, Method m2)

得到该法的指针

Method class_getInstanceMethod(Class aClass, SEL aSelector)

动态创建对象

创建新的类

Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

给类增加新的方法

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

注册新的类

void objc_registerClassPair(Class cls)

获得对象的isa指针所指向的对象

Class object_getClass(id obj)

Objective-C——Runtime理解的更多相关文章

  1. iOS --runtime理解

    iOS~runtime理解 Runtime是想要做好iOS开发,或者说是真正的深刻的掌握OC这门语言所必需理解的东西.最近在学习Runtime,有自己的一些心得,整理如下,一为 查阅方便二为 或许能给 ...

  2. Objective C Runtime 开发介绍

    简介 Objective c 语言尽可能的把决定从编译推迟到链接到运行时.只要可能,它就会动态的处理事情.这就意味着它不仅仅需要一个编译器,也需要一个运行时系统来执行变异好的代码.运行时系统就好像是O ...

  3. Runtime理解

    动态语言 OC是一门不折不扣的动态语言,所以它的很多机制都是动态运行时决定的.这点和C语言不一样,C语言是静态绑定,也就是编译后所有的一切都已经决定了.这一点和C语言的函数指针有些类似,很多时候函数指 ...

  4. runtime 理解笔记

    runtime 简称运行时,是系统运行的一种机制,在oc中通过c语言编写一个运行系统库.考进行一些非常底层的操作(oc无法完成的). 1.利用runtime,在程序运行过程中,动态创建一个类(比如KV ...

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

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

  6. 刨根问底Objective-C Runtime(4)- 成员变量与属性

    http://chun.tips/blog/2014/11/08/bao-gen-wen-di-objective[nil]c-runtime(4)[nil]-cheng-yuan-bian-lian ...

  7. Objective-C Runtime(一)预备知识

    很早就知道了Objective-C Runtime这个概念,「Objective-C奇技淫巧」「iOS黑魔法」各种看起来很屌的主题中总会有它的身影:但一直没有深入去学习,一来觉得目前在实际项目中还没有 ...

  8. Runtime系列(二)--Runtime的使用场景

    Runtime 理解介绍的文章非常多,我只想讲讲Runtime 可以用在哪里,而我在项目里哪些地方用到了runtime.多以实际使用过程为主,来介绍runtime的使用. * 那么runtime 怎么 ...

  9. iOS 面试基础题目

    转载: iOS 面试基础题目 题目来自博客:面试百度的记录,有些问题我能回答一下,不能回答的或有更好的回答我放个相关链接供参考. 1面 Objective C runtime library:Obje ...

随机推荐

  1. [HDU3586]Information Disturbing(DP + 二分)

    传送门 题意:给定一个带权无向树,要切断所有叶子节点和1号节点(总根)的联系,每次切断边的费用不能超过上限limit,问在保证总费用<=m下的最小的limit 二分答案,再 DP,看看最终结果是 ...

  2. noip模拟赛 轰炸

    题目描述 C国和W国爆发了战争!YJC决定对W国的n个城市进行轰炸.每个城市都有一个重要度ai.设xi=‘重要度大于ai的城市数+1’,那么编号为i城市就是第xi个被轰炸的城市.显然这样能保证重要度大 ...

  3. 高数(A)下 第十章

    10.1 10.2 10.3 10.4 10.5 10.7 自测题

  4. 两张图让git新手在项目中运用git命令行

    创建分支命令:    git branch (branchname) 切换分支命令:      git checkout (branchname) 当你切换分支的时候,Git 会用该分支的最后提交的快 ...

  5. 串口调试助手---VB源码

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/ ...

  6. Android 好看的搜索界面,大赞Animation

    转载请注明出处王亟亟的大牛之路 一直对Animation属于可有可无的不在意.看到个样例,认为在不切换的情况下,适当的应用还真是蛮好看的. 包结构: 一个类一个控件.内容简单. 执行效果: 下方的下方 ...

  7. mac 下安装caffe(一)

    1.brew install --build-from-source -vd boost boost-python 这一步出错:libtool: unrecognized option `-stati ...

  8. java文件和目录的增删复制

    在使用java进行开发时常常会用到文件和目录的增删复制等方法.我写了一个小工具类.和大家分享,希望大家指正: package com.wangpeng.utill; import java.io.Fi ...

  9. 初探C++运算符重载学习笔记&lt;2&gt; 重载为友元函数

    初探C++运算符重载学习笔记 在上面那篇博客中,写了将运算符重载为普通函数或类的成员函数这两种情况. 以下的两种情况发生.则我们须要将运算符重载为类的友元函数 <1>成员函数不能满足要求 ...

  10. IT痴汉的工作现状22-由Dalvik虚拟机引发的口水战

    Dalvik是啥呢? 从未知道冰岛有Dalvik这么一个重要的村庄.直到Dan Bornstein将自己为Android系统编写的进程虚拟机命名为Dalvik后才被我所知. 它是Android系统独有 ...