iOS 进阶知识集面试iOS学习

面试题1:NSObject 对象占用内存、isa/supercl

2018-07-08  本文已影响21人  Jacob_LJ

注:分析步骤参考 MJ底层原理班 内容,本着自己复习方便原则而记录

1 一个NSObject对象占用多少内存?

  1. 系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)
  2. 但NSObject 对象内部只使用了8个字节的空间(64bit 下,可以通过class_getInstanceSize函数获得)

1.1 OC 代码通过两种方法获得的大小

#import <Foundation/Foundation.h>

#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        
        // 获得 NSObject 类实例对象的成员变量所占用的大小
        NSLog(@"%zd", class_getInstanceSize([NSObject class]));
        
        // 获得 obj 指针指向内存的大小
        NSLog(@"%zd", malloc_size((__bridge const void *)obj));
        
    }
    return 0;
}


>>>> 打印结果
 8
16

1.2 class_getInstanceSizemalloc_size说明

extern size_t malloc_size(const void *ptr);
    /* Returns size of given ptr */
/** 
 * Returns the size of instances of a class.
 * 
 * @param cls A class object.
 * 
 * @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
 */
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
创建一个实例对象,至少需要多少内存?
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]); 

等价于 sizeof()获得的值。
//sizeof 获取类型大小,它是一个运算符并非函数,其在编译时既可以计算到给定类型的大小
创建一个实例对象,实际上分配了多少内存?
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);

1.3 通过源码解析class_getInstanceSize方法返回8个字节原因

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

因为 NSObject 对象中只有一个 isa 指针成员变量,而且 isa 的类型是一个指针。在64bit 设备下指针大小为8个字节

1.4 将 OC 转成 C/C++代码, 解析NSObject本质

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

方式一:简单转换

clang -rewrite-objc main.m -o main.cpp // 这种方式没有指定架构,如 arm64 架构生成 main.cpp

方式二:使用xcode工具 xcrun,指定架构模式

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

在生成的 main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPL(IMPL代表 implementation 实现)

struct NSObject_IMPL {
    Class isa;
};
// Class其实就是一个指针,类型如下
// typedef struct objc_class *Class;

1.5 为什么有8个字节又有16个字节的问题呢?

  1. 底层都是调用 callAlloc

    搜索 allocWithZone 获取对应信息
  2. class_createInstance方法创建 obj

    class_createInstance
  3. 找到分配内存函数instanceSize

  4. 原因: corefoundation 要求所有 objects 最少16 bytes


    最少16 bytes

1.6 简单继承的对象内存占用分析

#import <Foundation/Foundation.h>

#import <objc/runtime.h>
#import <malloc/malloc.h>


/* Person */
@interface Person : NSObject {
    int _age;
}
@end

@implementation Person
@end

/* Student */
@interface Student : Person {
    int _no;
}
@end

@implementation Student
@end



int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        NSLog(@"person - %zd", class_getInstanceSize([Person class]));
        NSLog(@"person - %zd", malloc_size((__bridge const void *)person));
        
        
        Student *stu = [[Student alloc] init];
        NSLog(@"stu - %zd", class_getInstanceSize([Student class]));
        NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu));
        
    }
    return 0;
}

>>>>打印结果
person - 16
person - 16
stu - 16
stu - 16

1.6.1 转成 C++ 后结构体成员分析

1.6.2 内存对齐

  1. 内存对齐:结构体的大小必须是最大成员大小的倍数
  2. Person 中的实际应该分配应该是12个字节,因为 isa 为8字节,int 类型的 _age 为4字节,为什么class_getInstanceSize返回的还是16字节呢?此时就要考虑内存对齐了。以最大成员大小,即8字节的 isa 的倍数算,最少就是8的2倍,16字节了。
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8
    int _age; // 4
}; // 16 内存对齐:结构体的大小必须是最大成员大小的倍数

1.6.3 优先利用空的连续的内存

  1. 上述中 Person 实例实际使用的内存是12字节,但是内存占用是16字节,那么多余的4个字节在 Student 实例创建时就需要被考虑使用了。
struct Student_IMPL {
    struct Person_IMPL Person_IVARS; // 16
    int _no; // 4
}; // 16,刚好 Person 分配的16字节中空余的4个字节可以放下 int 类型的 _no 成员变量

1.7 带@property的对象内存占用分析

// Person
@interface Person : NSObject
{
    int _age;
}
@property (nonatomic, assign) int height;
@end

@implementation Person
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        NSLog(@"person - %zd", class_getInstanceSize([Person class]));
        NSLog(@"person - %zd", malloc_size((__bridge const void *)person));

    }
    return 0;
}

>>>>打印结果
person - 16
person - 16
  1. @property 是作用是自动生成一个带下划线的实例变量,同时生成对应的getter 和 setter 方法
  2. 那么转换成 C++ 后代码如下
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8
    int _age; // 4
    int _height; // 4
};  // 16
  1. 也就如运行所得,占用内存为16个字节

1.8 为什么实例方法不在实例对象里呢?

  1. 实例方法是公用的,一份足以应付同一类型的多个实例对象。因为除了实例变量的值会变之外,方法的调用是不会变的。也就是 person1、person2、person3 它们调用 Person 类的方法都是一样的。

1.9 附加课程介绍的内存分析和修改内存的操作

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface Student : NSObject {
    @public
    int _no;
    int _age;
}
@end

@implementation Student
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
        stu->_no = 4;
        stu->_age = 5;
        
        NSLog(@"%zd", class_getInstanceSize([Student class]));
        NSLog(@"%zd", malloc_size((__bridge const void *)stu));
        
    }
    return 0;
}

C++ 代码

struct NSObject_IMPL {
    Class isa; // 8
};


struct Student_IMPL {
    Class isa;
    int _no;
    int _age;
};

1.9.1 实时查看内存数据

  1. 在 stu 生成后,打断点。
  2. 在 Xcode 的控制器查看 stu 实时地址
  3. 在Xcode 工具栏 选择 Debug -> Debug Workfllow -> View Memory (Shift + Command + M)然后在 address 中输入对象的地址
    View Memory
输入地址

从上图中,我们可以发现读取数据从高位数据开始读,查看前16位字节,每四个字节读出的数据为 16进制 0x00 00 00 04(4字节)、 0x00 00 00 05(4字节)、 isa的地址为 0x 00 D1 08 10 00 00 11 19(8字节)

1.9.2 LLDB 指令查看且修改内存值

1.9.2.1 在生成 stu 实例后,打断点,启动 LLDB。

1.9.2.2 通过 p 指令获得 stu 的地址

(lldb) p stu
(Student *) $0 = 0x000000010062cdd0

1.9.2.3 通过指令memory read读取对应的地址内存

(lldb) memory read 0x000000010062cdd0
0x10062cdd0: c9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00  ................
0x10062cde0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) 

指令memory read 可以简写成 x

(lldb) x 0x000000010062cdd0
0x10062cdd0: c9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00  ................
0x10062cde0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) 

1.9.2.4 增加读取条件

memory read/[数量][格式][字节数] 内存地址
简写:
x/[数量][格式][字节数] 内存地址

格式:x是16进制,f是浮点,d是10进制
字节大小:b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节

示例:x/4xw
/后面表示如何读取数据
w:表示4个4个字节读取
x:表示以16进制的方式读取数据
4:则表示读取4次

(lldb) memory read/4xw 0x000000010062cdd0
0x10062cdd0: 0x000011c9 0x001d8001 0x00000004 0x00000005

简写

(lldb) x/4xw 0x000000010062cdd0
0x10062cdd0: 0x000011c9 0x001d8001 0x00000004 0x00000005
(lldb) 

1.9.2.5 修改内存中的值

// 对象地址 +8个字节就是 _no 的地址
(lldb) memory write 0x000000010062cdd8 8
(lldb) p stu
(Student *) $0 = 0x0000000100600590
2018-07-08 11:08:51.663461+0800 Test1 [21943:3939579] no is 4, age is 5
(lldb) memory write 0x0000000100600598 8
2018-07-08 11:09:15.525190+0800 Test1[21943:3939579] -------------
2018-07-08 11:09:15.525299+0800 Test1[21943:3939579] no is 8, age is 5
Program ended with exit code: 0

1.10 OC对象内存分配对齐规则为16的倍数(最大是256)

#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
#import <objc/runtime.h>

// C++代码中对象的结构体表示
//struct NSObject_IMPL
//{
//    Class isa;
//};
//
//struct Person_IMPL
//{
//    struct NSObject_IMPL NSObject_IVARS; // 8
//    int _age; // 4
//    int _height; // 4
//    int _no; // 4
//}; // 24


@interface Person : NSObject {
    int _age;
    int _height;
    int _no;
}
@end
@implementation Person
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        
        NSLog(@"%zd", sizeof(struct Person_IMPL)); // 24
        
        NSLog(@"%zd %zd",
              class_getInstanceSize([Person class]), // 24
              malloc_size((__bridge const void *)(p))); // 32
    }
    return 0;
}

PS:更复杂的内存分配后续补充

2. 对象的isa指针指向哪里?

问题简答:

  1. instance对象的isa指针指向class对象
  2. class对象的isa指针指向meta-class对象
  3. meta-class对象的isa指针指向基类的meta-class对象
  4. 基类自己的isa指针也指向自己

问题理解方向如下:

2.1 OC对象的分类

2.2 instance对象在内存中存储的信息,主要包括

2.3 class对象在内存中存储的信息,主要包括

2.4 meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息,主要包括

2.5 instance、class、meta-class 存储区别

NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];

obj1 和 obj2 是NSObject的instance对象(实例对象),分别占用两块不同的内存
Class objClass1 = [obj1 class];
Class objClass2 = [obj2 class];
Class objClass3 = [NSObject class];
Class objClass4 = object_getClass(objClass1); //Runtime API
Class objClass5 = object_getClass(objClass2); //Runtime API

objClass1~5 都是NSObject 的 class 对象,它们都是同一个对象
Class objMetaClass = object_getClass([NSObject class]); // Runtime API

objMetaClass是NSObject的meta-class对象(元类对象)

2.6 isa 和 superclass 指向总结

isa 和 superclass 指向图,也是实例方法和类方法查找路线图
  1. instance的isa指向class

  2. class的isa指向meta-class

  3. meta-class的isa指向基类的meta-class

  4. class的superclass指向父类的class
    如果没有父类,superclass指针为nil

  5. meta-class的superclass指向父类的meta-class
    基类的meta-class的superclass指向基类的class

  6. instance调用对象方法的轨迹
    isa找到class,方法不存在,就通过superclass找父类

  7. class调用类方法的轨迹
    isa找meta-class,方法不存在,就通过superclass找父类

3. OC的类信息存放在哪里?

上一篇 下一篇

猜你喜欢

热点阅读