下面代码输出什么?
self super
@implementation Son : Father
- (id)init
{
self = [super init];
if (self){
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
这道面试题,主要是考察self与super的
OC中调用方法,会被转成消息发送机制的函数 objc_msgSend(id self, SEL cmd, ...),因此,方法内self与objc_msgSend()函数中的self是等价的,就是调用时传入的消息的接受者(Objective-C高级编程 p94)。
super是一个编译器指示符
当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。这种机制到底底层是如何实现的?
例如,当调用[super class]时,会转为 objc_msgSendSuper(),而非objc_msgSend(),看下 objc_msgSendSuper 的函数定义:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
struct objc_super {
id receiver;
Class superClass;
};
当编译器遇到 Son 里 init 方法里的 [super class] 时,开始做这几个事:
构建 objc_super 的结构体变量,此时该变量的第一个成员变量 receiver 就是当前方法内的self(Son *)而第二个成员变量 superClass 就是指当前方法所属类的父类 Father
调用 objc_msgSendSuper ,将结构体变量和 @selector(class) 传过去
objc_msgSendSuper函数里面在做的事情类似这样:从 objc_super 结构体指向的 superClass 的方法列表开始找 @selector(class) ,找到后再以 objc_super->receiver 去调用这个 selector,可能也会使用 objc_msgSend 这个函数
所以,当调用[self class]时,此时的self,就是init方法的接受者Son *。因此,[self class]转为objc_msgSend(),第一个参数是Son *,第二个参数是@selector(class)。根据isa先从Son类开始找,没有,然后到 Son的父类 Father中去找,也没有,再去 Father 的父类 NSObject 去找,一层一层向上找之后,在 NSObject 的类中发现这个 class 方法
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
self就是消息的接受者Son*,因此输出 Son
当使用 [super class] 时,这时要转换成 objc_msgSendSuper 的方法。先构造 objc_super 的结构体变量,第一个成员变量就是 self(Son *)第二个成员变量是 Father,然后要找@selector(class) 。先去 superClass 也就是 Father 中去找,没有,然后去 Father 的父类中去找,结果还是在 NSObject 中找到了。然后内部使用函数 objc_msgSend(objc_super->receiver, @selector(class)) 去调用,此时已经和我们用 [self class] 调用时相同了,此时的 receiver 还是Son *,所以这里输出的还是Son
其实很好理解。例如,在Son init方法中,调用[super init];,消息的接受者还是self,不然给谁初始化?只是,查找方法从父类开始了。
isKindOfClass 与 isMemberOfClass
下面代码输出什么?
@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}
先来分析一下源码这两个函数的对象实现
类对象调用class方法,直接返回类本身
+ (Class)class {
return self;
}
实例对象调用class方法,返回对象的isa
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class objc_object::getIsa()
{
if (isTaggedPointer()) {
uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
return ISA();
}
inline Class objc_object::ISA()
{
assert(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}
无论是实例对象还是类对象,object_getClass(obj),均返回对象isa
类对象调用这个与元类比较,由于元类同样的继承关系,只要是类对象的元类是所比较元类或其子类都返回真
+ (BOOL)isKindOfClass:(Class)cls {
//tcls 等价 于tcls != nil 因为根元类的父类是NSObject,NSObject的父类是nil
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
//能这么比较 tcls == cls,比较地址,也说明了 元类对象的唯一
if (tcls == cls) return YES;
}
return NO;
}
实例对象调用与类对象比较,只要是实例对象的类是所比较元类或其子类都返回真
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
//能这么比较 tcls == cls,比较地址,也说明了 类对象的唯一
}
类对象与元类对象比较,只有类对象的元类与所比较的元类相同才为真
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
实例对象与类对象比较,只有实例对象的类与所比较的类相同才为真
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
这道题,除了考察这几个方法还是考察了类与元类的继承体系,尤其是NSObject
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
[NSObject class]还是NSObject,NSObject的元类是根元类,与[NSObject class] == NSObject不等。循环,根元类的父类是NSObject,相等。res1 = YES
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
[NSObject class]还是NSObject,NSObject的元类是根元类,与[NSObject class] == NSObject不等
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
[Sark class]还是Sark,Sark的元类与Sark不等。循环,Sark元类的父类是根元类,与Sark不等。循环,根元类的父类是NSObject,与Sark不等。NSObject的父类是nil,循环结束。
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
Sark的元类与Sark不等
总结,isMember比较傲娇,调用者只取一次isa不等就是不等。isKindOf调用者取一次isa 不等取其父类
Class与内存地址
下面的代码会?Compile Error / Runtime Crash / NSLog…?
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Person
- (void)speak {
NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
首先编译能不能通过?其次,调用输出什么?
答案是可以编译通过,输出 my name is
这道题,涉及到实例变量的内存结构和类与对象的关系
cls指向Sark类的指针,而obj是一个指针,存储着cls的地址,也就指向obj的指针,类似一个二级指针。然后调用实例方法。这是怎么回事呢?
首先,将C语言的指针类型转为OC的id类型,但方法也不能随便调用,这个方法存在当前类或者导入头文件的声明中,而我们导入了#import "Person.h",编译通过。
然后,cls指向了Person类,而我们定义一个对象Person *p = [Person new]; 对象的isa也指向Person类,而实例对象的第一个成员是isa,因此实例对象的地址与isa的地址是相同的。obj指向cls,如同指向了实例对象的isa,如同指向了对象。那么,也就是cls如同一个实例对象,obj类似一个指向实例对象的指针,即p。
Person *person = [[Person alloc] init];
NSLog(@"%@",person); //
NSLog(@"%p %p",person,&person); //0x6000000098c0 0x7fff5a353a88
一个是指针所指对象地址,一个是指针地址
我们说过,方法最终转为函数,而函数默认两个参数,一个是消息的接受者,一个是选择子,对应的是参数名是self与_cmd。我们在viewDidLoad打印几个地址:
NSLog(@"%p",self); //0x7fe5e2e0b570
NSLog(@"%@",self); //
NSLog(@"%p %p",&self,&_cmd); //0x7fff58e08aa8 0x7fff58e08aa0
Class cls = [Person class];
void *p = &cls;
NSLog(@"%p %p",cls,&cls); //0x106df8140 0x7fff58e08a88
NSLog(@"%p %p",p,&p); //0x7fff58e08a88 0x7fff58e08a80
首先是ViewController实例对象的地址,然后是指向实例对象,指向选择子的指针地址,
NSLog(@"%p %p",cls,&cls); 打印的是cls指向的Person类的地址和cls的地址
NSLog(@"%p %p",p,&p); 打印的是p指向的cls的地址和p的地址
cls如同Person的实例对象,而p如同指向实例对象的指针,调用Person的方法
- (void)speak {
NSLog(@"%p",&_cmd);//0x7fff58e08a40
NSLog(@"%p %p",self,&self);//0x7fff58e08a88 0x7fff58e08a48
NSLog(@"my name's %@", self.name);
}
我们看到speak方法内,self指向的地址是0x7fff58e08a88,正是实例变量cls的地址。但是很明显,这个地址7fff与Person类地址106明显不像,一个是栈上的地址,一个是堆上的地址。
我们知道内存,对内存进行编制后,由下到上,地址越来越大,栈空间在上,分配时由上到下,越后分配的地址越小,而堆空间在下,分配时由下而上,越后分配的地址越大
cls的栈地址也说明了,它不是一个真正的分配在堆上的对象,或许这是披了一件外衣。但是,只有我们知道。
打印self.name时。前面说过实例对象在类中的内存结构。
Person对象
isa
*name
当一个类被编译时,实例变量的布局也就形成了,访问类的实例变量。从对象头部开始,实例变量依次根据自己所占空间而产生偏移量。
查找self.name也就是在实例变量的起始地址,偏移8个字节(64位下isa 8个字节),就得到name的首地址,而实例变量的首地址是 0x7fff58e08a88,偏移8个字节。(按字节编制,每个地址八位)
90 name
8f
8e
8d
8c
8b
8a
89
88 Person *
要去0x7fff58e08a90 查找name指针指向的字符串对象。可那么为什么会输出viewController相关的呢?
我们回到viewDidLoad方法,
NSLog(@"%p %p",&self,&_cmd); //0x7fff58e08aa8 0x7fff58e08aa0
我们调用函数,参数入栈,self,_cmd的地址是a8,a0,方法内,[super viewDidLoad]; 我们前面说过super是个指示符,需要构造结构体作为函数参数
struct objc_super {
id receiver;
Class superClass;
};
superClass指针是self所在类的父类 98,receiver指针指向self所指的实例对象 90,,至于为什么_cmd没分配,我也不知道。接下来的代码就是 Class cls = [Person class]; cls是88,void *p = &cls; p是80
这也就符合我们的打印结果,那么,我们上面寻找的90就是self了。因此,打印 my name's %@ 就是self所指的对象ViewController了。