欢迎阅读iOS探索系列(按序阅读食用效果更加)
- iOS探索 alloc流程
- iOS探索 内存对齐&malloc源码
- iOS探索 isa初始化&指向分析
- iOS探索 类的结构分析
- iOS探索 cache_t分析
- iOS探索 方法的本质和方法查找流程
- iOS探索 动态方法解析和消息转发机制
- iOS探索 浅尝辄止dyld加载流程
- iOS探索 类的加载过程
- iOS探索 分类、类拓展的加载过程
- iOS探索 isa面试题分析
- iOS探索 runtime面试题分析
- iOS探索 KVC原理及自定义
- iOS探索 KVO原理及自定义
- iOS探索 多线程原理
- iOS探索 多线程之GCD应用
- iOS探索 多线程之GCD底层分析
- iOS探索 多线程之NSOperation
- iOS探索 多线程面试题分析
- iOS探索 细数iOS中的那些锁
- iOS探索 全方位解读Block
写在前面
本文涉及的面试题如下:
isKindOfClass
和isMemberOfClass
的区别[self class]
和[super class]
的区别- isa综合运用——内存偏移
一、isKindOfClass 和 isMemberOfClass
这是一道涉及isa走位图
的面试题,大胆猜测下结果
#import <Foundation/Foundation.h>
#import "FXPerson.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];//1
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];// 0
BOOL re3 = [(id)[FXPerson class] isKindOfClass:[FXPerson class]];//0
BOOL re4 = [(id)[FXPerson class] isMemberOfClass:[FXPerson class]];// 0
NSLog(@"\n re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];//1
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];// 1
BOOL re7 = [(id)[FXPerson alloc] isKindOfClass:[FXPerson class]];//1
BOOL re8 = [(id)[FXPerson alloc] isMemberOfClass:[FXPerson class]];// 1
NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}
return 0;
}
这里先不揭晓答案,先来探索一下isKindOfClass
和isMemberOfClass
的实现
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- 这是一个类似于
for (int i = 0; i < 3; i ++)
的for循环object_getClass
得到当前类对象的类——元类
,初始化tcls
- 只要
tcls
有值就可以继续循环,即当tcls
为nil
时结束for循环 - 取得
tcls
的父类,作为它的新值,继续下一次循环
- 当for循环中有一次
tcls == cls
,返回YES
- 结束for循环时还没满足条件就返回
NO
结论一:+isKindOfClass是元类及其父类 vs 类
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
object_getClass
得到当前类对象的类——元类
,和类本身cls
进行比较- 相较于
+isKindOfClass
少了父类的比较,因此+isMemberOfClass
为YES时可以得到+isKindOfClass
为YES
结论二:+isMemberOfClass是元类 vs 类
结合isa走位图
(实线为父类走向)可以得出前面四个打印结果:
NSObject元类
与NSObject类
不相等,NSObject元类的父类
(指向NSObject类
)与NSObject类
相等——YESNSObject元类
与NSObject类
不相等——NOFXPerson元类
与FXPerson类
不相等,FXPerson元类的父类
与FXPerson类
不相等——NOFXPerson元类
与FXPerson类
不相等——NO
换成实例对象调用-isKindOfClass
和-isMemberOfClass
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
同理可得:-isMemberOfClass
是拿实例对象的类(即当前类)和cls
作比较,-isKindOfClass
多了一步for循环类对象的父类
结论三:-isKindOfClass是类本身及其父类 vs 类
结论四:-isMemberOfClass是类本身 vs 类
后面四个结果分析如下:
NSObject类
与NSObject类
相等——YESNSObject类
与NSObject类
相等——YESFXPerson类
与FXPerson类
相等——YESFXPerson类
与FXPerson类
相等——YES
二、[self class] 和 [super class]
FXSon
继承于FXFather
,主程序初始化FXSon
,求问打印内容以及思路
#import "FXSon.h"
@implementation FXSon
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"[self class] = %@", NSStringFromClass([self class]));
NSLog(@"[super class] = %@", NSStringFromClass([super class]));
NSLog(@"[self superclass] = %@", NSStringFromClass([self superclass]));
NSLog(@"[super superclass] = %@", NSStringFromClass([super superclass]));
}
return self;
}
@end
打印结果如下
emmm...有点出乎意料,[self class]
点进去来到NSObject.mm
文件查看源码
class
方法返回类superclass
方法返回类的父类
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
+ (Class)superclass {
return self->superclass;
}
- (Class)superclass {
return [self class]->superclass;
}
从这段代码能解释[self class]
和[self superclass]
,但是另外两个又怎么解释呢?
终端clang编译代码得到super.cpp
,就能看到初始化的底层代码了
clang -rewrite-objc FXSon.m -o super.cpp
已知调用方法就是发送消息objc_msgSend
,那objc_msgSendSuper
也是发送消息吗?
查看源码中对objc_msgSendSuper
的定义,注释中提示了objc_super
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
/// Specifies the superclass of an instance.
struct objc_msgSendSuper {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
从以上源码得出,使用objc_msgSendSuper
向objc_super
发送消息,而objc_super
在objc2.0
下有两个元素——id
类型的receiver
和Class
类型的super_class
其实早在iOS探索 方法的本质和消息查找流程中就提过这个方法,然后笔者进行了仿写
记得导入<objc/message.h>
,报错Too many arguments
就去修改编译期配置
[super class]
一样的结果了
那苹果为什么要这么设计呢?把消息查找
和isa走位图
联系起来就明白了!
son实例对象
的实例方法
存在FXSon类
中
- 调用
[self class]
就是son照着FXSon->FXFather->NSObject
顺序问老爸要-class方法
- 调用
[super class]
就是son跳过FXSon
,直接通过FXFather->NSObject
查找 - 还有比
[super class]
更快找到class方法的写法- 结构体中
[self class]
改为[super class]
,直接找到NSObjct
- 结构体中
补充: 当结构体中[self class]
改为[FXFather class]
时,因为类方法存在元类中,会按FXFather元类->NSObject元类->NSObject根元类
找+class
方法,最后也是会输出FXSon
结论:
[self class]
就是发送消息objc_msgSend
,消息接收者是self
,方法编号是class
[super class]
就是发送消息objc_msgSendSuper
,消息接收者是self
,方法编号是class
,只不过objc_msgSendSuper
会跳过self
的查找
三、内存偏移
这是一道比较经典的“丧心病狂”的内存偏移面试题,如果你没有研究过,大概率很难答上来
1.原始题
程序能否运行吗?是否正常输出?
#import "ViewController.h"
@interface FXPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation FXPerson
- (void)printMyProperty {
NSLog(@"当前打印内容为%s", __func__);
}
@end
//——————————————————————————————————————//
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [FXPerson class];
void *obj= &cls;
[(__bridge id)obj printMyProperty];
FXPerson *p = [FXPerson new];
[p printMyProperty];
}
@end
运行结果与普通初始化对象一模一样,可面试的时候不可能只说能或不能,还要说出个所以然来
正常初始化:指针p->实例对象isa->类对象
- 对象的本质为
objc_object
,第一个元素为isa
指针p
存储着FXPerson类
实例出来的对象内存地址,所以指针p
指向对象的首地址——p->实例对象isa
实例对象
的isa指向类对象
——实例对象isa->类对象
骚操作:指针obj->指针cls->类对象
id cls = [LGPerson class]
获取到类对象指针void *obj= &cls
获取到指向该类对象cls的对象obj
2.拓展一
修改打印方法printMyProperty
——不但打印方法,同时打印属性name
- (void)printMyProperty {
NSLog(@"当前打印内容为%s——————%@", __func__, self.name);
}
重新运行代码,得到结果如下
当前打印内容为-[FXPerson printMyProperty]——————<ViewController: 0x7fc72cd09450>
当前打印内容为-[FXPerson printMyProperty]——————(null)
为什么属性name还没有赋值,却打印出了ViewController
的内存地址?
- 由于栈
先入后出
,viewDidLoad
入栈先拉伸栈空间,然后依次放入self、_cmd
局部变量 - 调用
[super viewDidLoad]
,继续放入super_class、self
- 正常情况下获取name,本质是p的内存地址往下偏移8字节
- 同样的骚操作也是obj的内存地址往下偏移8字节得到
self
3.拓展二
修改viewDidLoad
——在obj
前面加个临时字符串变量
- (void)viewDidLoad {
[super viewDidLoad];
NSString *temp = @"1";
id cls = [FXPerson class];
void *obj= &cls;
[(__bridge id)obj printMyProperty];
FXPerson *p = [FXPerson alloc];
[p printMyProperty];
}
重新运行代码,得到结果如下
当前打印内容为-[FXPerson printMyProperty]——————1
当前打印内容为-[FXPerson printMyProperty]——————(null)
同样道理,在obj
入栈前已经有了temp
变量,此时访问self.name
就会访问到temp
4.拓展三
去掉临时变量,FXPerson类
新增字符串属性hobby
,打印方法改为打印hobby
,运行
ViewController
就是obj
偏移16字节拿到的super_class
([super viewDidLoad]压栈进去的)
5.拓展四
①去掉[super viewDidLoad]
,运行
②临时变量改成NSInteger类型
这两种情况就是野指针
——指针偏移的offset不正确,获取不到对应变量的首地址
6.万变不离其宗的理论
int a = 1;
int b = 2;
int c = 3;
int d = 4;
NSLog(@"\na = %p\nb = %p\nc = %p\nd = %p\n",&a,&b,&c,&d);
打印结果
a = 0x7ffee0ebd1bc
b = 0x7ffee0ebd1b8
c = 0x7ffee0ebd1b4
d = 0x7ffee0ebd1b0
局部变量的存放顺序,是根据定义的先后顺序,从函数栈底(高地址)开始,一个一个排列
关于这题还没搞明白的可以看下Runtime笔记(八)—— 面试题中的Runtime里面的图很形象
写在后面
面试题是面试官用知识点变着法玩你的一种手段,同时也能表现出你掌握知识的熟练度。只有在平时多练习多研究,才能在面试的时候给面试官留下一个好的印象