Objective-C runtime机制(11)——结业考试

  1. 下面代码的输出结果?
	    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, isMemberOfClassclass方法均有两个版本:实例方法 和 类方法两个版本。 也意味着,类实例和类调用这三个方法的实现,是不一样的。我们来看它们的具体实现:

+ (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的底层实现,再结合下面这张经典的图,就可以给出答案:
Objective-C runtime机制(11)——结业考试_第1张图片

答案会输出

1 0 0 0 
  1. 下面代码会输出什么?编译错误?运行时crash?
@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 classRoot meta class中查找是否有对应的foo实现。没有,则会到Root meta classsuper class,也就是NSObject class中寻找对应的实现。而在NSObject class中,我们定义了foo的实现(不必区分是+还是-实现),因此, [NSObject foo]会调用 NSLog(@"AAAA");实现。

当调用 [[NSObject new] foo]时,则会到NSObject中寻找对应的实现。这个自然也会调用NSLog(@"AAAA");
但是,在Xcode中做了一个检查,当在头文件中没有声明对应的方法时,会报错。而这里只有+ foo,却没有- foo,因此,会报错。但反过来,如果只定义了- foo,确可以用类方法形式调用 [NSObject foo]的。

  1. 试分析如下代码
#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

Objective-C runtime机制(11)——结业考试_第2张图片

附加题 :
下面的代码会: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的存在,编译器会自动在我们代码的合适位置插入retainrelease代码。但仍要明确,OC类实例对象是分配在堆上的

- (void)viewDidLoad {
		...
		id sark = [Sark new];
		...
}

可以理解为:

在等号右边生成了一个堆上的实例对象,并会把堆地址返回给等号左边,id sark这个指针也就存储了这个堆地址。

Objective-C runtime机制(11)——结业考试_第3张图片

在图中,我们还看到了self,其本质也是一个指向实例的指针,当我们调用self方法的时候,系统会在栈上为我们生成一个指向Sark实例的指针,指向Sark实例的堆地址

你可能感兴趣的:(ios开发,Objective-C,runtime,漫游指南)