OC语言最大的特色,OC是C的升级、OC通过运行时将代码转为C然后再转为汇编。
OC是一门动态语言,类型的判断、类的成员变量、方法的内存地址都是在程序的运行阶段才最终确定,并且还能动态的添加成员变量和方法。也就意味着你调用一个不存在的方法时,编译也能通过,甚至一个对象它是什么类型并不是表面我们所看到的那样,只有运行之后才能决定其真正的类型
虽然实际开发中使用的少,但OC中的runtime无处不在,学习runtime可以帮助我们更好的理解OC。
一、实例 (instance)
实例 (instance) 到底是由什么构成,我们查看一下objc.h中的源码:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
由此看出,我们创建的一个对象或实例其实就是一个struct objc_object结构体,而我们常用的动态类型 id 也就是这个结构体类型的指针。
我们创建的类的实例最终获取的都是一个结构体指针,这个结构体只有一个成员变量就是Class
类型的isa
指针,日常实例使用,我们调用实例的 class 方法,返回一个Class类型对象,也就是实例的类,并且可以通过这个class来初始化实例。所以我们可以知道: 这个Class就是代表这个实例的类,所以实例的isa指针指向的是这个实例的类。
NSString *str = [[NSString alloc] initWithString: @"Hello World"];
Class strClass = [str class];
NSString *str2 = [[strClass alloc] initWithString: @"Hello World"];
从这可以推断我们上面描述的 isa指针指向的就是这个实例的类。
二、class object、metaclass
OC中的类本身也是一个对象:类对象(class object),上面实例中的 [str class] 方法返回的就是一个类对象。那么类对象又是什么? Class
是结构体指针,指向 结构体 objc_class
, 下面是这个结构体:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
Class _Nullable super_class
const char * _Nonnull name
long version
long info
long instance_size
struct objc_ivar_list * _Nullable ivars
struct objc_method_list * _Nullable * _Nullable methodLists
struct objc_cache * _Nonnull cache
struct objc_protocol_list * _Nullable protocols
}
这个结构体就是整个类对象,包含了实例变量和实例方法等所有信息,我们称这个结构体包含的数据为元数据(metadata),在metadata中我们可以看到第一个参数依然是一个Class类型的 isa指针,这个类对象的isa指针指向的对象,我们称之为 元类(metaclass),元类中存储了类变量和类方法等所有信息。
下面是一张网上copy的图,很详细的描述了整个闭环:
代码解释:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
//输出1
NSLog(@"%d", [p class] == object_getClass(p));
//输出0
NSLog(@"%d", class_isMetaClass(object_getClass(p)));
//输出1
NSLog(@"%d", class_isMetaClass(object_getClass([Person class])));
//输出0
NSLog(@"%d", object_getClass(p) == object_getClass([Person class]));
//元类的isa指向哪里 输出1
NSLog(@"%d", object_getClass([[Person class]class]) == object_getClass([Person class]));
}
return 0;
}
tips:
1、元类的isa指向哪里? 指向自己
2、类对象的super_class指向其父类,一直到NSObject,NSObject的super_class是 nil
3、NSObject 的元类的父类指向 NSObject 类
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 测试代码
[NSObject foo];
[[NSObject new] foo];
执行结果:编译运行正常,两行代码都执行-foo。 [NSObject foo]方法查找路线为 NSObject metaclass –super-> NSObject class,
三、元数据(vars、methods、protocols)
从类对象的结构体我们可以看到下面四个结构体:
类对象中包含了实例数据,元类对象中包含了类数据。
struct objc_ivar_list * _Nullable ivars 属性列表
struct objc_method_list * _Nullable * _Nullable methodLists 方法列表
struct objc_cache * _Nonnull cache 缓存
struct objc_protocol_list * _Nullable protocols 协议列表
1、属性列表
2、方法列表
这个列表其实是一个字典,key是selector,value是IMP(imp是一个指针类型,指向方法的实现),并且selector和IMP之间的关系是在运行时才决定的,而不是编译时。如此们就可以做出一些特别事情来。
3、缓存
每个类对象都有一份独立的缓存,同时包括继承的方法和在该类中定义的方法。
//下面来剖析一段苹果官方方法查找运行时源码:
static Method look_up_method(Class cls, SEL sel, BOOL withCache, BOOL withResolver) {
// 1. 声明IMP
Method meth = NULL;
// 2. 从cache中查找
if (withCache) {
meth = _cache_getMethod(cls, sel,&_objc_msgForward_internal);
if (meth == (Method)1) {
// cache中包含了这个方法的话,就停止搜索
// Cache contains forward:: . Stop searching.
return NULL;
}
}
// Ivar class_getInstaceMethod(Class cls, SEL name);
// Ivar class_getClassMethod(Class cls, SEL name);
// 3. 如果找不到从方法列表中查找(包括类方法列表和对象方法列表)
if (!meth) meth = _class_getMethod(cls, sel);
// 4. 将找到的方法缓存到cache中
if (!meth && withResolver) meth = _class_resolveMethod(cls, sel);
return meth;
}
1,首先去该类的方法 cache 中查找,通过SEL查找对应函数method(cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度)如果找到了就返回它;
2,如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP 返回,并将 它加入 cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次 调用再次查找的开销。
3,如果在该类的方法列表中没找到对应的 IMP,在通过该类结构中的 super_class指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的 IMP,返回它,并加入 cache 中;
4,如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则看是不是可以进行动态方法解析
5,如果动态方法解析没能解决问题,进入下面要讲的消息转发流程。
4、实例方法查找路线
1、根据实例对象的isa指针去该对象的类对象方法列表中查找,如果找到了就执行
2、如果没有找到,就去该类的父类类对象方法列表中查找
3、如果没有找到就一直往上找,直到根类(NSObject)
4、如果都没有就会调用NSObjec的一个方法doesNotRecognizeSelector:,这个方法就会报unrecognized selector错误(其实在调用这个方法之前还会进行消息转发,还有三次机会来处理,消息转发在后文会有介绍)。
5、类方法查找路线
1、根据类对象的isa指针,去元类对象的方法列表中查找,如果找到了就执行
2、如果没有找到,就去该元类的父类类对象方法列表中查找
3、如果没有找到就一直往上找,直到根类(NSObject)
4、如果NSObject的元类对象方法列表里面也没有,则找到 NSObject类对象 的方法列表 (因为NSObject元类的父类是NSObject类)
5、如果都没有就会调用NSObjec的一个方法doesNotRecognizeSelector:,这个方法就会报unrecognized selector错误(其实在调用这个方法之前还会进行消息转发,还有三次机会来处理,消息转发在后文会有介绍)
四、消息传递
消息传递只要是写iOS的基本也都知道是什么个意思,这里分析一些细节点:
先了解几大内容:
Method
一个方法 Method,其包含一个方法选标 SEL – 表示该方法的名称,一个 types – 表示该方法参数的类型, 一个 IMP - 指向该方法的具体实现的函数指针。
struct objc_method {
SEL _Nonnull method_name //方法名称 也就是selector
char * _Nullable method_types //方法的参数类型
IMP _Nonnull method_imp //函数指针,指向方法的具体实现
}
// IMP 和 SEL是在运行时才配对的,一对一,通过selector去查找IMP,找到执行方法的地址,才能确定具体的执行代码
SEL
指向objc_selector的指针,表示方法的名字
typedef struct objc_selector *SEL;
当不同类的实例对象 performSelector相同的selector的时候,会在各自的方法链表,根据SEL查找对应的IMP,然后执行,不同类可以有相同selector
IMP
函数指针,指向方法的实现
typedef id (*IMP)(id, SEL, ...)
IMP 函数指针,包含一个接受消息的对象id,调用方法的方法名 SEL,以及参数个数,并返回一个id
IMP 是消息最终调用的代码
objc_msgSend
objc_msgSend(p, sel_registerName("showMyself"));
OC的runtime通过objc_msgSend函数将一个面向对象的消息传递转为了面向过程的函数调用。
objc_msgSend函数根据消息的接受者和selector选择适当的方法来调用,那它又是如何选择的呢?
我们分析一下 一个成功执行的 objc_msgSend
参数:消息接收者,方法名,消息参数
[person run]; --> objc_msgSend(person, @selector(run));
objc_msgSend消息函数做了动态绑定所需要的一切工作:
1、找到SEL对应的方法实现IMP,不同的类对同一个方法名会有不同的实现 (前面提及的方法查找)
2、将消息接收者对象(指向对象的指针) 以及方法中指定的参数传递给方法实现IMP
3、最后,将方法实现的返回值作为该函数的返回值返回
找到对应的方法实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,他还将传递两个隐藏参数 -- 消息接受者 以及 方法名SEL,(消息接受者去调用对应的方法实现)
如果查找失败就会调用NSObjec的一个方法doesNotRecognizeSelector:,这个方法就会报unrecognized selector错误
五、消息转发
看完前面的消息发送,基本了解消息是怎么一回事了,前面提及如果查找失败会自动调用到根类 NSObject 里面的错误响应方法,但在这之前其实还有一个消息转发。
会调用所属类的方法
+(BOOL)resolveInstanceMethod:(SEL)name (所属类动态方法解析)
- (id)forwardingTargetForSelector:(SEL)aSelector;(备援接收者)
- (void)forwardInvocation: (NSInvocation*)invocation;(消息重定向)
1、所属类动态方法解析
在接收者对象中实现 resolveInstanceMethod: 即可以捕获方法查找并处理。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(doSth)) {
//动态添加一个方法实现,后续会解释
class_addMethod(self, @selector(doSth), class_getMethodImplementation(self, @selector(doOther)), "v@:");
}
return [super resolveInstanceMethod:sel];
}
- (void)doOther {
NSLog(@"doOther");
}
2、备援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [Human new];
}
如果接收者没有这个方法的实现,可以选择其他接收者返回回去。在其他接收者对象当中,甚至不需要再头文件将方法名暴露出来,系统会找到要转发的类,自动查找。
如果不处理 返回 nil。
3、消息重定向
forwardInvocation 方法并不会直接进,必须要 methodSignatureForSelector 能返回一个方法签名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(doSth)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(doSth)) {
[anInvocation invokeWithTarget: [Human new]];
return;
}
[super forwardInvocation:anInvocation];
}
methodSignatureForSelector用于描述被转发的消息,系统会调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
签名参数具体怎么编写?
首先,先要了解的是,每个方法都有self和cmd两个默认的隐藏参数,self即接收消息的对象本身,cmd即是selector选择器,所以,描述的大概格式是:返回值@:参数。@即为self,:对应_cmd(selector),返回值和参数根据不同函数定义做具体调整。
比如这个函数 -(void)testMethod;
返回值为void,没有参数,按照上面的表格中的符号说明,再结合上面提到的概念,这个函数的描述即为 v@:
v代表void,@代表self(self就是个对象,所以用@),:代表_cmd(selector)
再练一个 -(NSString *)testMethod2:(NSString *)str;
描述为 @@:@
第一个@代表返回值NSString*,对象;第二个@代表self,:代表_cmd(selector);第三个@代表参数str,NSString对象类型。如果实在拿不准,不会写,还可以简单写段代码,借助method_getTypeEncoding方法去查看某个函数的描述,比如:
Method method = class_getInstanceMethod(self.class, @selector(testMethod2));
const char *des = method_getTypeEncoding(method);
NSString *desStr = [NSString stringWithCString:des encoding:NSUTF8StringEncoding];
NSLog(@"%@",desStr);
return @"";
更多描述的格式要遵循以下规则点击打开链接.
六、动态属性
允许获取属性列表、动态添加属性,特别是在一些系统库或者三方库的基础上需要扩展一个属性,这个时候动态添加一个属性就很有意义。
property
unsigned int count = 0;
//添加property
objc_property_attribute_t attributes = {"T@\"NSString\",&,N,V_age"};
class_addProperty([p class], "age", &attributes, 1);
//获取property列表
objc_property_t *list = class_copyPropertyList([p class], &count);
关联对象
管理关联对象的方法:
objc_setAssociatedObject(id object, void * key, id value,
对象关联类型
关联类型 | 等效的@property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
动态添加Ivar
重点: 不能在已存在的class中添加Ivar,必须通过objc_allocateClassPair动态创建一个class,才能调用class_addIvar创建Ivar,最后通过objc_registerClassPair注册class。
添加变量
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size,
uint8_t alignment, const char * _Nullable types)
name : 变量名称、
size: 变量大小、
alignment: 变量对齐数
alignment获取方式: 如果是NSString类型 alignment = log2(_Alignof(NSString *) 或者 log2(sizeOf(NSString *))
types: 变量编码类型
七、Method Swizzling
这个应该是运行时再日常开发中最常用的功能了。
OC中的 + load方法,用来做Method Swizzling最合适。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originSEL = @selector(doSth);
SEL newSEL = @selector(doOther);
Method originMethod = class_getInstanceMethod(self, originSEL);
Method newMethod = class_getInstanceMethod(self, newSEL);
BOOL result = class_addMethod(self, originSEL, class_getMethodImplementation(self, newSEL), method_getTypeEncoding(newMethod));
if (result) {
class_replaceMethod(self, newSEL, class_getMethodImplementation(self, originSEL), method_getTypeEncoding(originMethod));
} else {
method_exchangeImplementations(originMethod, newMethod);
}
});
}
- (void)doSth {
NSLog(@"doSth");
// [self doSth];
}
- (void)doOther {
NSLog(@"doOther");
[self doOther];
}
注意点:当方法实现交换后,再次想调用原始方法 需要调用新的方法名。
iOS + initialize 与 +load 方法
1.load方法的调用时机,main函数之前,先调用类中的,再调用类别中的(类别中如果有重写),依赖库类优先调用。
应用场景:runtime交换方法实现
2.initialize方法的调用时机,当向该类发送第一个消息(一般是类消息首先调用,常见的是alloc)的时候,先调用类中的,再调用类别中的(类别中如果有重写);如果该类只是引用,没有调用,则不会执行initialize方法。
应用场景:初始化常用静态变量
动态判断、动态检查
isKindOfClass
isMemberOfClass
isSubclassOfClass
instancesRespondToSelector
respondsToSelector
performSelector 执行原理
performSelector是运行时系统负责去找方法的,在编译时候不做任何校验;
Cocoa支持在运行时向某个类添加方法,即方法编译时不存在,但是运行时候存在,这时候必然需要使用performSelector去调用。所以有时候如果使用了performSelector,为了程序的健壮性,会使用检查方法
- (BOOL)respondsToSelector:(SEL)aSelector;
performSelector 最终走的还是objc_msgSend的调用
动态加载:根据需求加载所需要的资源