4.runtime 类与对象

参考:https://www.jianshu.com/p/6ebda3cd8052
Objective-C 扩展了 C 语言,加入了面向对象新特性,用C和汇编写runtime库
它是OC的面相对象和动态机制的基石。OC是一个动态语言,意味着不仅需要一个编译器,还需要一个运行时系统来动态
得创建类和对象,进行消息传递和转发。
OC:首先要转写为纯C语言再进行编译和汇编的操作。从OC到C语言的过度就是Runtime来实现的。
《一》类对象:
参考:https://blog.csdn.net/kesalin/article/details/7211228

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

Class 是一个 objc_class 结构类型的指针;而 id(任意对象) 是一个 objc_object 结构类型的指针,其第一个成员是一个 objc_class 结构类型的指针。注意这里有一关键的引申解读:内存布局以一个 objc_class 指针为开始的所有东东都可以当做一个 object 来对待

metaclass 存储类的static类成员变量与static类成员方法(+开头的方法);实例对象中的 isa 指向类结构称作 class(普通的),class 结构存储类的普通成员变量与普通成员方法(-开头的方法)。
规则一:类的实例对象的 isa 指向该类;该类的 isa 指向该类的 metaclass;
规则二:类的 super_class 指向其父类,如果该类为根类则值为 NULL;
规则三:metaclass 的 isa 指向根 metaclass,如果该 metaclass 是根 metaclass 则指向自身;
规则四:metaclass 的 super_class 指向父 metaclass,如果该 metaclass 是根 metaclass 则指向该 metaclass 对应的类;
(1)objc_class. 类对象
类对象 () ——>superclass
——>isa (元类) metaclass


DE7FF79D-3D14-4715-8AB2-484ECC9752F9.png

57A608F9-FD1E-4B0F-B4A2-30E0EEECEF2B.png

(2)objec_object 实例对象(isa)——> 类对象,

F04DC34F-4A43-410D-9408-51B93DDBD927.png

142DEE35-56AB-4634-86A7-FE8F700CAE6C.png

class 与 metaclass 有什么区别呢?

class 是 instance object 的类类型。当我们向实例对象发送消息(实例方法)时,我们在该实例对象的 class 结构的 methodlists 中去查找响应的函数,如果没找到匹配的响应函数则在该 class 的父类中的 methodlists 去查找(查找链为上图的中间那一排)。如下面的代码中,向str 实例对象发送 lowercaseString 消息,会在 NSString 类结构的 methodlists 中去查找 lowercaseString 的响应函数。
NSString * str;
[str lowercaseString];

metaclass 是 class object 的类类型。当我们向类对象发送消息(类方法)时,我们在该类对象的 metaclass 结构的 methodlists 中去查找响应的函数,如果没有找到匹配的响应函数则在该 metaclass 的父类中的 methodlists 去查找(查找链为上图的最右边那一排)。如下面的代码中,向 NSString 类对象发送 stringWithString 消息,会在 NSString 的 metaclass 类结构的 methodlists 中去查找 stringWithString 的响应函数。
[NSString stringWithString:@"str"];

(3)SEL
typedef struct objc_selector *SEL;
SEL就是对方法的一种包装。包装的SEL类型数据它对应相应的方法地址,找到方法地址就可以调用方法。在内存中每个类的方法都存储在类对象中,每个方法都有一个与之对应的SEL类型的数据,根据一个SEL数据就可以找到对应的方法地址,进而调用方法。
@seletor(). 或者 runtime sel_registerName
selector 是一个string Object-c seletor只记住了method的name 没有参数,


F2FF7C99-179D-4D83-B70B-47BFCA7A0066.png
  • (void)showInt:(NSInteger)age;
  • (void)showString:(NSString *)age;

(4)IMP
typedef id (*IMP)(id, SEL, ...); 指向方法实现的指针。
所以 IMP 其实是一个函数指针,第一个参数是一个对象,第二个参数是一个方法名。这两个参数其实就对应着 self 和 _cmd
(5)消息转发
https://www.jianshu.com/p/6ebda3cd8052
一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:

  • 首先,通过obj的isa指针找到它的 class ;
  • 在 class 的 method list 找 foo ;
  • 如果 class 中没到 foo,继续往它的 superclass 中找 ;
  • 一旦找到 foo 这个函数,就去执行它的实现IMP 。

但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度.,

#import "ViewController.h"
#import "objcDEMO-Swift.h"
#import "SnapKit-Swift.h"
//Person
@interface Person:NSObject
@end
@implementation Person
- (void)fool{
    NSLog(@"fool调用");
}
- (void)foolNew{
    NSLog(@"fool调用");
}
@end
@interface ViewController ()
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(fool)];
}
//例子1
//void fool(){
//    NSLog(@"Doing fool");//新的foo函数
//}
//void fooMethod(id obj, SEL _cmd) {
//    NSLog(@"Doing foonew");//新的foo函数
//}
//+ (BOOL)resolveInstanceMethod:(SEL)sel{
//    if (sel == @selector(fool)) {
//        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
//        return YES;
//    }
//    return [super resolveInstanceMethod:sel];
//
//}

//例子2
//+(BOOL)resolveInstanceMethod:(SEL)sel{
//    return YES;
//}
//-(id)forwardingTargetForSelector:(SEL)aSelector{
//    if (aSelector == @selector(fool)) {
//        return [Person new];
//    }
//    return  [super forwardingTargetForSelector:aSelector];
//}

//列子3
//+(BOOL)resolveInstanceMethod:(SEL)sel{
//    return YES;
//}
//-(id)forwardingTargetForSelector:(SEL)aSelector{
//    if (aSelector == @selector(fool)) {
//           return nil;
//    }
//    return  [super forwardingTargetForSelector:aSelector];
//}
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
//    if ([NSStringFromSelector(aSelector) isEqualToString:@"fool"]) {
//        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名
//    }
//    return  [super methodSignatureForSelector:aSelector];
//}
//
//- (void)forwardInvocation:(NSInvocation *)anInvocation{
//    SEL sel = anInvocation.selector;
//    Person * p =[Person new];
//    if ([p respondsToSelector:sel]) {
//        [anInvocation invokeWithTarget:p];
//    }
//    else{
//        [self doesNotRecognizeSelector:sel];
//    }
//}


@end

1.resolveInstanceMethod 更改IMP的实现方法。
2.forwardTargetForselector 类 转发给对象的方法。
3.如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。

https://tech.meituan.com/DiveIntoMethodCache.html
http://www.cocoachina.com/ios/20150818/13075.html
查看汇编objc-msg-arm.s查看 _objc_msgSend
objc_msgSend(就arm平台而言)的消息分发分为以下几个步骤:

  • 判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象
  • 从缓存里寻找,找到了则分发,否则
  • 利用objc-class.mm中_class_lookupMethodAndLoadCache3(为什么有个这么奇怪的方法。本文末尾会解释)方法去寻找selector
  • ····1.如果支持GC,忽略掉非GC环境的方法(retain等)
  • ····2.从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则转发这个selector,否则报错,抛出异常.
    //
    (6)
    objc_cache:
    1.但这种实现有个问题,效率低。但一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class 中另一个重要成员objc_cache 做的事情 - 再找到foo 之后,把foo 的method_name 作为key ,method_imp作为value 给存起来。当再次收到foo 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度.,

2.从上面的分析中我们可以看到,当一个方法在比较“上层”的类中,用比较“下层”(继承关系上的上下层)对象去调用的时候,如果没有缓存,那么整个查找链是相当长的。就算方法是在这个类里面,当方法比较多的时候,每次都查找也是费事费力的一件事情。
struct objc_cache {
uintptr_t mask; /* total = mask + 1 */
uintptr_t occupied;
cache_entry *buckets[1];
};
嗯,objc_cache的定义看起来很简单,它包含了下面三个变量:
1)、mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
2)、occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
3)、buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存
(buckets定义在objc_cache的最后,说明这是一个可变长度的数组)

typedef struct {
    SEL name;     // same layout as struct old_method
    void *unused;
    IMP imp;  // same layout as struct old_method
} cache_entry;

cache_entry定义也包含了三个字段,分别是:
1)、name,被缓存的方法名字
2)、unused,保留字段,还没被使用。
3)、imp,方法实现
这是往方法缓存里存放一个方法的代码片段,我们可以看到sel被散列后找到一个空槽放在buckets中,而CACHE_HASH的定义如下:

define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))

我们看objc-msg-arm.mm 里面的CacheLookup方法:虽然是汇编,但是注释太详尽了,理解起来并不难,还是求hash,去buckets里找,找不到按照hash冲突的规则继续向下,直到最后。


DE7FF79D-3D14-4715-8AB2-484ECC9752F9.png

为什么类的方法列表不直接做成散列表呢,做成list,还要单独缓存,多费事?这个问题么,我觉得有以下三个原因:

  • 散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的;Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。
  • list的方法还保存了除了selector和imp之外其他很多属性
  • 散列表是有空槽的,会浪费空间

(6)Category 类扩展(extension)是category的一个特例
https://www.jianshu.com/p/6ebda3cd8052
Category 如果写属性。需要实现get. Set 方法。
name:是指 class_name 而不是 category_name。 cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。 instanceMethods:category中所有给类添加的实例方法的列表。 classMethods:category中所有添加的类方法的列表。 protocols:category实现的所有协议的列表。 instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta) {
        if (isMeta) return nil; // classProperties;
        else return instanceProperties;
    }
};

https://www.jianshu.com/p/244dbc17d011 . Category添加@property
objc_getAssociatedObject 和 objc_setAssociatedObject来实现。

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

推荐阅读更多精彩内容