BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [[Son class] isKindOfClass:[Son class]];
BOOL res4 = [[Son class] isMemberOfClass:[Son class]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
解答:这道题的关键在于知道,isKindOfClass
, isMemberOfClass
和 class
方法均有两个版本:实例方法 和 类方法两个版本。 也意味着,类实例和类调用这三个方法的实现,是不一样的。我们来看它们的具体实现:
+ (Class)class {
return self;
}
- (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()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
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;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
这里需要注意的是,对于+ (BOOL)isKindOfClass:(Class)cls
和+ (BOOL)isMemberOfClass:(Class)cls
,其实现里面会再去取cls所属的class:
Class tcls = object_getClass((id)self)
对于类来说,这时候tcls是元类
。
知道了runtime的底层实现,再结合下面这张经典的图,就可以给出答案:
答案会输出
1 0 0 0
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
NSLog(@"AAAA");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[NSObject foo];
[[NSObject new] foo];
}
return 0;
}
解答:根据上面的图以及类方法会调用会在类的元类
中查找IMP。
当调用 [NSObject foo]
时,会到NSObject meta class
即 Root meta class
中查找是否有对应的foo实现。没有,则会到Root meta class
的super class
,也就是NSObject class
中寻找对应的实现。而在NSObject class
中,我们定义了foo的实现(不必区分是+还是-实现),因此, [NSObject foo]
会调用 NSLog(@"AAAA");
实现。
当调用 [[NSObject new] foo]
时,则会到NSObject中寻找对应的实现。这个自然也会调用NSLog(@"AAAA");
但是,在Xcode中做了一个检查,当在头文件中没有声明对应的方法时,会报错。而这里只有+ foo,
却没有- foo
,因此,会报错。但反过来,如果只定义了- foo
,确可以用类方法形式调用 [NSObject foo]
的。
#import
@interface Student : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, strong) NSNumber *age;
@end
@interface Sark : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, assign) double fNum;
@property(nonatomic, strong) Student *myStudent;
@property(nonatomic, strong) NSNumber *age;
- (void)speak;
@end
@interface ViewController : UIViewController
@end
#import "ViewController.h"
@implementation Student
@end
@implementation Sark
- (instancetype)init {
if (self = [super init]) {
_name = @"Lily";
}
return self;
}
- (void)speak {
NSLog(@"Instance address is %p", self); // 此处如果输出 Instance address is 0x280589860
// 那么以下该输出什么???
NSLog(@"name address is %p", &_name);
NSLog(@"fNum address is %p", &_fNum);
NSLog(@"myStudent address is %p", &_myStudent);
NSLog(@"age address is %p", &_age);
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Sark *sark = [Sark new];
Sark *sark2 = [Sark new];
NSLog(@"sark 地址=%p", &sark); // 此处如果输出 sark 地址=0x16f60d348
NSLog(@"sark2 地址=%p", &sark2); // 那么 sark2 地址=??
[sark speak];
[sark2 speak];
}
解答:这个问题实质上是考察OC内存分配时,栈和堆的区别,以及OC类实例的内存布局。
(1)对于临时变量,如sark2指针,OC会分配到栈上,而OC中的类实例则是分配在堆上的。(2)在栈上的地址是从高向低分配的,在堆上的地址是从低到高分配的。
(3)OC类实例的内存布局是线性布局,会按照实例变量的声明顺序,依次排列。这里的实例变量都是指针类型,64位操作系统中,指针占用8个字节。
请结合注释和结果分析本题:
#import "ViewController.h"
@implementation Student
@end
@implementation Sark
- (instancetype)init {
if (self = [super init]) {
_name = @"Lily";
}
return self;
}
- (void)speak {
// OC的对象 内存在堆上
// 堆: 由低向高分配内存
NSLog(@"Instance address is %p", self);
NSLog(@"name address is %p", &_name);
NSLog(@"fNum address is %p", &_fNum);
NSLog(@"myStudent address is %p", &_myStudent);
NSLog(@"age address is %p", &_age);
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 对象指针 分配在栈上
// 栈: 由高向低分配内存
NSLog(@"对象指针 在栈上分配内存----------------------");
Sark *sark = [Sark new];
Sark *sark2 = [Sark new];
NSLog(@"sark 地址=%p", &sark); // 栈上地址
NSLog(@"sark2 地址=%p", &sark2); // 栈上地址
NSLog(@"");
NSLog(@"对象 在堆上分配内存--------------------");
NSLog(@"sark--------------------");
[sark speak];
NSLog(@"");
NSLog(@"sark2--------------------");
[sark2 speak];
}
@end
附加题 :
下面的代码会:compile error/runtime crash/正常运行?
@interface Sark : NSObject
@property(nonatomic, strong) NSString *name;
- (void)showYourName;
@end
@implementation Sark
- (void)showYourName {
NSLog(@"My name is %@", self.name);
}
@end
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [Sark class];
void* obj = &cls;
[(__bridge id)obj showYourName];
}
@end
解答:
这个问题首先,来判断是否会crash。核心是分析
[(__bridge id)obj showYourName];
是否会crash。
这里如果obj是Sark的一个实例,则不会crash。但此时obj是
void* obj = &cls;
也就是Sark Class的一个指针。根据Class在runtime中的定义:
typedef struct objc_class *Class;
obj定义是
void* obj = &(objc_class *);
即
obj = objc_class **;
而我们生成一个Sark对象则是:
Sark *sarkObj = [Sark new];
我们将Sark用NSobject替换:
Sark *sarkObj = [NSObject new];
即
Sark *sarkObj = NSObject *;
而NSObject定义是:
@interface NSObject {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
可以用Class isa来替代NSObject:
Sark *sarkObj = Class *;
即
Sark *sarkObj = objc_class**;
根据
obj = objc_class **;
和
Sark *sarkObj = objc_class**;
可知
obj == sarkObj
很奇妙!这里涉及到OC中对象的本质:
Objc中的对象是一个指向objc_class类型的指针,即 id obj = objc_object * , 而对象的实例变量地址 void
*ivar = obj + offset(N)
这个结论和冰霜的文章神经病院 Objective-C Runtime 入院第一天—— isa 和 Class有些出入。但笔者认为上面的描述应该更准确些
。
在冰霜的文章中是这么总结的:
Objc中的对象是一个指向ClassObject地址的变量,即 id obj = &ClassObject , 而对象的实例变量 void *ivar = &obj + offset(N)
其中提到对象的实例变量 void *ivar = &obj + offset(N)
。 为了验证结果,我们来做个试验,有如下代码:
@interface Sark : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, strong) NSNumber *age;
- (void)speak;
@end
@implementation Sark
- (instancetype)init {
if (self = [super init]) {
_name = @"Lily";
}
return self;
}
- (void)speak {
// OC的对象 内存在堆上
// 堆: 由低向高分配内存
NSLog(@"Instance address is %p", self); // self表示当前实例的this指针,这里应该输出实例的地址(堆地址)
NSLog(@"name address is %p", &_name); //name 实例变量地址
NSLog(@"age address is %p", &_age); // age实例变量地址
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// 对象指针 分配在栈上
// 栈: 由高向低分配内存
id sark = [Sark new];
NSLog(@"&sark = %p", &sark); // 按照冰霜的结论,此处应该输出实例的地址 ("%p", self)
NSLog(@"sark = %p", sark); // 按照本文结论,此处应该输出实例的地址 ("%p", self)
[sark speak];
}
NSLog(@"sark = %p", sark);
输出了实例的地址0x281b00840
,并且也符合结论:
对象的实例变量地址 void *ivar = obj + offset(N)
此时N=8 bytes
,因为这里的实例变量都是指针类型,而指针在64位机器中占据8个bytes。
而按照冰霜博客中的方式
NSLog(@"&sark = %p", &sark);
则输出了栈地址0x16fad5348
。这是因为id sark
是分配在栈上的,直接输出&sark
,即sark的地址
,自然是栈地址。
当然,这里仅限于技术上的讨论。说实话,我个人是很敬佩冰霜兄博客的深度和漂亮的排版的,就像其博客签名所说“天道酬勤,勤能补拙
”,这种技术人的态度,值得我学习。
言归正传,为了更好的了解上面关于OC实例变量地址的问题,我们来看一下,当我们调用方法
- (void)viewDidLoad {
...
id sark = [Sark new];
...
}
系统是怎么分配内存的。
我们知道,在函数中生成的变量,被称作“临时变量”
,“临时变量”
内存是分配在栈上的。在栈上的变量是不需要我们手动管理内存的,系统会自动释放栈上的变量。在这里id类型的sark,就是一个栈变量。sark的实质是一个指针,它指向Sark类实例
。
而在等号的右边,我们调用了[Sark new]
,这会生成一个Sark 类实例对象
。而在OC中,类实例对象是在堆内存上创建的。在堆上的变量,是需要我们手动管理内存的。而这里为什么没有内存管理相关的代码呢?这是因为ARC的存在,编译器会自动在我们代码的合适位置插入retain
和release
代码。但仍要明确,OC类实例对象是分配在堆上的
。
- (void)viewDidLoad {
...
id sark = [Sark new];
...
}
可以理解为:
在等号右边生成了一个堆上的实例对象,并会把堆地址返回给等号左边,id sark这个指针也就存储了这个堆地址。
在图中,我们还看到了self
,其本质也是一个指向实例的指针,当我们调用self方法的时候,系统会在栈上为我们生成一个指向Sark实例的指针,指向Sark实例的堆地址
。