参考: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
(2)objec_object 实例对象(isa)——> 类对象,
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 没有参数,
- (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冲突的规则继续向下,直到最后。
为什么类的方法列表不直接做成散列表呢,做成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来实现。