The nature of OC objects

Normally we use Objective-C syntax to write code, but it’s really all C or C++ code underneath. Objective-c is the addition of object-oriented features to C. We can convert Objective-C code to C++ code with the following command:

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

If the OC file needs to be linked to another framework, use the -framework argument:

Xcrun -sdk iphoneOS clang-arch arm64 -rewrite-objc OC file -o Output target CPP file -framework Name of the frameworkCopy the code

At the same time, you need to download the runtime source code. Download the objC source code for the latest version of objC for subsequent use.

The underlying implementation of OC objects

OC objects are most commonly used during development. Almost all class objects are subclasses of NSObject, but how does NSObject work underneath OC? As mentioned above, all OC code is eventually converted to C code, so let’s take a look at the underlying implementation of NSObject through an example.

  • First, create an XLPerson object
@interface XLPerson : NSObject

@end

@implementation XLPerson


@end
Copy the code
  • Go to the xlPerson. m file directory and use the following command to convert the xlPerson. m file to xlperson_cpp.cpp
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XLPerson.m -o XLPerson_cpp.cpp
Copy the code
  • Search for XLPerson in the generated xlperson_cpp.cpp file to find the following structure definitions
struct XLPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};
Copy the code

XLPerson_IMPL contains a structure member, NSObject_IVARS, which is of type NSObject_IMPL. The code for NSObject_IMPL is as follows:

struct NSObject_IMPL {
	Class isa;
};
Copy the code

As you can see, objects in OC are implemented through structures. The NSObject_IMPL contains a member of Class type ISA. Continue to look at the Class definition:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
Copy the code

Class is a structure pointer of type objC_class. Objc-runtimenew. h objc-runtimenew. h objc runtimenew. h objC runtimenew. h objC runtimenew. h

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        returnbits.data(); }}Copy the code

Objc_class inherits from objc_Object, which is defined as follows and has only one isa pointer inside

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
Copy the code

Objc_class inherits the ISA pointer to objc_object, so objc_class can be converted as follows:

struct objc_class {
    Class isa;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        returnbits.data(); }}Copy the code
  • Isa is an inherited property from objc_Object (more on this later)
  • Superclass represents the parent class of the current class
  • Cache stands for method cache.
  • Bits is an attribute of type class_datA_bits_t and is used to store class information.

The implementation of class_datA_bits_t is as follows:

Struct class_data_bits_t {...... class_rw_t*data() {
        return(class_rw_t *)(bits & FAST_DATA_MASK); }... }Copy the code

The bits function data() can be used to retrieve class_rw_t data.

struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; // Read-only property ro method_array_t methods; // Method list property_array_t properties; // Protocol_array_t protocols; // Protocol list Class firstSubclass; Class nextSiblingClass; }Copy the code

It’s stored in the class_rw_t structure

  • List of methods
  • Property list properties
  • Protocol list Protocols.
  • A read-only variable ro of type class_ro_t

Class_ro_t class_ro_t

struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; // Uint8_t * ivarLayout; const char * name; // Class name method_list_t * baseMethodList; // List of basic methods protocol_list_t * baseProtocols; // List of base protocols const ivar_list_t * ivars; // List of member variables const uint8_t * weakIvarLayout; property_list_t *baseProperties; // Basic attribute list}Copy the code

The difference between class_rw_t and class_ro_t is that class_ro_t contains the original method list, property list, and so on, which are generated at compile time and are read-only and cannot be modified at run time. Class_rw_t contains not only compiler-generated method lists and attribute lists, but also dynamically generated methods and attributes at runtime. It can be read and written. The deeper differences between class_rw_t and class_ro_t will be explained when I introduce the Runtime.

Memory allocation of OC objects

allocWithZone

[[NSObject alloc] init], where [NSObject alloc] allocates memory for NSObject.

  • [NSObject alloc] actually calls the allocWithZone method to allocate memory space, so look at the objc source nsobject.mm file. Find the _objc_rootAllocWithZone function:
id 
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
    id obj;

#if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
#else
    if(! zone) { obj = class_createInstance(cls, 0); }else {
        obj = class_createInstanceFromZone(cls, 0, zone);
    }
#endif

    if(slowpath(! obj)) obj = callBadAllocHandler(cls);return obj;
}
Copy the code
  • Class_createInstance (CLS, 0);
id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}
Copy the code
  • Find the _class_createInstanceFromZone(CLS, extraBytes, nil) method, because the method is long, only the core code is shown here:
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    ......
    
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if(! zone && fast) { obj = (id)calloc(1, size);if(! obj)returnnil; obj->initInstanceIsa(cls, hasCxxDtor); }... }Copy the code

Calloc takes two parameters: the first parameter indicates the number of objects, and the second parameter size indicates the number of bytes of memory the object occupies. Size therefore represents the amount of memory required by the current object.

  • CLS ->instanceSize(extraBytes);
    // May be unaligned depending on class's ivars. uint32_t unalignedInstanceSize() { assert(isRealized()); return data()->ro->instanceSize; } // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

    size_t instanceSize(size_t 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

CLS ->unalignedInstanceSize() indicates the size of the unaligned memory, and CLS ->alignedInstanceSize() indicates the size of the unaligned memory.

One detail here is that if the size of memory obtained by the alignment operation is less than 16 bytes, then the final allocated memory size is 16 bytes, that is, when we create the object, the allocated memory is at least 16 bytes.

Memory allocation of OC objects

How to get the size of memory?

In iOS, there are three ways to get the memory size of an object.

sizeof

Sizeof, which is not really a function but an operator that, like a macro definition, converts the type passed in to a specific memory size at compile time. For example, if int is 4 bytes, sizeof(int) is replaced by 4 at compile time

Note: sizeof requires a type to be passed in, which returns the sizeof the memory used by the type

class_getInstanceSize

Class_getInstanceSize (Class _Nullable CLS), pass in an object of Class type to get the memory size of the current Class. For example, class_getInstanceSize([NSObject class]) returns 8, which means that NSObject takes up 8 bytes in memory, and since NSObject_IMPL is converted to NSObject_IMPL, The isa pointer takes up 8 bytes of storage space.

The alignedInstanceSize function is called to get the actual memory size of the object.

size_t class_getInstanceSize(Class cls)
{
    if(! cls)return 0;
    return cls->alignedInstanceSize();
}
Copy the code

AlignedInstanceSize = alignedInstanceSize = alignedInstanceSize = calloc;

id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    ......
    size_t size = class_getInstanceSize(cls);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if(! zone && fast) { obj = (id)calloc(1, size);if(! obj)returnnil; obj->initInstanceIsa(cls, hasCxxDtor); }... }Copy the code

Class_getInstanceSize (Class _Nullable CLS) returns the actual memory size required by the object.

malloc_size

The malloc_size(const void * PTR) function returns the size of memory allocated by the operating system by passing in a const void * parameter. Such as: Malloc_size ((__bridge const void *)([[NSObject alloc] init]))), taking an instance of NSObject as an argument, resulting in 16, This is different from the 8 we got using class_getInstanceSize([NSObject class]).

This is because in iOS, if an object needs less than 16 bytes of memory when allocating memory, the object is allocated 16 bytes of memory. That is, at least 16 bytes of memory per object

size_t instanceSize(size_t 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

Comparison of three methods of obtaining memory

  • First, create the object XLPerson and add three attributes, as follows
@interface XLPerson : NSObject{
    int _height;
    int _age;
    long _num;
}
Copy the code
  • Create an XLPerson instance object in the main function and get the memory size of the XLPerson class using the above three methods:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface XLPerson : NSObject{
    @public
    int _height;
    int _age;
    long _num;
}

@end

@implementation XLPerson

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        XLPerson *p = [[XLPerson alloc] init];
        p->_height = 10;
        p->_age = 20;
        p->_num = 25;
        
        NSLog(@"sizeof --> %lu", sizeof(p));
        
        NSLog(@"class_getInstanceSize --> %lu", class_getInstanceSize([XLPerson class]));
        
        NSLog(@"malloc_size --> %lu", malloc_size((__bridge const void *)(p)));
    }
    return 0;
}
Copy the code
  • Run the program and print the following result:

You can see that sizeof(p) returns 8 bytes, class_getInstanceSize returns 24 bytes, and malloc_size returns 32 bytes. All three methods return different sizes of memory. Why?

Why does sizeof(p) only return 8 bytes?

Sizeof (p) returns 8 bytes, which makes sense because sizeof is passed in p, which in this case represents a pointer to an XLPerson instance object. In iOS, pointer types take up 8 bytes of memory. So sizeof(p) does not return the memory sizeof the XLPerson object.

To get the memory sizeof an XLPerson object using sizeof, you need to know what type the XLPerson will eventually be converted to. From what we learned above, XLPerson is actually a structure inside that converts a file to a.cpp file using the XCRUN directive

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

Analysis of the main.cpp file shows that XLPerson will eventually be converted to the following structure type

struct NSObject_IMPL {
    Class isa;
};

struct XLPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _height;
    int _age;
    long _num;
};
Copy the code

If you call sizeof(struct XLPerson_IMPL), the sizeof the XLPerson_IMPL type is 24 bytes.

The sizeof(struct XLPerson_IMPL) operator and the class_getInstanceSize([XLPerson class] function return the actual sizeof the object.

Why does malloc_size return a different amount of memory than the object actually needs?

Before we look at the malloc_size function, let’s examine the actual memory size required by the XLPerson internal structure.

struct XLPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _height;
    int _age;
    long _num;
};
Copy the code
  • First we know that NSObject_IMPL actually has only one ISA pointer inside, so it takes up 8 bytes of memory
  • The _height variable of type int takes 4 bytes
  • The variable _age of type int takes up 4 bytes
  • The variable _num of type long takes up 8 bytes

So, looking purely at the structure level, we can see that the XLPerson_IMPL structure requires 24 bytes of memory, This is the same size as sizeof(struct XLPerson_IMPL) and class_getInstanceSize([XLPerson class]). As you can see, the memory required for XLPerson is 24 bytes.

Why does malloc_size return 32 bytes of memory? This brings us to memory alignment operations.

The memory alignment operation of the structure

First, we will modify the XLPerson attribute mentioned above to remove the _age attribute

@interface XLPerson : NSObject{
    @public
    int _height;
    long _num;
}
@end
Copy the code

Then re-run the project, and you can see that XLPerson actually takes up 24 bytes of memory, whereas analysis shows that XLPerson only needs 20 bytes of memory.

This is the result of the alignedInstanceSize function. So what is a structure’s memory alignment operation?

Structure is not like arrays, can hold different types of data structures, it is not the size of the simple sum of the size of each data member, limited to read the memory requirements, but each member in memory storage should be according to certain offset to store, depending on the type, each member should according to certain number of aligned storage, Finally, the size of the entire structure should be aligned according to a certain number of alignment.

The memory alignment rules for the structure are as follows:

  • The first member starts at address 0
  • The first address of each member is an integer multiple of its own size
  • The total memory size of a structure is an integer multiple of the largest type contained in its members

This is why XLPerson has a memory size of 24 bytes.

Memory alignment operation on iOS

Since XLPerson has a memory footprint of 24 bytes, why does the system allocate 32 bytes to it? In fact, memory alignment also exists in iOS.

We can print memory information to see if 32 bytes have been allocated, again using the example above

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        XLPerson *p = [[XLPerson alloc] init];
        p->_height = 1;
        p->_num = 3;
    }
    return 0;
}
Copy the code
  • In the breakpoint state, use Po p to obtain the memory address 0x1005b4fd0 of P
(lldb) po p
<XLPerson: 0x1005b4fd0>
Copy the code
  • In Xcode, open Debug Workflow->View Memory and enter the Memory address to obtain the Memory allocation of object P

The first eight bytes in the blue box hold the isa pointer, the four bytes in the blue box hold the _height=1, and the eight bytes in the green box hold the _num=3 memory address. Malloc_size ((__bridge const void *)(p))) returns the memory allocated to the p object.

OC Object classification

OC objects fall into three main categories

  • Instance Instance object
  • Class class object
  • Mata-class metaclass object

The instance objects

An instance object is an object created by the alloc operation. Each call to the alloc operation creates a different instance object, each of which has its own memory allocation. For example, the XLPerson instance object used above

XLPerson *p1 = [[XLPerson alloc] init];
XLPerson *p2 = [[XLPerson alloc] init];
Copy the code

P1 and P2 are instance objects. You can have multiple instance objects of the same class in memory at the same time. Each of them has a memory space for storing unique information. The internal contents of the instance object are as follows (using the instance object of XLPerson as an example) :

XLPerson *p1 = [[XLPerson alloc] init];
p->_height = 10;
p->_num = 25;
Copy the code
  • Isa pointer to its class object
  • _height = 10
  • _num = 25

Because an instance object can be created with [XLPerson alloc], each instance object holds an ISA pointer to its class object and the specific values of other member variables that have been defined.

Class class object

Class objects are abstracted from objects with similar properties and methods to form class objects. It can define some similar methods and attributes, and different instance objects reference the attributes or methods of class objects, which can reduce the rate of code repetition.

Run the following code:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XLPerson *p1 = [[XLPerson alloc] init];
        XLPerson *p2 = [[XLPerson alloc] init];
        
        Class c1 = [XLPerson class];
        Class c2 = [p1 class];
        Class c3 = [p2 class];
        Class c4 = object_getClass(p1);
        Class c5 = object_getClass(p2);
        
        NSLog(@"\n c1 -> %p,\n c2 -> %p,\n c3 -> %p,\n c4 -> %p,\n c5 -> %p", c1, c2, c3, c4, c5);
    }
    return 0;
}
Copy the code

The result can be:

The result shows that all class objects have the same memory address, which means that there is only one class object in memory, and the class object is the same regardless of which method is used.

An object_class object contains an object_class structure. The structure is defined in the previous section.

  • Isa pointer
  • superClass
  • Properties, which stores the name of the property, the type of the property, etc., only need to store one copy of these information in memory
  • Object Method Information (Methods)
  • Protocols
  • Member variable information and so on (IVars)

Mata-class metaclass object

A metaclass is also an object of class type. Its internal structure is the same as that of a class object, but only the following information is stored in a metaclass:

  • Isa pointer
  • superClass
  • Class Method information

Metaclasses, like classes, have only one metaclass object in memory. You can obtain metaclass objects through the Runtime methods

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XLPerson *p1 = [[XLPerson alloc] init];
        XLPerson *p2 = [[XLPerson alloc] init];
        
        Class c1 = object_getClass(p1);
        Class c2 = object_getClass(p2);
        
        Class mataC1 = object_getClass(c1);
        Class mataC2 = object_getClass(c2);
        
        BOOL c1_isMataClass = class_isMetaClass(c1);
        BOOL c2_isMataClass = class_isMetaClass(c2);
        BOOL mataC1_isMataClass = class_isMetaClass(mataC1);
        BOOL mataC2_isMataClass = class_isMetaClass(mataC2);
        NSLog(@"\n c1_isMataClass:%d,\n c2_isMataClass:%d,\n mataC1_isMataClass:%d,\n mataC2_isMataClass:%d"
              ,c1_isMataClass, c2_isMataClass, mataC1_isMataClass, mataC2_isMataClass);
        NSLog(@"\n c1 -> %p,\n c2 -> %p,\n mataC1 -> %p,\n mataC2 -> %p",
              c1, c2, mataC1, mataC2);
    }
    return 0;
}
Copy the code

The result is as follows:

In the figure above, c1 and c2 are both class objects, so return 0. MataC1 and mataC2 are metaclass objects, so return 1. MataC1 and mataC2 have exactly the same memory address, which indicates that there is really only one metaclass in memory.

The relationship between instance objects, class objects, and mata-class objects

As mentioned above, there is always an ISA pointer inside a Class object. What does this ISA pointer do? The isa pointer isa bridge between instance, class, and mata-class.

The role of isa Pointers

  • The isa pointer to an instance refers to a class object, and as mentioned above, only the ISA pointer and the specific property values are stored in the instance object. When we call the instance method of an instance, we actually find the class object through the ISA pointer. Find the implementation of the method in the class object’s methods list, and call.
  • The isa pointer to the class object points to the mata-class object. When a class method is called, the metaclass mata-class object is found through the isa pointer to the class object. The implementation of the corresponding class method is then found in the list of mata-class methods and executed.

The action of the superClass pointer

SuperClass refers to the parent of a class object or a mata-class object.

@interface XLPerson : NSObject - (void)run; + (void)sleep; @end int main(int argc, const char * argv[]) { @autoreleasepool { XLPerson *p1 = [[XLPerson alloc] init]; [p1 run]; [XLPerson sleep]; }}Copy the code

The role of the superClass pointer in the class object

XLPerson inherits from NSObject and declares a class method, sleep(), and an object method, run(), when P1 calls the object method run()

  • The class object of instance object P1 is first found through the isa pointer inside it. Look for the run() method in the list of methods on the class object.
  • If the class object does not have an implementation of the method in its list of methods, then the superClass pointer to the class object finds its superClass object, in this case the NSObject object, and looks for the implementation of the run() method in the NSObject object’s list of methods, and calls it.

The role of the superClass pointer in a mata-class object

To illustrate this, XLPerson calls the class method sleep()

  • First find XLPerson’s metaclass object through its ISA pointer, and look for the sleep() method in the metaclass’s list of methods.
  • If the sleep() method is not found in XLPerson’s metaclass object, then the superClass pointer to XLPerson’s metaclass object will find the metaclass object of XLPerson’s superClass object, in this case NSObject, Find the sleep() method in the list of methods on the NSObject metaclass object and execute it.

Isa, superClass summary

Let’s start with a classic picture of the relationship between instance objects, class objects, and metaclass objects. The dotted line represents the ISA pointer and the solid line represents the superClass pointer.

  • The ISA pointer to an instance object points to its class object
  • The ISA pointer to a class object points to its Mata-class object
  • The ISA of the mata-class object points to the base class object mata-class
  • The superClass of a class object points to the class object of its parent, and the superClass of a class object of a base class points to nil
  • The superClass of the mata-class refers to the mata-class of the parent object, and the superClass of the base object refers to the base object itself.
  • Instance method to find the route
    • The first thing I do is look in the class object through the ISA pointer
    • If not, use superClass to look for the superClass object
    • If you can’t find it, look in the base class object
  • Class method lookup route
    • The isa pointer to the class object is first looked up in mata-class
    • If not found in mata-class, use superClass to look for the superClass metaclass object
    • If it cannot be found in the metaclass object of the parent class, look for it in the metaclass object of the base class object
    • If it is not found in the metaclass object of the base class, it is looked for in the base class object.

OC interview questions

How much memory does an NSObject take up?

An NSObject is allocated 16 bytes of memory (via malloc_size), but NSObject has only one ISA pointer inside it, so it actually uses 8 bytes of memory, and due to ios memory alignment rules, The system allocates at least 16 bytes of memory space.

You can get the memory usage of NSObject by using the class_getInstanceSize function

Where does the object’s ISA point to? What does it do?

  • The ISA pointer to the instance object points to the class object
  • The ISA pointer to a class object points to a mata-class object
  • The ISA pointer to a meta-class object points to a meta-class object of the base class

Where is OC’s class information (methods, attributes, member variables, and so on) stored?

  • OC instance object methods, attributes, member variables, protocol information, and so on are stored in the class object
  • Class methods are stored in mata-class
  • The specific value of a member variable is stored in the instance object. Because the description of a member variable, such as its type is int, and so on, only one copy is stored in memory, the attribute description is stored in the class object. However, the value of the member variable is different from that of each instance variable, so each instance object stores one copy

conclusion

The above content is purely personal understanding, if there is anything wrong, welcome to comment.

Learn together and make progress together