iOS Deep Dive: Exploring the Internals of Classes and Objects

The Essence of Classes and Objects

When we create a MyClass in Objective-C and instantiate it in main, what does Objective-C actually look like underneath?

First, we need to understand the underlying structure of Objective-C objects. Objective-C code is essentially converted to C/C++ code. Use the following command to convert main.m to C++:

clang -rewrite-objc main.m

Opening the generated C++ file, we see that MyClass is actually defined as:

#ifndef _REWRITER_typedef_MyClass
#define _REWRITER_typedef_MyClass
typedef struct objc_object MyClass;
typedef struct {} _objc_exc_MyClass;
#endif

struct MyClass_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};

From this we observe:

  • The Objective-C class MyClass becomes a struct MyClass_IMPL.
  • The inheritance @interface MyClass : NSObject is reflected as typedef struct objc_object MyClass;.
  • Every Objective-C method has two hidden parameters: self (a pointer to the receiver) and _cmd (the selector). That's why we can use self inside methods, though these parameters are not explicitly visible.
  • In getters and setters, values are accessed and stored by offsetting the base pointer with the member variable's offset, and then casting back to the appropriate type (e.g., (NSString **)). The process is: get the address of the member variable, then read the value stored at that address.

The implementation of struct NSObject_IMPL is found by searching the code:

#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif

struct NSObject_IMPL {
    Class isa;
};

This struct contains only a Class isa field.

Further checking Class:

typedef struct objc_class *Class;

Class is just a pointer to a objc_class struct. We'll discuss objc_class in the class structure section.

Compiling reveals:

  • The class TestPerson compiles to TestPerson_IMPL struct.
  • The function _I_TestPerson_name is the getter, with default parameters self and _cmd.
  • The function _I_TestPerson_setName_ is the setter, with self, _cmd, and a parameter name.

From the compiled structure, we conclude:

  • An object is essentially an objc_object struct.
  • The difference between a property and an instance variable: a property consists of an instance variable, a getter, and a setter.

Structure of an Instance Object

An instance object is a struct containing an isa pointer pointing to its class and the instance variables.

Structure of a Class

Now let's look at objc_class. Searching the objc4 source code:

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // method cache
    class_data_bits_t bits;    // used to retrieve class_rw_t and flags
};

Here we see that a class is a struct containing superclass, cache, and bits. objc_class inherits from objc_object, meaning a class is itself an object. The isa pointer is inherited from objc_object, hence the comment // Class ISA;.

Further searching objc_object:

struct objc_object {
private:
    isa_t isa;
public:
//...
};

We find an isa_t type property isa.

isa

Looking at isa_t:

#include "isa.h"

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

#if ISA_HAS_INLINE_RC
    bool isDeallocating() const {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif // ISA_HAS_INLINE_RC

#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated) const;
    Class getDecodedClass(bool authenticated) const;
};

And the bitfield definition:

# if  __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
    // armv7k or arm64_32

#   define ISA_BITFIELD                         \
      uintptr_t nonpointer        : 1;          \
      uintptr_t has_assoc         : 1;          \
      uintptr_t indexcls          : 15;         \
      uintptr_t magic             : 4;          \
      uintptr_t has_cxx_dtor      : 1;          \
      uintptr_t weakly_referenced : 1;          \
      uintptr_t unused            : 1;          \
      uintptr_t has_sidetable_rc  : 1;          \
      uintptr_t extra_rc          : 7
#endif

In this definition, isa_t is a union with:

  • bits: an unsigned integer representing the entire isa value.
  • cls: a class pointer.
  • A bitfield structure containing multiple fields, each representing part of the isa pointer.

What is isa?

The isa pointer holds the memory address of the class object. Since a class object is unique globally, every object created from that class has an isa property storing the class object's address. Through isa, runtime can query the object's properties, methods, protocols, etc.

The Role of isa

  • Class pointer: The main function is to point to the object's class. This allows the runtime to find the class definition and call appropriate methods.
  • Flags: In modern Objective-C runtime, isa also contains extra flags to store metadata. For example, nonpointer indicates whether isa includes extra metadata, has_assoc indicates whether the object has associated objects, extra_rc stores the reference count, etc.

Before arm64, isa was just a pointer holding the memory address. After arm64, Apple optimized isa into a union structure using bitfields to store more information.

Note: Nowadays, isa is a struct that stores the class address. Its no longer a simple pointer; saying "isa points to" is not strictly accurate, but it helps understanding.

isa Initialization Flow

isa initialization flow

bits

In objc_class, we have:

class_data_bits_t bits;

bits is of type class_data_bits_t, whose source code:

public:
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        ASSERT(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }

    const class_ro_t *safe_ro() const {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            return maybe_rw->ro();
        } else {
            return (class_ro_t *)maybe_rw;
        }
    }

This code handles class object data with three main methods:

  • data(): Returns the class data by masking bits with FAST_DATA_MASK.
  • setData(class_rw_t *newData): Sets the class data after validation, using atomic operation.
  • safe_ro(): Safely returns read-only data. If realized, it returns the ro() from class_rw_t; otherwise, it returns the data directly as class_ro_t*.

The important types are class_rw_t and class_ro_t.

class_rw_t

This struct stores read-write data for the class, including instance method lists, protocol lists, property lists, etc. When you add methods or protocols to a class dynamically via runtime, these changes are stored in class_rw_t.

class_rw_t structure

class_ro_t

This struct stores read-only data for the class, such as the class name, superclass, instance size, etc. This information is determined at class creation and cannot be changed at runtime, so it resides in class_ro_t.

class_ro_t structure

cache_t

cache_t is a struct that stores the method cache for the class. Each class object has a cache_t member that caches recently used methods. When a message is sent to an object, the runtime first checks this cache. If found, the cached method is called directly, improving performance. If not found, the runtime searches the class's method list and adds the found method to the cache.

Class Inheritance

Suppose we have three classes: BaseClass, Sub1Class, Sub2Class; where Sub2Class inherits from Sub1Class, and Sub1Class inherits from BaseClass.

@interface BaseClass : NSObject
@end

@interface Sub1Class : BaseClass
@property (nonatomic, assign) int age;
@end

@interface Sub2Class : Sub1Class
@property (nonatomic, strong) NSString *name;
@end

Using clang -rewrite-objc main.m to convert to C++:

struct NSObject_IMPL {
    Class isa;
};
struct BaseClass_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};
struct Sub1Class_IMPL {
    struct BaseClass_IMPL BaseClass_IVARS;
};
struct Sub2Class_IMPL {
    struct Sub1Class_IMPL Sub1Class_IVARS;
};

Each subclass's struct contains the parent struct. Therefore, Objective-C inheritance is implemented using struct nesting at the underlying level.

Pointer Memory Translation

Pointer memory translation involves manipulating the pointer value to move the memory location pointed to. In Objective-C runtime, this technique is widely used to access instance variables, method lists, etc.

Why Categories Cannot Add Instance Variables

Because categories are added at runtime, they can only operate on the class_rw_t structure, but class_rw_t does not have an entry for adding instance variables. Instance variables are stored in class_ro_t, which cannot be modified. Hence, categories cannot add instance variables.

Relationship Among Object, Class, Superclass, and Metaclass

  1. Instance

    • Each instance is an instance of some class, containing the class's instance variables and methods.
    • At runtime, an instance's isa pointer points to its class.
  2. Class

    • A class is the blueprint for objects, defining instance variables and methods.
    • A class itself is an object, an instance of the Class type.
    • Each class has an isa pointer pointing to its metaclass.
  3. Superclass

    • A class can inherit from enother class, called its superclass.
    • Inheritance allows subclasses to use the superclass's instance variables and methods, and also add new ones or override methods.
    • Each class has a super_class pointer pointing to its superclass.
  4. Metaclass

    • A metaclass is the class of a class, defining the behavior and methods of class objects.
    • Each metaclass is also a class and has its own isa pointer pointing to the root metaclass.
    • A metaclass also has a super_class pointer pointing to the metaclass of its superclass.

Metaclass Definition

The principle is that when an Objective-C object sends a message, the runtime follows the object's isa pointer to get the class. This class contains all instance methods applicable to instances and a pointer to its superclass to find inherited methods. The runtime checks the class and its superclass method lists to find the method corresponding to the message. The compiler converts the message into a call to the message function objc_msgSend.

As shown in the diagram: Every instance's isa points to its class; every class's isa points to its metaclass; the superclass's metaclass is also the parent of the subclass's metaclass; the root metaclass's isa points to itself, and its superclass is the root class (e.g., NSObject metaclass's superclass is NSObject class).

Object, class, metaclass relationship

From the Message Sending Perspective:

  • When you send a message to an instance, the message looks for the method list of that instance's class.
  • When you send a message to a class, the message looks for the method list of that class's metaclass.

Class Object

All Objective-C objects can be categorized into three types: instance objects, class objects, and metaclass objects. Most objects we create inherit from NSObject and are instance objects. Instance objects point to class objects via isa. Class objects point to metaclass objects via isa. Both class objects and metaclass objects have the same structure, derived from objc_class.

The class Method

The class method is a special method. Its main purpose is to return the class object of the class that invoked the method. The class object is a special object containing metadata such as the class name, superclass, instance variables (ivars), etc.

Some important points:

  • Class objectClass = [[NSObject class] class]; still returns the class object, not the metaclass object.
  • Both - (Class)class and + (Class)class return the class object.

Explanation: Class objectClass = [[NSObject class] class]; first gets the NSObject class object, then calls class again on it. In theory, this should return the metaclass. However, Objective-C is designed so that when you call class on a class object, it returns the class object itself, not the metaclass. Both the instance method -class and the class method +class return the class object.

  • Although metaclass objects and class objects have the same memory structure, their purposes differ.

Class objects contain metadata for instances: ivar layout, property info, instance method list, etc. These are used when creating instances. Metaclass objects mainly contain class method information. When a class method is called, that information is used. Other parts like ivar layout and property info are usually empty.

// Instance objects
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];

// Class objects (all point to the same NSObject class)
Class class1 = [obj1 class];
Class class2 = [obj2 class];
Class class3 = [NSObject class];
Class class4 = object_getClass(obj1);
Class class5 = object_getClass(obj2);

// Metaclass objects
Class metaClass1 = object_getClass([NSObject class]);
Class metaClass2 = [[NSObject class] class];

// Check if it's a metaclass
NSLog(@"instance - %p %p", obj1, obj2);
NSLog(@"class - %p %p %p %p %p %d", class1, class2, class3, class4, class5, class_isMetaClass(class3));
NSLog(@"metaClass - %p %p %d", metaClass1, metaClass2, class_isMetaClass(metaClass1));

/* Output:
instance - 0x100511920 0x10050e840
class - 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0
metaClass - 0x7fff91da20f0 0x7fff91da2118 1
*/

No matter how many times you call class, you still get the class object.

Tags: iOS Objective-C runtime Class object

Posted on Fri, 31 Jul 2026 16:57:49 +0000 by LordMalek