【MJ底层课】OC对象本质

606 阅读6分钟

先上一道面试题:

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

想要弄清这个问题,那么我们如果知道了一个NSObject对象在内存中是如何布局的,就知道了这个对象占用了多少内存。

想必小伙伴们都知道,OC作为C语言的超集,它的面向对象的能力是以C/C++来实现的。Xcode也是支持将OC转成C++代码的:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件

OC对象的底层实现

了解了如何将OC代码重写为C++代码,我们首先看一下在NSObject.h中是如何定义NSObjcet的呢?

// NSObject.h
@interface NSObject <NSObject> {
    Class isa;
}

从框架的头文件中可以看到,NSObject只有一个isa的成员变量。我们将如下代码重写为C++然后观察一下。

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
}

细心的话可以发现的这样一段代码:

struct NSObject_IMPL {
	Class isa;
};

从结构体的名称就可以看出,NSObject在底层被重写为一个结构体。只有一个成员变量isa。而NSObjcet作为OC中所有类的基类,我们可以得出一条结论:OC对象在底层是用结构体来实现的。

我们还可以追溯Class的定义:

typedef struct objc_class *Class;

所以Class类型,是一个指向objc_class结构体的指针。同时我们知道,一个指针在64位系统上占用8个字节,32位系统上占用4个字节。所以我们可以推断出,**只有一个成员变量的结构体NSObject_IMPL所占的内存大小就是isa所占的内存空间空间大小。**即64位环境下:一个NSObject对象占8个字节。

但实际上不是,实际上是16个字节。

通过API来获取对象大小

先介绍两个用来获取对象大小的函数

// 通过类型来获取该类型实例对象所占的内存大小
#import <objc/runtime.h>
size_t class_getInstanceSize(Class _Nullable cls);
// 通过一个指针来获取该指针指向内存的大小
#import <malloc/malloc.h>
size_t malloc_size(const void *ptr);

我们通过这两个函数来获取一下NSObject的实例对象所占内存的大小:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
    
        // >>8
        NSLog(@"%zd", class_getInstanceSize([NSObject class]));
        // >>16
        NSLog(@"%zd", malloc_size((__bridge const void *)obj));
    }
    return 0;
}

那么系统到底为这个obj对象分配了多少内存呢?我们从NSObjectalloc着手分析一下。

在源码中,我们可以看到alloc的实现:调用了_obj_rootAllic()

//NSObject.mm
+ (id)alloc {
    return _objc_rootAlloc(self);
}

继续搜索_obj_rootAllic(),其实现为:

//NSObject.mm
id _objc_rootAlloc(Class cls) {
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

继续搜索callAlloc函数,其实现为:

//NSObject.mm
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

搜索_objc_rootAllocWithZone的实现:

//NSObject.mm
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

搜索_class_creatInstanceFromZone的实现:

unsigned _class_createInstancesFromZone(Class cls, size_t extraBytes, void *zone, 
                               id *results, unsigned num_requested)
{
    unsigned num_allocated;
    if (!cls) return 0;

    size_t size = cls->instanceSize(extraBytes);

    num_allocated = 
        malloc_zone_batch_malloc((malloc_zone_t *)(zone ? zone : malloc_default_zone()), 
                                 size, (void**)results, num_requested);
    for (unsigned i = 0; i < num_allocated; i++) {
        bzero(results[i], size);
    }

    // Construct each object, and delete any that fail construction.

    unsigned shift = 0;
    bool ctor = cls->hasCxxCtor();
    for (unsigned i = 0; i < num_allocated; i++) {
        id obj = results[i];
        obj->initIsa(cls);    // fixme allow nonpointer
        if (ctor) {
            obj = object_cxxConstructFromClass(obj, cls,
                                               OBJECT_CONSTRUCT_FREE_ONFAILURE);
        }
        if (obj) {
            results[i-shift] = obj;
        } else {
            shift++;
        }
    }

    return num_allocated - shift;    
}

从这里我们可以看到,到_class_creatInstanceFromZone中:

size_t size = cls->instanceSize(extraBytes);,而instanceSize中:

size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

可以看到 如果size最小为16,所以NSObject的实例对象占用16个字节是源代码中影响规定的,而实际上,一个NSObject只用了8个字节(64位)。

整体的调用流程为:

alloc的调用流程.jpg

API结果的含义

根据我们前面一系列的探索,我们最终得知,一个NSObject对象系统分配了16个字节。

前面我们使用的两个函数,也应该知道了获取的真正含义:

  • malloc_size():创建一个实例对象,实际上分配了多少内存。
  • class_getInstanceSize():创建一个实例对象,至少需要多少内存(结果等同于sizeof()运算符)

sizeof()的注意点:

sizeof是一个运算符,并不是一个函数。传入一个类型,返回该类型的所占用的内存。在编译的时候就编译为常数。

假如传入一个指针连变量,则返回这个指针变量所占的内存大小。至于这个指针指向内存的大小,则需要malloc_size()

NSObject的子类

通过前边我们了解了NSObject的底层实现,以及实例对象所占内存的大小。那么对于NSObject的子类情况又是如何呢?

我们先自定义一个Student

@interface Student : NSObject {
    @public
    int _no;
    int _age;
}
@end
@implementation Student
@end
  
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;

我们将其重写为C++,可以看到Student在底层的实现为:

struct Student_IMPL {
	struct NSObject_IMPL NSObject_IVARS;	//父类的结构体
	int _no;
	int _age;
};

可以看到底层的结构体,第一个成员为父类的结构体,其余依次为自身成员属性。

在代码中显示声明结构体Student_IMPL,也可以将Student对象转换为Student_IMPL结构体:

Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
        
struct Student_IMPL *stu2 = (__bridge  struct Student_IMPL *)stu;//OC转C,需要使用__bridge
NSLog(@"%d,%d",stu2->_no,stu2->_age);//4,5

那么Student的实例对象占用多少内存呢?

我们打一个断点,来获取代码中stu的内存地址:0x100713890。通过Xcode Debug -> Debug Workflow -> View Memory来查看实时内存。我们大概的可以推断出Student也占16个字节。

WX20201221-231232.png

Student在底层转换实现的结构体可以看出,Student_IMPL结构体一共有三个成员变量,其中NSObject_IVARS分配了16个字节,但是只是用了8个字节,还有八个字节,正好可以存放两个int型变量。

WX20201221-232722.png

// >>16
NSLog(@"%zd",malloc_size((__bridge const void *)stu));
// >>16
NSLog(@"%zd",class_getInstanceSize([Student class]));

再多一个成员

当然如果Student的再多一个int成员变量:

@interface Student : NSObject
{
    @public
    int _no;
    int _age;
    int _height;
}
@end
@implementation Student
@end
  
Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
stu->_height = 6;

// >>32
NSLog(@"%zd",malloc_size((__bridge const void *)stu));
// >>24
NSLog(@"%zd",class_getInstanceSize([Student class]));
// >> 24
NSLog(@"%zd",sizeof(struct Student_IMPL));

我们可以看到系统分配的空间为32个字节,实例所占的内存空间而是24个字节。这就涉及到了iOS系统底部内存优化和内存对齐的问题。通过跟踪alloc函数,程序最终调用了:

obj = (id)calloc(1, size); // size即24

可以看出,这个对象至少需要24个字节。当向系统申请24个字节内存的时候,系统分配了32个字节。通过在libmalloc开源代码中,了解到iOS系统分配内存都是16的倍数(推测是加快内存分配速度和访问速度)。所以系统向其分配了32个字节。

更为复杂的场景

现在我们假设有这样的场景:

现在有两个类: StudentPerson。其中他们的继承关系为:Student -> Person -> NSObject

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

那么Person实例对象,Student的实例对象分别占用多少内存呢?。答案都是16个字节。

我们知道,PersonStudent对象在底层都是这样实现的:

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS; //8个字节
	int _age; //4个字节
}; //16个字节:结构体的大小必须是最大成员大小的倍数(内存对齐)。

struct Student_IMPL {
	struct Person_IMPL Person_IVARS;//16个字节,但是空余4个字节
	int _no; //4个字节,因为前边正好有4个空余的字节,正好用来存放_no。
};

我们来证实一下:

Person *p = [[Person alloc] init];
// >>16,这里从上面的分析来看应该返回12,但是这个函数是返回对齐后的内存大小。
NSLog(@"Person:%zd",class_getInstanceSize([Person class]));
// >>16
NSLog(@"p:%zd",malloc_size((__bridge  const void *)p));
        
Student *stu = [[Student alloc] init];
// >>16
NSLog(@"Student:%zd",class_getInstanceSize([Student class]));
// >>16
NSLog(@"stu:%zd",malloc_size((__bridge  const void *)stu));

@property 成员

对于@property修饰的成员变量,编译器自动生成成员变量:_ + 变量名;并且生成setter&getter方法。

OC实现:

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

底层实现:

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
	int _height;
};
static int _I_Person_height(Person * self, SEL _cmd) { 
  return (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)); 
}
static void _I_Person_setHeight_(Person * self, SEL _cmd, int height) { 
  (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)) = height; 
}