经过对类,Runtime等底层的相关探索,对原理已经有了掌握,本章经过几个面试题来加深印象,会保持持续更新。面试
先上几篇原理文章,对面试题的理解会更深入缓存
传送门☞iOS底层学习 - Runtime之方法消息的前世此生(一)bash
传送门☞iOS底层学习 - Runtime之方法消息的前世此生(二)markdown
答:是由C 和C++ 汇编 实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能。平时编写的OC代码,在程序运⾏过程当中,其实最终会转换成Runtime的C语⾔代 码,Runtime
是 Objective-C
的幕后⼯做者。app
好比:将数据类型的肯定由编译时推迟到了运⾏时,比较典型的就是类的ro
和rw
属性。ro
在编译期就肯定好(read-only),而rw
是运行时才肯定,能够进行修改(read-write)。像下面的例子就比较典型👇框架
方法的本质就是消息的发送,就底层_objc_msgSennd
方法寻找方法IMP的过程,主要经历了如下几个步骤:ide
objc_msgSend
)查找cache_t
中缓存的消息lookUpImpOrForward
递归查找当前类和父类的rw
中methodlist
的方法resolveInstanceMethod
方法,来实现消息动态处理CoreFoundation
框架来触发消息转发流程,forwardingTargetForSelector
实现快速转发,其余类可实现处理方法methodSignatureForSelector
方法,来获取到方法的签名,从在生成相对应的invocation
,经过forwardInvocation
来对invocation
进行处理,通常处置崩溃都在此处理SEL
是方法编号,也是方法名,在dyld加载镜像带内存时,经过_read_image
方法加载到内存的表中了函数
IMP
就是咱们函数实现指针 ,找IMP
就是找函数的过程oop
SEL
就至关于书本的⽬录 tittle,post
IMP
就是书本的⻚码,
函数
就是具体页码对应的实现内容
查找具体的函数就是想看这本书⾥⾯具体篇章的内容
不能。
由于咱们编译好的实例变量存储的位置在ro
,⼀旦编译完成,内存结构就彻底肯定 就⽆法修改,只能修改rw
中的方法或者能够经过关联对象的方式来添加属性
关联对象添加的主要步骤以下:
objc_setAssociatedObject
设置set
方法:找到关联对象的总哈希表,而后经过指针地址找到该类的哈希表,而后经过key值进行存储objc_getAssociatedObject
设置get
方法:和set方法同样查询表,找到值dealloc
会清除关联对象的哈希表isKindOfClass
和 isMemberOfClass
区别先上例子🌰
1.第一个
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
复制代码
👆上述Log的打印结果为1,0,0,0
2.第二个
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; // 1
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; // 1
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]]; // 1
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]]; // 1
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
复制代码
👆上述Log的打印结果为1,1,1,1
相信对于第二个例子,你们都使用的很是熟练。咱们发现两个例子的主要区别在于消息接受者是实例对象仍是类对象,打印结果,咱们看源码探究
既然[NSObject class]
类也能够调用这两个方法,说明这两个方法是有对应的类方法和实例方法的,只不过咱们平时不适用类方法而已😂
/*********************************************************************** * object_getClass. * Locking: None. If you add locking, tell gdb (rdar://7516456). **********************************************************************/ Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } 复制代码
✅//object_getClass()取得的是对象的isa指针指向的对象,也就是判断传入的类对象的元类对象是否与传入的这个对象相等,因此这个cls应该是元类对象才有可能相等 + (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } ✅//判断传入的实例对象的类对象是否与传入的对象相等,因此cls只有多是类对象才有可能相等 - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } ✅//循环判断传入的类对象的元类对象及其父类的元类对象是否等于传入的cls + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } ✅//循环判断实例对象的父类的类对象是否等于传入的对象cls,也就是判断实例对象是不是cls及其子类的一种 - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } 复制代码
经过这个两个方法的源码咱们能够知道,
isMemberOfClass:
是检测方法调用者对象的类是否等于传入的这个类。isKindOfClass:
是判断方法调用者对象的类是否等于传入的这个类或者其子类。还有一个适用于这四个方法的一点是,若是方法调用者是实例对象,那么传入的就应该是类对象;若是方法调用者是类对象,那么传入的就应该是元类对象。
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
为啥打印是1呢?答:[NSObject class]
类对象调用isKindOfClass
代表其元类会递归判断是否等于当前类或者其父类,咱们知道NSObject
的元类为根元类,根据继承链关系根元类的父类即为NSObject
类对象,即[NSObject class]
,因此相等
建立一个Student
类继承子Person
类,下面代码打印出什么
NSLog(@"[self class] = %@", [self class]); NSLog(@"[super class] = %@", [super class]); NSLog(@"[self superclass] = %@", [self superclass]); NSLog(@"[super superclass] = %@", [super superclass]); 复制代码
先上正确答案:
2020-01-17 15:54:02.224686+0800 TEST[8409:174143] [self class] = Student
2020-01-17 15:54:02.224922+0800 TEST[8409:174143] [super class] = Student
2020-01-17 15:54:02.225040+0800 TEST[8409:174143] [self superclass] = Person
2020-01-17 15:54:02.225922+0800 TEST[8409:174143] [super superclass] = Person
复制代码
咱们发现第二个和第四个和咱们猜测的貌似不太同样,并非Person
,先上源码看一下第一个和第三个的理解
/******************************************************* ✅ //经过对象的isa指针获取类的类对象 Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } + (Class)class { return self; } - (Class)class { return object_getClass(self); } + (Class)superclass { return self->superclass; } - (Class)superclass { return [self class]->superclass; } Class class_getSuperclass(Class cls) { if (!cls) return nil; return cls->superclass; } ******************************************************/ 复制代码
咱们知道,这里方法中的self
都是指消息的接受者,在问题中就表示Student
类,根据以上源码,第一个和第三个没啥疑问,都是寻找Student
的isa
和父类,那么super
调用时,有啥不一样,咱们须要先知道super
的本质
经过clang编译代码,咱们能够发现,底层调用时super
调用的方法不是magSend
,而是objc_msgSendSuper
,传入了一个super
的结构体和方法,而super
的结构体以下
objc_msgSendSuper(object ,superclass, @selector(class))
复制代码
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
复制代码
两个参数分别为消息的接受者和父类,在这里Student
即为消息接受者,Person
为父类.
咱们知道消息发送的时候,慢速查找流程是须要从自身递归查找到NSObject
的,而objc_msgSendSuper
就表示直接从消息接受者的父类开始递归查找,跳过了自己的方法列表,这样查找的速度能够更快
因此调用[super class]
和[super superclass]
本质上消息的接受者仍是self
,即Student
类,因为class
方法的实现,实际上是在基类NSObject
中的,因此不论是从Student
类方法列表开始查询,仍是从父类Person
方法列表查询,最终都会走到基类中的class
方法,而此方法根据源码就是寻找消息接受者的isa
和superclass
,因此出现上述的打印结果
[self class]
就是发送消息objc_msgSend
,消息接受者是 self
,⽅法编号:class
[super class]
本质就是objc_msgSendSuper
, 消息的接受者仍是 self
⽅法编号:class
只是objc_msgSendSuper
会更快 直接跳过 self
的查找,可是都会走到NSObject
基类的实现方法中,可是都是以self
为接受者
主要总结以下:
经过SideTable
找到咱们的weak_table
weak_table
根据referent
找到或者建立 weak_entry_t
而后append_referrer(entry, referrer)
将新弱引⽤的对象加进去entry
最后weak_entry_insert
把entry
加⼊到咱们的weak_table
在类dealloc
时,会根据插入的步骤找到对应的弱引用,并置为nil
关于weak
的相关知识作了单独的总结,详情能够看下方文章👇
关于Method Swizzling
方法交换的总结,详情能够看下面的文章:
iOS底层学习 - Runtime之Method Swizzling黑魔法
***********************LGPerson*************************** @interface LGPerson : NSObject @property (nonatomic, copy)NSString *name; //@property (nonatomic, copy)NSString *subject; //@property (nonatomic)int age; - (void)print; @end @implementation LGPerson - (void)print{ NSLog(@"NB %s - %@",__func__,self.name); } @end **************************调用***************************** - (void)viewDidLoad { [super viewDidLoad]; NSString *tem = @"WY"; id cls = [LGPerson class]; void *obj= &cls; [(__bridge id)obj print]; // LGPerson *person = [LGPerson alloc]; // [person print]; } 复制代码
2020-01-20 17:40:15.322404+0800 LGTest[86411:20872420] NB -[LGPerson saySomething] - KC
复制代码
相信看到这个结果你们都是和我同样一脸懵逼。下面就分为2个问题,咱们来研究一下
经过结果也看到了,这段代码是能够运行的。而且成功调用了[LGPerson saySomething]方法,咱们首先看为什么能成功调用,它和咱们注释的普通的实例对象调用为什么能同样。
首先来看一下普通方法调用的时序:
如图所示:
接着来分析代码中的执行时序:
id cls = [LGPerson class];
获取到类对象指针void *obj= &cls;
获取到指向该类对象cls
的对象obj
[(__bridge id)obj print];
消息发送按照上面的时序的结构总结一下,就是:
obj
--> 指针cls
--> [LGPerson class]类地址因此从本质上来讲,最后消息发送的时候,都是根据isa
获取到类的内存空间,而后再方法列表中查找IMP
。而obj
指向的是一个cls
,可是cls
正巧也是指向类的内存空间的,因此一样也能够找到方法列表中的print
方法,进行调用
经过运行结果咱们能够看到,方法调用
self.name
居然打印出了VC
中的NSString
的临时变量的值,那么为何会出现这个结果呢?
咱们知道任何OC方法的底层都是一个C函数,而且函数头两个参数是默认参数id self
和 SEL _cmd
,那么self是谁呢
首先咱们仍是来看一下正常的调用self.name
是如何找到的
self
(消息接受者)指的就是咱们实例化的对象person
,而person
指向的是实例对象的内存空间首地址,而内存空间首地址是第一个元素isa
,占用8
字节,name
是第二个元素,也是占用8
个字节。寻找self.name
的过程就是指针偏移的过程,由于isa
占用了8
个,因此找到name
的值时,只须要向后便宜8
个字节便可。如图所示:那么咱们再来看一下代码中时如何找到self.name
的
经过上面的时序,咱们知道这二者在调用方法上是对等的。这里消息发送时的self
(消息接受者)指的就是obj
,cls
指针至关于person
指针所指向的实例对象里面的isa
指针,同理,此时指向的类的首地址,可是要找的是name
,向下指针偏移8个后,找到了生命的临时变量的值,因此会打印出来
这里就涉及到了为何会找到临时变量的问题,若是去掉了临时变量,又会打印什么呢,这里就有了函数栈空间的做用
栈空间的做用,是用来存放被调用函数其内部所定义的局部变量的。咱们都知道栈的特色是先入后出,因此先存进栈的在底层。因为方法的调用和super
的调用,都会产生局部变量,因此viewdidload
的栈示意图以下:
self
,即当前的
viewcontroller
若是加了一个临时变量NSString
,栈的结构就会变成以下所示,因此会打印它的值
能够看出此时对象的实例变量获取即为void *ivar = &obj + offset(N)
可是咱们指针偏移的offset
不正确,获取不到对应变量的首地址,此时就会出现野指针的状况,因此千万不能像代码中那么用