Let’s start with an interview question:

How much memory does an NSObject take up?

So to figure that out, if we know how an NSObject object is laid out in memory, we know how much memory that object takes up.

As you all know, OC, as a superset of C language, realizes its object-oriented capability by C/C++. Xcode also supports converting OC into C++ code:

Xcrun -sdk iphoneOS clang-arch arm64 -rewrite-objc OC source file -o Output CPP fileCopy the code

The underlying implementation of OC objects

Now that we know how to rewrite OC code as C++ code, let’s first look at how NSObjcet is defined in nsobjject. H.

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

As you can see from the framework header file, NSObject has only one isa member variable. Let’s rewrite the following code into C++ and observe.

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

If you are careful, you can find this code:

struct NSObject_IMPL {
	Class isa;
};
Copy the code

As you can see from the structure name, NSObject is rewritten as a structure underneath. There is only one member variable isa. As NSObjcet is the base class of all classes in OC, we can draw a conclusion that OC objects are implemented with structures at the bottom level.

We can also trace back to the definition of Class:

typedef struct objc_class *Class;
Copy the code

So the Class type is a pointer to the objc_class structure. We also know that a pointer takes up 8 bytes on a 64-bit system and 4 bytes on a 32-bit system. Therefore, we can infer that the size of the memory used by the NSObject_IMPL structure with only one member variable is the size of the memory used by isa. ** in 64-bit environments: an NSObject takes 8 bytes.

But it’s not, it’s actually 16 bytes.

Get the object size through the API

I’ll start with two functions to get the size of an object

// Get the memory size of the instance object by type
#import <objc/runtime.h>
size_t class_getInstanceSize(Class _Nullable cls);
Copy the code
// Use a pointer to get the size of the memory that the pointer points to
#import <malloc/malloc.h>
size_t malloc_size(const void *ptr);
Copy the code

We use these two functions to get the memory size of an instance of 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;
}
Copy the code

So how much memory does the system allocate for this OBJ object? So let’s start with NSObject alloc.

In the source code, we can see the implementation of alloc: _obj_rootAllic() is called,

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

Continue searching for _obj_rootAllic(), which is implemented as follows:

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

Continue searching for the callAlloc function, which is implemented as follows:

//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));
}

Copy the code

Search the implementation of _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);
}
Copy the code

An implementation of searching _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;    
}
Copy the code

From here we can see that in _class_creatInstanceFromZone:

size_t size = cls->instanceSize(extraBytes); And 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;
    }
Copy the code

You can see that if size is at least 16, an instance object of NSObject takes 16 bytes, which is what the source code dictates. In fact, an instance of NSObject takes only 8 bytes (64-bit).

The overall call process is as follows:

Meaning of API results

Based on our previous series of explorations, we finally learned that an NSObject object system allocates 16 bytes.

The two functions we used in the previous section should also know what the fetch really means:

  • malloc_size(): How much memory is actually allocated to create an instance object.
  • class_getInstanceSize(): How much memory is required to create an instance objectsizeof()The operator)

Sizeof ()

Sizeof is an operator, not a function. Pass in a type and return the memory used for that type. It is compiled as a constant at compile time.

If a pointer variable is passed, return the size of memory occupied by the pointer variable. As for the size of memory this pointer points to, malloc_size() is required.

A subclass of NSObject

We learned about the underlying implementation of NSObject and how much memory the instance objects take up. What about subclasses of NSObject?

Let’s start by defining a Student class

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

We rewrite this as C++, and we can see that the underlying implementation of Student is:

struct Student_IMPL {
	struct NSObject_IMPL NSObject_IVARS;	// The parent structure
	int _no;
	int _age;
};
Copy the code

You can see the underlying structure, with the first member being the parent structure, followed by its own member attributes.

Student_IMPL = Student_IMPL; Student_IMPL = Student; Student_IMPL = Student_IMPL;

Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
        
struct Student_IMPL *stu2 = (__bridge  struct Student_IMPL *)stu;// from OC to C, use __bridge
NSLog(@"%d,%d",stu2->_no,stu2->_age);/ / 4, 5
Copy the code

thenStudentHow much memory does the instance object of?

We break a breakpoint to get the memory address of the STU in the code: 0x100713890. Xcode Debug -> Debug Workflow -> View Memory to View live Memory. We can probably deduce that Student is also 16 bytes.

Student_IMPL: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS: NSObject_IVARS

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

One more member

Of course if Student has one more int member variable:

@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));
Copy the code

We can see that the space allocated by the system is 32 bytes, while the memory occupied by the instance is 24 bytes. This comes down to memory optimization and memory alignment at the bottom of iOS. By tracing the alloc function, the program ends up calling:

obj = (id)calloc(1, size); / / the size is 24
Copy the code

As you can see, this object requires at least 24 bytes. When 24 bytes of memory are requested from the system, 32 bytes are allocated. Through thelibmallocIn the open source code, it is learned that the iOS system allocates memory in multiples of 16 (presumably to speed up memory allocation and access speed). So the system allocates 32 bytes to it.

More complex scenarios

Now let’s assume the following scenario:

There are now two classes: Student and Person. Their inheritance is Student -> Person -> NSObject.

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

So how much memory does the Person instance object and the Student instance object take up? . The answer is always 16 bytes.

We know that both the Person and Student objects are implemented at the bottom level like this:

struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS; / / 8 bytes
	int _age; / / 4 bytes
}; //16 bytes: The size of the structure must be a multiple of the maximum member size (memory alignment).

struct Student_IMPL {
	struct Person_IMPL Person_IVARS;//16 bytes, but 4 bytes free
	int _no; //4 bytes, because there are 4 bytes in front of it, just right for _no.
};
Copy the code

Let’s verify:

Person *p = [[Person alloc] init];
// >>16, which should return 12 from the above analysis, but this function returns the aligned memory size.
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));
Copy the code

@ property members

For @property modified member variables, the compiler automatically generates member variables: _ + variable names; And generate setter&getter methods.

OC implementation:

@interface Person : NSObject
{
    @public
    int _age;
}
@property (nonatomic.assign) int height;
@end
Copy the code

Underlying implementation:

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; 
}
Copy the code