iOSer总要了解Runtime的

runtime中的重要结构体模型  

id指向objc_object,Class指向objc_class,NSObject类中定义了Class isa。

1.0时,类(objc_class)的结构体中定义了 指向元类的isa指针、父类、类名、实例大小、属性列表ivars、方法列表methodLists、方法缓存cache、协议protocols。而对象(objc_object)的结构体中,定义了isa指针指向类,Class isa。

2.0后,类(objc_class)继承了对象(objc_object { private isa_t isa }),struct objc_class: objc_object{ //Class isa; super; cache; data_bits},从内存角度优化了isa指针(Tagged Pointer),其他的成员变量也做了一些结构上的调整。

class_rw_t和class_ro_t

ro-readonly编译阶段就确定的,rw-readwrite是在类第一次初始化,运行时构建的,并把编译阶段的信息拷进去。

class_ro_t在编译阶段就装载完成类的属性、方法和协议,类未初始化之前class_data_bits_t指针指向class_ro_t;class_rw_t在类调用realizeClass(类首次调用时才会执行)时候动态构建后,赋值ro和flag,并在methodizeClass时赋值属性、方法和协议。class_rw_t + rr/alloc(可释放)即class_data_bits_t。


元类meta-class

元类其实是类对象的类,引入元类是为了让类对象和实例对象,查找方法的机制统一。当给实例对象发消息时,从对象的类的方法列表里找;给类对象发消息时,从元类的方法列表里找。实例对象的isa指向类对象,类对象的isa就指向元类,元类的isa是root元类,root元类的isa指向自己。Root Class(NSObject) 的 isa→ Root Meta Class的super class→ NSObject,形成闭环。

根类isa->根元类,根元类isa->指向自己。isa闭环最终指向自己

根类isa->根元类,根元类super->指向根类。父类闭环指向nil

[xxx class]:类方法return self,实例方法object_getClass,return isa(即类)

objc_getClass:入参字符串获取类对象;object_getClass:入参类获取isa

isKindOfClass:类方法调object_getClass获取isa(循环查找isa指向的类的父类)后,与入参比较;实例方法调[self class]

isMemberOfClass:类方法调用object_getClass获取isa(不找父类)后,与入参比较;实例方法调[self class]。

注1:isMemberOfClass与isKindOfClass区别就是是否找消息接受者的isa的父类。

注2:OC中的内省方法:isKindOfClass,isMemberOfClass,respondToSelector,conformsToProtocol(遍历继承树,是否遵循协议,是否响应方法等)。

注3:self是隐藏参数,是在[self method]转化为obj_sendMsg时的第一个参数(id),即方法的接收者,是给当前类发消息;super是编译标识符,会去调objc_msgSendSuper(而非obj_sendMsg(super)!),而objc_msgSendSuper的第一个参是objc_super(结构体包括receiver和super_class),所以实际是给obj_receiver(实例)发消息,但是是从父类中去找方法的IMP,最终变成obj_msgSend(obj_receiver, @selector(class))。(-(Class)class 方法)


Category

category的源码实现过程:由于是在运行时动态加载的,入口_objc_init,内部remethodizeClass方法中拼接分类列表,attachCategories方法把属性、方法、协议列表取出并倒序遍历(最先访问最后编译的方法),attachLists方法添加到宿主类的数组中(实例方法添加到类上,类方法添加到元类上),期间会重新开辟内存,并将原有的移动到内存尾部,最后重新写入class_rw_t中。所以会"覆盖"同名方法,其实两个都存在。Q:如何调用在方法列表后面的方法(在方法列表中寻找最后一个对应方法名的方法class_copyMethodList→method_getName→字符串相等→取最后一个的SEL、IMP//www.greatytc.com/p/40e28c9f9da5)。

总结→运行时类初始化时加载,遍历获取分类中的属性方法协议,开辟内存,旧方法放尾部,新方法放头部,重新写入。

category的附加工作会先于load调用(load_image中,loadAllCategories→call_load_methods),所以可以在load中调用分类的方法属性等。多个category中load的调用顺序:先调类中的load,然后根据编译顺序(Build Phases中的文件顺序)调用每一个category中的load。

与extension的区别:category是运行时决定的没有自己的isa指针没有ivar列表等,所以能添加属性(自己实现settergetter方法),但不会生成成员变量ivar,extension编译期决定的可以添加。PS:要有源码才能实Extension,因为要在.m中实现.h中声明的方法。


6、+load和+initialize      //www.greatytc.com/p/86df46c405d5

+load:加载dylb之后,main函数之前,runtime加载类和分类的时候调用,且只会调用一次

源码:_objc_init → load_image → prepare_load_methods → schedule_class_load → add_class_to_loadable_list → add_category_to_loadable_list → call_load_methods()

解析:准备阶段优先递归获取父类的load方法再获取类的load方法,并把IMP存到loadable_class集合中;并处理分类的load方法也存起来;正式调用时也根据IMP加入集合的顺序,遍历加载父、子、分类的load方法(注意是直接指针调用(*load_method)(cls, SEL_load);)

+initialize:类第一次收到消息的时候调用([Class new];),也只会调用一次(但父类的initialize可能调用多次)。

源码:lookUpImpOrForward,首先isInitialized判断类有没有初始化,递归调用_class_initialize先初始化父类,都初始化完后调用callInitialize(cls){ ((void(*)(Class,SEL))objc_msgSend)(cls,SEL_initialize); },最终走消息发送。

注:按照方法缓存和查找的顺序,分类中的initialize方法会优先调用分类中的;子类没有实现时调用父类的;父类的可能调多次(实际是子类的初始化)。

区别:调用时机不同;调用方式不同


7、消息发送和消息转发    https://juejin.im/post/5aa79411f265da237a4cb045 

消息发送过程:objc_msgSend(id , SEL, ...),首先根据消息接受者id的isa指针找到类,调用入口方法lookupMethodAndLoadCache,参数为(调用者id,接受者cls,方法名SEL)。lookUpImpOrForward为核心实现:cache_getImp先从缓存里找,再从class_rw_t的方法列表中找,都没找到开始从父类中找,找到了返回IMP。

动态方法解析:_class_resolveMethod,在resolveInstanceMethod / resolveClassMethod中利用class_addMethod将未实现的SEL绑定到已经实现的方法IMP上(class_addMethod([self class], sel, (IMP)myMethod, "v@:@");)完成动态方法解析,return YES,否则return super.xxx。

消息转发机制:当以上过程中没有返回IMP或完成处理方法,则开始启动消息转发机制

1、备援接受者:forwardingTargetForSelector中,return [Class new],让它去Class里找。

2、完整的消息转发:- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { } 中签名方法;forwardInvocation中转发到别的类对象去实现[anInvocation invokeWithTarget:testObj];

优劣:不太安全,方法的执行可能会被篡改;消息转发过程越往后代价越大;允许动态的处理未知的方法;增加处理消息的机会。


其他

class_copyIvarList获取所有成员变量;class_copyPropertyList获取@property生成的属性变量(对ivar的包装,加了getter,setter)

IMP、SEL、Method:SEL是方法的标识,IMP是函数指针,id (*IMP)(id, SEL, ...),保存了方法的实现;IMP和SEL可以相互映射。Method指针指向objc_method里面保存了SEL、IMP和Type。//www.greatytc.com/p/4a09d5ebdc2c


旧版

如果别人问你什么是Runtime

Runtime,用C和汇编编写的运行时系统 ,实现了OC语言的动态性,支持了面向对象

Runtime中的基本概念

obj.h中的Class、id、SEL

typedef struct objc_class *Class;  //指向“类”(objc_class)这个结构体的指针,objc_class这个结构体里又包括指向它的元类的isa。

注:如果一个类的isa指针指向的类是它自己,则这个类就是元类。isa就是类指针

typedef struct objc_object *id;  //指向“对象实例”(objc_object)的指针,而objc_object结构里为指向这个实例的类的指针(Class isa)

注:objc_object中id是灵活的对象指针,可指向任何继承NSObject的对象且不需要强制转换。与instancetype,意义相同都是万能指针,指向一个对象,但instancetype可以在编译时就判断对象的真实类型,一般作为返回值写在构造函数(初始化/赋值函数)内。

typedef struct objc_selector *SEL; //指向“方法ID”(objc_selector)的指针,是方法名和参数的的唯一ID(而method包含了名字和实现)

注:objc_method结构体中就定义了SEL,char,IMP,IMP是指向实现方法的指针,即方法的实例,利用@selector()获取,可直接调用:

IMP methodPoint = [self methodForSelector:methodId];

methodPoint();

runtime.h中定义的成员

typedef struct objc_method *Method;  //指向“类中的方法”的指针

注:Method指向的objc_method结构体中包含了SEL method_name、char *method_types、IMPmethod_imp三个属性。

typedef struct objc_ivar *Ivar;  //指向“类中的变量”的指针

注:Ivar指向的objc_ivar结构体中包含了char *ivar_name、char *ivar_type、int ivar_offset,64位系统下还有int space等属性。

typedef struct objc_property *objc_property_t;  //属性

typedef struct objc_category *Category;  //扩展方法

一些runtime方法会用到上述成员做参数或返回值,如

Method *methodList = class_copyMethodList([self class], &count); //获取方法列表,作为返回值

method_exchangeImplementations(systemMethod, swizzMethod); //方法交换,作为参数

class结构体的定义

objc.h中定义了Class(结构体指针类型),其结构体的实现在runtime.h中,如下

struct objc_class {

     Class isa; //Class 也有一个 isa 指针,指向其所属的元类(meta)

#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; //指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。methodLists 是指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因

    struct objc_cache *cache; //Runtime 系统会把被调用的方法存到 cache 中,下次查找的时候效率更高

    struct objc_protocol_list *protocols; //指向该类的协议列表

#endif

} OBJC2_UNAVAILABLE;

/* Use `Class` instead of `struct objc_class *` */


Runtime的常用方法及实际应用

获取属性列表(注属性property = ivar + getter + setter,ivar实例变量是用@synthesize声明的)

objc_property_t *propertyList = class_copyPropertyList([self class], &count); //count取地址,方法内部更改count获取属性个数

获取方法列表

Method *methodList = class_copyMethodList([self class], &count);

获取成员变量列表

Ivar *ivarList = class_copyIvarList([self class], &count);

获取协议列表

__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);

实际应用

动态关联属性 / AssociatedObject(给任何NSObject对象添加自定义新属性)

NSObject+Property.h中添加新属性(newProperty),重写set方法,用objc_setAssociatedObject把属性关联给对象;重写get方法,objc_getAssociatedObject取出属性。


字典转模型 / MakeModel(将接口返回的json直接快速转化成mode,MJExtension)

NSObject添加扩展方法,modelWithDict,参数为传入字典dict和需要改变的属性名键值对字典updateDict,class_copyIvarList获取Ivar数组,循环,ivar_getName转字符串(注意成员变量默认带_,应从下标为1开始取),若源dic中无此属性值,则通过updateDict赋值,然后setValue ForKeyPath。

对象归解档 / ObjectArchive(存储属性较多的model对象时快速归解档)

model实现<NSCoding>协议中的initWithCoder和encodeWithCoder,class_copyIvarList遍历属性,ivar_getName转字符串,decodeObjectForKey解档转化成model,encodeObject: forkey归档成NSData,NSKeyedArchiver调用archivedDataWithRootObject:model归档,unarchiveObjectWithData:data解档。

方法交换 / MethodSwizzling(一般用于将系统类中的某个方法替换成自定义方法执行,如dealloc统一输出、全局埋点等)

class_getClassMethod获取当前类待替换的oriMehtod和自定义的cusMethod,然后执行method_exchangeImplementations。(直接执行前提是默认已知oriMehtod是类已经实现的方法)

方法拦截并替换 / 方法增加额外功能(如交换sendAction:to:forEvent:记录按钮点击)

class_getInstanceMethod获取两个方法oriMehtod、cusMethod(可以是不同类的),使用class_addMethod给源方法oriMehtod添加新的实现method_getImplementation(cusMethod)添加成功,class_replaceMethod替换为新方法的实现;添加失败,说明已有实现,则直接执行method_exchangeImplementations方法交换。

注:method_exchangeImplementations是当前类层级的,即交换类方法的IMP指针,而class_replaceMethod是跨类的,可以修改类。比如,如果源方法是在父类中实现的(UIButton的事件是在UIControl里实现的),则要通过addMethod添加新实现来替换。


消息发送(传递)机制

在C语言中,函数的调用是在编译的时候就决定了,编译完成直接顺序执行无二义性;而OC的函数调用实际上是消息发送的过程,编译阶段并不能决定,属于动态调用(OC编译阶段可以调用任何函数即使它未实现,但C中会因为函数未实现而报错)

[receiver message];

objc_msgSend(receiver, @selector(message));  //Build Setting -> Enable Strict Checking of objc_msgSend Calls设置为NO后可直接调用

否则请使用:( (void (*) (id, SEL, id)) objc_msgSend ) (id, selector, id);  //要对objc_msgSend强制转换,变成有参数和返回值的方法

解析:objc_msgSend方法执行时,@selector()获取到方法的SEL(唯一标识),然后去实例对象receiver或类的methodlist中去寻找方法的实现IMP此过程会用到cache缓存提高查找效率,找不到还会去父类里面去找),找到之后还会把receiver和参数传递给IMP,最后IMP的返回即为函数的返回。多个类可能有共同的一个SEL但他们的IMP可能不同,但在一个类中,SEL和IMP是一对一的。所以,消息的调用其实是通过SEL找到IMP,并执行的过程。

注:OC分别通过OC源代码Foundation框架NSObject类三个层级中定义的方法,对runtime中的C语言函数进行直接调用,所以通常我们只需要写OC代码。

注:1、方法缓存缓存在类对象(实例的isa)中,每个类只有一份,子类从父类中取到方法也会缓存在子类的元类中,父类的类对象调了也会在父类的元类中缓存。2、缓存的list是有顺序的,因为category中的方法在原始方法的前面,需要先被找到。

消息转发机制

概论:找不到,先将传入的SEL与现有IMP搭配;搭配不到,发给别的对象实现处理;最后启用完整的消息转发NSInvocation进行多样处理

当对象(包括实例和类对象)收到无法解析的方法时(即无法从methodlist中直接找到的SEL),会启用消息转发机制,如果不做任何处理,那程序则会以unrecognized selector sent to instance crush。而消息转发机制首先启用的,是动态方法解析

动态方法解析:+(BOOL)resolveInstanceMethod:(SEL)sel / +(BOOL)resolveClassMethod:(SEL)sel,这两个方法的目的是在对象无法找到方法时,传入那个未知的SEL,给它机会用class_addMethod去动态添加方法的实现。即可以实现未找到的SEL与现有IMP的任意搭配。

+ (BOOL)resolveInstanceMethod:(SEL)selector {

       NSString *selectorString = NSStringFromSelector(selector);

       if ([selectorString hasPrefix:@"set"]) {

               class_addMethod(self, selector, (IMP)autoDictionarySetter,"v@:@");

               return YES;

        }

        return [super resolveInstanceMethod:sel];

}

如果通过动态方法解析还是没有获取到方法的实现(return NO),则启用第二种方案,备援接受者:- (id)forwardingTargetForSelector:(SEL)aSelector,即把这个无法处理的消息SEL,传递给别的对象去处理。

最后,只好启用基于NSInvocation的,完整的消息转发机制:- (void)forwardInvocation:(NSInvocation *)anInvocation,把所有未处理的target/selector/参数封装在anInvocation里并处理,处理方式多种多样(包括动态解析和备援接受者),甚至可以让一个消息触发多个对象的多个方法实现执行(转发给多个对象)。

三种方案循序渐进,代价逐渐增大。如果三种方案都无法处理这个消息的转发,则调用NSObject的doesNotRecognizeSelector抛出异常

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