preface

In the last chapter, we mainly explored the nature of the object and the relationship between the object and the class. In this chapter, we mainly explored the structure of the class to understand what is stored in the class and what is the function.

The structure of the class

Class

If we want to know about classes, let’s look at classes, let’s go to the source code and see what’s stored in a Class.

typedef struct objc_class *Class;
Copy the code

We can see that Class is an object, so a Class is also an object, and we usually call it a Class object.

objc_class

So let’s click in and see what objc_class is?

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() { return bits.data(); } void setData(class_rw_t *newData) { bits.setData(newData); } /** omit several hundred lines */}Copy the code

The code here is somewhat familiar and has been covered in previous chapters.

So this inherits objc_Object, so let’s see what the parent class holds.

struct objc_object {
private:
    isa_t isa;    
}
Copy the code

The parent class holds the isa pointer.

We have examined ISA and Superclasses in the last two chapters. So let’s focus on cache_t and class_data_bits_t.

cache_t

From the name, we might guess that this is a cache, but what is it? How big is the occupation?

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

_buckets: a pointer to a structure, which is a hash table.

_mask: indicates the current maximum storage capacity.

_occupied: indicates the number of cached methods.

buckets

Let’s see what they have in buckets.

struct bucket_t { private: // IMP-first is better for arm64e ptrauth and no worse for arm64. // SEL-first is better for armv7* and i386 and x86_64.  #if __arm64__ explicit_atomic<uintptr_t> _imp; explicit_atomic<SEL> _sel; #else explicit_atomic<SEL> _sel; explicit_atomic<uintptr_t> _imp; #endifCopy the code

Bucket_t has _IMP and _sel properties in it. Obviously, this is the method used to store the cache.

cache_fill

When we call the object’s method, we go through the objc_msgSend process, and we come in with cache_fill.

void cache_fill(Class cls, SEL sel, IMP imp, id receiver) { runtimeLock.assertLocked(); #if ! DEBUG_TASK_THREADS // Never cache before +initialize is done if (cls->isInitialized()) { cache_t *cache = getCache(cls);  #if CONFIG_USE_CACHE_LOCK mutex_locker_t lock(cacheUpdateLock); #endif cache->insert(cls, sel, imp, receiver); } #else _collecting_in_critical(); #endif }Copy the code

If the class is already initialized and the cache is fetched, the cache->insert method is called.

We analyze it directly in the code.

void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) { ASSERT(sel ! = 0 && cls->isInitialized()); // Use the cache as-is if it is less than 3/4 full // newOccupied: new method cache number = number of cached methods + 1 mask_t newOccupied = occupied() + 1; Mask +1 unsigned oldCapacity = capacity(), capacity = oldCapacity; Slowpath (isConstantEmptyCache())) {// Cache is read-only. Replace it. // If capacity is 0, initialize to 4 if (! capacity) capacity = INIT_CACHE_SIZE; Reallocate (oldCapacity, capacity, /* freeOld */false); } // If newOccupied < three quarters of capacity, meaning sufficient storage space, Else if (fastPATH (newOccupied + CACHE_END_MARKER <= capacity /4 * 3)) {// Cache is less than 3/4 full. Use it As-is.} // More than 3/4 else {// Expand the current capacity by 2 times. capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; If (capacity > MAX_CACHE_SIZE) {capacity = MAX_CACHE_SIZE; // If (capacity > MAX_CACHE_SIZE) {capacity = MAX_CACHE_SIZE; } // Optimize the capacity space by passing true reallocate(oldCapacity, capacity, true); } // If the hash table has a cache for the method, insert the cache if it does not. bucket_t *b = buckets(); mask_t m = capacity - 1; mask_t begin = cache_hash(sel, m); mask_t i = begin; do { if (fastpath(b[i].sel() == 0)) { incrementOccupied(); b[i].set<Atomic, Encoded>(sel, imp, cls); return; } if (b[i].sel() == sel) { return; } } while (fastpath((i = cache_next(i, m)) ! = begin)); cache_t::bad_cache(receiver, (SEL)sel, cls); }Copy the code

There are two calls to RealLocate, one true and one false. Let’s see what’s going on inside.

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld) { bucket_t *oldBuckets = buckets(); // bucket_t *newBuckets = allocateBuckets(newCapacity); ASSERT(newCapacity > 0); ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1); // Set Buckets and mask to -1. setBucketsAndMask(newBuckets, newCapacity - 1); Cache_collect_free (oldBuckets, oldCapacity); if (freeOld) {// If (freeOld) is true, cache_collect_free(oldBuckets, oldCapacity); }}Copy the code

Add the code for the relationship between capacity and mask.

unsigned cache_t::capacity()
{
    return mask() ? mask()+1 : 0; 
}
Copy the code

Summary of chche method

So let’s summarize what we did up here.

  1. Initialize the cache space first. Method; newOccupied+1; capacity; newOccupied+1;

  2. If the cache is empty, we initialize capacity to 4. At this point, the Occupied method occupies no more than three quarters of our initialization capacity, and the hash table buckets stores the method.

  3. We initialized the capacity: Occupied; we expanded the capacity by two times; the maximum capacity is 16. The existing cache information is also released. Re-store the new method.

  4. Method cache: Occupied; method cache: Occupied; method cache: Occupied;

class_data_bits_t

struct class_data_bits_t {

    friend objc_class;

    uintptr_t bits;

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

Combining the bits. Data () and bits. Setdata () we can get and setclass_rw_t.

class_rw_t

struct class_rw_t { uint32_t flags; uint16_t witness; #if SUPPORT_INDEXED_ISA uint16_t index; #endif explicit_atomic<uintptr_t> ro_or_rw_ext; Class firstSubclass; Class nextSiblingClass; private: using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>; const ro_or_rw_ext_t get_ro_or_rwe() const { return ro_or_rw_ext_t{ro_or_rw_ext}; }}Copy the code

So ro_or_rw_ext, which is class_ro_t at compile time and class_rw_ext_t at compile time, The compile-time class_ro_t is held as a const class_ro_t *ro member of class_rw_ext_t.

class_rw_ext_t

The attributes, methods, and protocols that a class follows are stored in class_rw_ext_t.

At runtime, class_rw_ext_t copies the contents of class_ro_t as attributes, and then copies the attributes and methods of the current class into them.

Of course, the actual access to the class methods, attributes and so on are also access class_rw_T content.

struct class_rw_ext_t {

    const class_ro_t *ro;
    
    method_array_t methods;

    property_array_t properties;

    protocol_array_t protocols;

    char *demangledName;

    uint32_t version;

};
Copy the code

The following information is stored in class_rw_ext_t:

  • ro : class_ro_t.
  • methods: List of methods, including classification methods.
  • properties: Attribute list.
  • protocols: Protocol list.
  • demangledName: Indicates the name of the owning class.

class_ro_t

Class_ro_t stores properties, methods, and protocols that were determined at compile time for the current class and is immutable.

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

The following information is stored in class_ro_t:

  • baseMethodList : List of methods.
  • baseProperties: Attribute list.
  • baseProtocols: Protocol list.
  • ivars: List of member variables.

Bits to summarize

From the above analysis, it can be seen that the main information of the class object is stored in the class_rw_ext_t and class_ro_t structures.

conclusion

From the above analysis, we can clearly know the structure of class isa pointer, superClass, cache method cache, and bits (containing method list, attribute list, protocol list, and member variable list).

In the next chapter, we’ll talk more about the nature of methods and how they are called.