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抛出异常。