This article will address the following issues

  • 1. What is Class
  • 2. Memory layout of classes
  • 3. Design philosophy of class_rw_T and class_RO_T
  • 4. Relationship between class_rw_t and classification

View the source (source version OBJC4-781.2)

Open objc-privately. h to look at the source code and see that Class is a pointer to a structure

typedef struct objc_class *Class;
Copy the code

We continue to search for “struct objc_class” in the source code, and there are five header files defined. Finally, we confirm that objc-Runtimenew.h is valid in OC2.0, and several other files are defined by the related macro definition

The objc_class structure is briefly defined as follows

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() const { return bits.data(); }... };Copy the code

Objc_class inherits from objc_object (C ++ extends c structures to allow functions to be defined, allow inheritance and default access to public, unlike c++ classes). Let’s take a look at the definition of objc_object

struct objc_object { private: isa_t isa; public: // ISA() assumes this is NOT a tagged pointer object Class ISA(); // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA Class rawISA(); // getIsa() allows this to be a tagged pointer object Class getIsa(); . };Copy the code

So now we can understand that this structure looks something like this

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() const { return bits.data(); }... Class ISA(); // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA Class rawISA(); // getIsa() allows this to be a tagged pointer object Class getIsa(); . };Copy the code

This thing down here isa private member, so the place that isa is operated on inside the class is using a set of functions that are encapsulated in objc_object, well, that’s in keeping with the open closed principle

private:
    isa_t isa;
Copy the code

Let’s go from top to bottom:

We define a superClass pointer of type Class, we define a cache_t object, class_data_bits_t object, and notice the language here. In OC, objects are Pointers, unlike structs, which are 8 bytes long on 64-bit systems. The size of the structure object is the sum of all the bytes of the internal member variables. Of course, we also need to consider the principle of memory alignment. IOS will align the object with 8 bytes, and 16 bytes as a unit, for the sake of access efficiency

Cache_t structure resolution

A cursory definition of cache_t is as follows, keeping all member variables and omitting functions

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;
    
    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;
    
    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    
    // Ensure we have enough bits for the buckets pointer.
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _maskAndBuckets stores the mask shift in the low 4 bits, and
    // the buckets pointer in the remainder of the value. The mask
    // shift is the value where (0xffff >> shift) produces the correct
    // mask. This is equal to 16 - log2(cache_size).
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

    static constexpr uintptr_t maskBits = 4;
    static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
    static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

public:
    ...
};
Copy the code

We know that conditional compilation only takes one branch. Static variables are stored in a static area, and structs do not allocate memory for them. So how much memory does a cache_t object occupy? Let’s streamline the structure again

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

public:
    ...
};
Copy the code

Explicit_atomic is a struct template whose size is the size of the parameters passed in

template <typename T> struct explicit_atomic : public std::atomic<T> { explicit explicit_atomic(T initial) noexcept : std::atomic<T>(std::move(initial)) {} operator T() const = delete; T load(std::memory_order order) const noexcept { return std::atomic<T>::load(order); } void store(T desired, std::memory_order order) noexcept { std::atomic<T>::store(desired, order); } static explicit_atomic<T> *from_pointer(T *ptr) { static_assert(sizeof(explicit_atomic<T> *) == sizeof(T *), "Size of atomic must match size of original"); explicit_atomic<T> *atomic = (explicit_atomic<T> *)ptr; ASSERT(atomic->is_lock_free()); return atomic; }};Copy the code

So what’s mask_t? Well, it’s an unsigned integer of 4 bytes to 32 bits

typedef uint32_t mask_t;
Copy the code

How is the uintptr_t defined? Well, a 64-bit system takes 8 bytes

typedef unsigned long int uintptr_t;
Copy the code

So let’s calculate the size of cache_t, 8+4+2+2, um ~16 bytes

Class_data_bits_t structure resolution

There is only one member variable inside, um ~8 bytes

struct class_data_bits_t {
    uintptr_t bits;
};
Copy the code

Next, look at this constant function, which returns a pointer to class_rw_t

class_rw_t *data() const {
    return bits.data();
}
Copy the code

Class_rw_t structure parsing

Class_rw_t ¶ Class_rw_t returns a pointer to a structure of type class_ro_t, method_array_t, property_array_t, and protocol_array_t

struct class_rw_t { const class_ro_t *ro() const {... } const method_array_t methods() const {... } const property_array_t properties() const {... } const protocol_array_t protocols() const {... }};Copy the code

After reading the source code, we found that they inherit from the template list_array_tt, and implement the add, store, release and other management functions internally

class method_array_t : public list_array_tt<method_t, method_list_t> 
{
    ...
};

class property_array_t : public list_array_tt<property_t, property_list_t> 
{
    ...
};

class protocol_array_t : public list_array_tt<protocol_ref_t, protocol_list_t> 
{
    ...
};
Copy the code

We’ll focus on the attachLists function of the ** template class, which is the core of OC’s dynamic support:

If there are multiple elements, then move the old data to array()->lists, then copy the addedLists data from the memcpy function,else if the original list is null then assign the value to addedLists,else do a one to many merge **, Therefore, in terms of data structure, methods, properties, and protocols all support dynamic update

template <typename Element, typename List> class list_array_tt { void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists uint32_t oldCount = array()->count; uint32_t newCount = oldCount + addedCount; setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount; memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (! list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1:0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); }}};Copy the code

Class_ro_t structure parsing

OldList has methods, attributes, protocols, and member variables

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
}
Copy the code

Why do you see class_ro_t attributes, methods and other member variables is oldLists, look at a section of source code

/*********************************************************************** * realizeClassWithoutSwift * Performs first-time initialization on class cls, * including allocating its read-write data. * Does not perform any Swift-side initialization. * Returns the real class structure for the class. * Locking: runtimeLock must be write-locked by the caller **********************************************************************/ static Class realizeClassWithoutSwift(Class cls, Class previously) { runtimeLock.assertLocked(); class_rw_t *rw; Class supercls; Class metacls; if (! cls) return nil; if (cls->isRealized()) return cls; ASSERT(cls == remapClass(cls)); // fixme verify class is not in an un-dlopened part of the shared cache? auto ro = (const class_ro_t *)cls->data(); auto isMeta = ro->flags & RO_META; if (ro->flags & RO_FUTURE) { // This was a future class. rw data is already allocated. rw = cls->data(); ro = cls->data()->ro(); ASSERT(! isMeta); cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE); } else { // Normal class. Allocate writeable class data. rw = objc::zalloc<class_rw_t>(); rw->set_ro(ro); rw->flags = RW_REALIZED|RW_REALIZING|isMeta; cls->setData(rw); }... }Copy the code

B: Well, Apple has been pretty clear when we perform so short a period of time.

The first time a class is initialized, this function is executed. The initial information of the class is stored in class_ro_t. After a short operation, the initial information ro is assigned to the ro in rW. Class_data_bits_t

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() const { return bits.data(); }... };Copy the code

When the object is initialized, it executes the extAlloc function, takes method_list_t, property_list_t, protocol_list_t from ro, and then carries out the attachLists method to merge it into rW

class_rw_ext_t *class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy) { runtimeLock.assertLocked(); auto rwe = objc::zalloc<class_rw_ext_t>(); rwe->version = (ro->flags & RO_META) ? 7:0; method_list_t *list = ro->baseMethods(); if (list) { if (deepCopy) list = list->duplicate(); rwe->methods.attachLists(&list, 1); } // See comments in objc_duplicateClass // property lists and protocol lists historically // have not been deep-copied // // This is probably wrong and ought to be fixed some day property_list_t *proplist = ro->baseProperties; if (proplist) { rwe->properties.attachLists(&proplist, 1); } protocol_list_t *protolist = ro->baseProtocols; if (protolist) { rwe->protocols.attachLists(&protolist, 1); } set_ro_or_rwe(rwe, ro); return rwe; }Copy the code

Design philosophy of class_rw_T and class_RO_T

Why does Apple define two similar structures to implement Class?

Ro: Read only, rw: Read write: class_ro_t is a compile-time product. Attributes, methods, protocols, and member variables in class_ro_t are stored in class_ro_t at compile time. Class_rw_t is a runtime product. The attributes, protocols, and methods in class_ro_t are dynamically merged into the corresponding data structure at runtime. Note: member variables are not included

Categories can really add attributes

Auto rwe = CLS ->data()->extAllocIfNeeded(); ExtAllocIfNeeded () calls extAlloc(), which merges rw with ro. That’s why we always say that methods with the same name in a category are called first. And so on. Methods with the same name are always queried first for classes loaded after multiple classes of a class

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();

    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
Copy the code

As mentioned above, we saw that the attributes of a category are also added to the list of attributes of the class, so why do we say that the category cannot add attributes? It has been added:

Well, that’s because we access attributes through dot syntax, and we end up accessing member variables, and the members become in class_ro_t, and classes don’t dynamically add member variables, so when we access attributes that are added to classes, we return nil, and we can’t access them through underscores. Since the member variables do not exist, they cannot be compiled, so how to make the attributes added to the class work by manually implementing the getter and setter methods and simulating the addition of the member variables, that is

conclusion

This article only discusses the memory structure. The next part will discuss ISA ~

1. What is Class

Inherits from objc_object, pointer to objc_class

2. Memory layout of classes

isa

In 32-bit, it is a CLS pointer. In 64-bit, it stores a lot of information about the class, such as whether there is a custom c++ destructor, whether there is an associated object, whether there is a weak reference, whether there is an optimized reference count with sidetable, etc

superclass

Pointer to the parent class

cache

Cache a list of methods that have been called on this class

class_rw_t

The structure stores dynamic data types. AttachLists function is used to dynamically update methods, properties, and protocols

class_ro_t

Static data types. The initial information of the class is stored in class_ro_T. At run time, take method_list_t, property_list_t, and protocol_list_t from RO and execute the attachLists method to merge them into RW

3. Design philosophy of class_rw_T and class_RO_T

Class_rw_t is designed to support Class dynamics. At runtime, attributes, protocols, and methods in class_ro_t are dynamically merged into corresponding data structures. Note: member variables are not included (dynamic addition or deletion of member variables will cause memory address disturbances).

4. Relationship between class_rw_t and classification

The attachCategories function, which merges methods in a class into class_rw_t, has already merged ro into rW, so that methods in a category with the same name as those in the original class are called first, and so on. Methods with the same name that have been loaded after multiple categories of a class are always queried first