runtime笔记

OC是一门动态语言,它将很多静态语言在编译和链接时期做的事情推迟到了运行时来处理,这就意味着它不仅需要一个编译器,还需要一个运行时系统来执行编译的代码(动态得创建类和对象、进行消息的传递和转发)。这个运行时系统就是Objc Runtime。Objc Runtime其实是一个Runtime库,它基本上是由c语言和汇编编写的。

RunTime库主要做下面几件事:

1.封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。

2.找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。

Objc从三种不同的层级上与Runtime系统进行交互,分别是通过Objective-C源代码、通过Foundation框架的NSObject类定义的方法、通过对runtime的函数直接调用。

1)Objective-C源代码

大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。

还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend函数及其参数列表中的id和SEL都是啥)

2)NSObject的方法

Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。

有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

3)Runtime的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

[receiver message]会被编译器转化为:

objc_msgSend(receiver, selector)

如果消息含有参数,则为:

objc_msgSend(receiver, selector, arg1, arg2, ...)

如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector对应的实现内容,要么就干脆玩完崩溃掉。

现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。

Runtime术语

objc_msgSend:方法的真身是这样的:

id objc_msgSend (id self, SEL op, ... );

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。

SEL

objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:typedf struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型(NSNumber一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。

id

objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

typedef struct objc_object * id;

那objc_object又是啥呢:

struct objc_object{ Class isa; };

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做isa-swizzling的技术,详见官方文档

Class

之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:

typedef struct objc_class * Class;

而objc_class就是我们摸到的那个瓜,里面的东西多着呢:


struct objc_class {

Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__

Class super_class OBJC2_UNAVAILABLE;

constchar*name OBJC2_UNAVAILABLE;

long version OBJC2_UNAVAILABLE;

long info OBJC2_UNAVAILABLE;

long instance_size OBJC2_UNAVAILABLE;

structobjc_ivar_list *ivars OBJC2_UNAVAILABLE;

structobjc_method_list **methodLists OBJC2_UNAVAILABLE;

structobjc_cache *cache OBJC2_UNAVAILABLE;

structobjc_protocol_list *protocols OBJC2_UNAVAILABLE;

#endif

} OBJC2_UNAVAILABLE;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。

PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码。

Objective-C 2.0 的头文件虽然没暴露出objc_class结构体更详细的设计,我们依然可以从Objective-C 1.0 的定义中小窥端倪:

在objc_class结构体中:ivars是objc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。而最新版的 Runtime 源码对这一块的描述已经有很大变化,可以参考下美团技术团队的深入理解Objective-C:Category。

PS:任性的话可以在Category中添加@dynamic的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)

其中objc_ivar_list和objc_method_list分别是成员变量列表和方法列表:

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

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}      

如果你C语言不是特别好,可以直接理解为objc_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method数组列表,而objc_method结构体存储了类的某个方法的信息。

最后要提到的还有一个objc_cache,顾名思义它是缓存,它在objc_class的作用很重要,在后面会讲到。

不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当[NSObject alloc]这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。

Method

Method是一种代表类中某个方法的类型。

typedef struct objc_method *Method;

objc_method存储了方法名,方法类型和方法实现:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}      
  • 方法名类型是SEL
  • 方法类型method_types 是个char指针,其实存储着方法的参数类型和返回值类型
  • method_imp是一个IMP类型的函数指针:
typedef id (*IMP)(id, SEL, ...); 

它指向了方法的实现。

Ivar

Ivar是代表了类中的实例变量的类型:
typedef struct objc_ivar *Ivar;
objc_ivar的定义如下:

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
}   

IMP

IMP的定义是:

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

它就是一个指向函数的指针,这是由编译器生成的,当你发起一个ObjC消息之后,最终他会执行的那段代码,就是有这个函数指针指定的。

Cache

Cache的定义如下:
typedef struct objc_cache *Cache
objc_cache的定义如下:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

[obj message]会被编译成objc_msgSend(obj, message),这个函数要做的事情是:

  1. 首先,通过obj的isa指针找到他的class;
  2. 在class里的method list找message;
    3.如果class中没有找到message,继续在他的superclass中找;
    4.一旦找到message这个函数,就去执行他的实现IMP
    但这种实现有个问题,效率低,一个class往往只有20%的函数会被经常调用。每个消息都要便利一次objc_method_list并不合理。如果把经常调用的函数缓存下来,那么可以大大提高函数查询的效率。这也就是objc_class中一个重要成员objc_cache做的事情。在找到message之后,把message的method_name作为key,method_imp作为value给结合起来。当再次受到message消息时,可以直接在cache找,避免遍历objc_method_list。

Property

typedef struct objc_property *objc_property_t;

 /**获取Person类中的属性**/  
   unsigned int outCount;
    //返回了一个包含objc_property_t的数组,最后必须要释放这个数组使用free()
    objc_property_t *properties = class_copyPropertyList([Person class], &outCount);
    for (unsigned int i = 0; i<outCount; i++) {
        objc_property_t property = properties[i];
        //获取属性名称
        const char * propertyName = property_getName(property);
        //返回了C字符串包含了属性名称和@encode类型字符串
        const char * propertyAttr = property_getAttributes(property);
        fprintf(stdout, "%s %s\n",propertyName, propertyAttr);
    }
      free(properties);

使用class_copyPropertyList只能获取类的属性,而不包含成员变量,但此时获取的属性名是不包含下划线的。

消息转发

[obj message]会被编译成objc_msgSend(obj, message),如果obj的isa指针指向的class及其superclass没有找到message,通常会抛出unrecognized selector sent to … 的异常,但在抛出异常前,runtime会给你三次拯救的机会:
1.method resolution
2.fast forwarding
3.normal forwarding

Method Resolution

NSObject在运行时会调用+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel,让你有机会提供一个函数式先来添加方法。

void message(id obj, SEL _cmd)
 {
    NSLog(@"Doing thing");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
  if(aSEL == @selector(message)){
  class_addMethod([self class], see, (IMP)message, "v@:");
  return YES; 
  }
  return [super resolveInstanceMethod];
}

Core Data 有用到这个方法。NSManagedObjects 中 properties 的 getter 和 setter 就是在运行时动态添加的。
如果 resolve 方法返回 NO ,运行时就会移到下一步:消息转发(Message Forwarding)
iOS4.3加入很多新的runtime方法,主要是以imp为前缀的方法,比如imp_implementationWithBlock()用block快速创建一个imp。

Fast forwarding

如果目标对象实现了- (id)forwardingTargetForSelector:(SEL)aSelector,runtime就会调用这个方法,把消息转发给其他对象。

- (id)forwardingTargetForSelector:(SEL)aSelector{
   if(aSelector == @selector(message)){
    return alternateObject;
   } 
 return [super forwardingTargetForSelector:aSelector];
}

只要这个方法返回的不是nil或self,整个消息发送的过程就会被重启,发送的对象会变成你返回的那个对象,否则,就会继续Normal Forwarding。
这里的fast是为了区别Normal Forwarding转发机制,因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快。

Normal forwarding

这是runtime最后一次给你挽救的机会,首先runtime会调用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法获得函数的参数和返回值类型,如果返回值是nil,runtime会调用- (void)doesNotRecognizeSelector:(SEL)aSelector的方法,程序就会挂掉;如果返回了一个函数签名,runtime就会创建一个NSInvocation对象并发送- (void)forwardInvocation:(NSInvocation *)anInvocation消息。
NSInvocation实际上就是对一个消息的描述,包扩了selector以及参数等信息,所以可以在- (void)forwardInvocation:(NSInvocation *)anInvocation里修改传进来的NSInvocation对象,然后发送- (void)invokeWithTarget:(id)target消息给他,穿进去一个新的目标。

- (void)forwardInvocation:(NSInvocation *)invocation{
 SEL sel = invocation.selector; 
 if([alternateObject respondsToSelector:sel]) { 
 [invocation invokeWithTarget:alternateObject]; 
 } else {
  [self doesNotRecognizeSelector:sel]; 
 }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,182评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,548评论 33 466
  • 感恩好朋友邀请我们去看电影,让我们心情愉悦放松心情。感恩电影院工作人员为大家清理卫生让我们有个洁净的环境享受电影带...
    念秀阅读 143评论 0 1
  • 今天没有阅读与跑步。 原计划是睡醒就去跑步至少一小时目标。 看手机和看视频又是到四点半,发现外面大雨,就一切外出所...
    卡卡22阅读 216评论 0 0