runtime

isa

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
#endif
};
Copy the code
  • Nonpointer: indicates whether pointer optimization is enabled for isa Pointers. Zero, for ordinary pointer, is stored
  • The memory address of a Class or meta-class object is 1, indicating that it has been optimized to use bitfields to store more information
  • Has_assoc: is the associated object set? 0 does not exist, 1 exists. If not, it will be released faster.
  • Has_cxx_dtor: does C++ destructor (.cxx_destruct) exist? If it does, the destructor logic needs to be done. If not, the object can be freed faster.
  • Shiftcls: Stores the memory address information of Class and meta-class objects. In the ARM64 architecture, 33 bits are used to store Class Pointers. So the last three bits of the address of a class object or metaclass object are all 0
  • Magic: Used during debugging to tell if an object has not been initialized
  • Weakly_referenced: Whether a weak reference has been pointed to, if not, the release will be faster
  • Deallocating: Whether the object is being released
  • Has_sidetable_rc: Whether the object reference count is greater than 10. If it is greater than 10, sideTable is used for storage
  • Extra_rc: The value stored in extra_rc is the reference counter minus 1, which represents the reference count of the object. In fact, the reference count is actually minus 1. For example, if the object’s reference count is 10, extra_rc is 9. If the reference count is greater than 10, the following has_sideTABLE_rc is used.

The class structure:

Methods, properties, and protocols in class_rw_t are two-dimensional arrays that can be read and written and contain the initial content of classes and classified content

Cache_t Method cache

sstruct cache_t { explicit_atomic<struct bucket_t *> _buckets; // explicit_atomic<mask_t> _mask; // Hash length -1 #if __LP64__ uint16_t _flags; #endif uint16_t _occupied; Public: static bucket () *emptyBuckets(); struct bucket_t *buckets(); mask_t mask(); mask_t occupied(); void incrementOccupied(); void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask); void initializeToEmpty(); unsigned capacity(); bool isConstantEmptyCache(); bool canBeFreed(); #if __LP64__ bool getBit(uint16_t flags) const ; void setBit(uint16_t set) ; void clearBit(uint16_t clear) ; #endif #if FAST_CACHE_ALLOC_MASK bool hasFastInstanceSize(size_t extra) const; size_t fastInstanceSize(size_t extra) const; void setFastInstanceSize(size_t newSize); #else bool hasFastInstanceSize(size_t extra) const { return false; } size_t fastInstanceSize(size_t extra) const { abort(); } void setFastInstanceSize(size_t extra) { // nothing } #endif static size_t bytesForCapacity(uint32_t cap); static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap); void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld); void insert(Class cls, SEL sel, IMP imp, id receiver); static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn, cold)); };Copy the code
struct bucket_t { private: explicit_atomic<uintptr_t> _imp; explicit_atomic<SEL> _sel; uintptr_t modifierForSEL(SEL newSel, Class cls) const { return (uintptr_t)&_imp ^ (uintptr_t)newSel ^ (uintptr_t)cls; } modifiers. uintptr_t encodeImp(IMP newImp, SEL newSel, Class cls) const { if (! newImp) return 0; return (uintptr_t)newImp ^ (uintptr_t)cls; } public: inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); } inline IMP imp(Class cls) const { uintptr_t imp = _imp.load(memory_order::memory_order_relaxed); if (! imp) return nil; return (IMP)(imp ^ (uintptr_t)cls); } template <Atomicity, IMPEncoding> void set(SEL newSel, IMP newImp, Class cls); };Copy the code
  • deposit
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver) { runtimeLock.assertLocked(); ASSERT(sel ! = 0 && cls->isInitialized()); if (slowpath(isConstantEmptyCache())) { // Cache is read-only. Replace it. if (! capacity) capacity = INIT_CACHE_SIZE; reallocate(oldCapacity, capacity, /* freeOld */false); } else if (fastpath(newOccupied <= capacity / 4 * 3)) { // Cache is less than 3/4 full. Use it as-is. } else { capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; if (capacity > MAX_CACHE_SIZE) { capacity = MAX_CACHE_SIZE; } // Locate reallocate(oldCapacity, capacity, true); } 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)) {cache_t _occupied++ incrementOccupied(); b[i].set<Atomic, Encoded>(sel, imp, cls); return; } if (b[I].sel() == sel) {if (b[I].sel() == sel) { } } while (fastpath((i = cache_next(i, m)) ! = begin)); Cache_t ::bad_cache(receiver, (SEL) SEL, CLS); }Copy the code

Bucket_t _imp&mask, mask= hash length -1, source code as follows

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}
Copy the code

If the calculated position is occupied, the order will be -1 until the empty place, at most one circle, source code is as follows:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
Copy the code

method_t

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...) ; struct method_t { SEL name; // name const char *types; // Code values (return values, parameters) MethodListIMP IMP; // function address};Copy the code
  1. SEL stands for method \ function name, usually called selector, and has the same underlying structure as char *
  2. This can be obtained via @selector() and sel_registerName()
  3. You can convert it to a string by sel_getName() and NSStringFromSelector()
  4. Methods with the same name in different classes have the same method selector
  5. Types contains strings of function return values and parameter encoding

A directive called @encode is provided in iOS to represent specific types as string encodings


objc_msgSend

The execution process of objc_msgSend can be divided into three phases

  1. Message is sent
  2. Dynamic method parsing
  3. forward

Source code analysis

  • Message is sent

In this stage, the method will be searched in the cache of the class pointed to by ISA. If it is not found, the method will be searched in the RW of the class. If it is not found, the method will be searched in the superclass of the class.

ENTRY _objc_msgSend CBZ r0, LNilReceiver_f // If ro (self) is 0, LNilReceiver LDR R9, [r0] // r9 = self->isa getFromisa NORMAL, _objc_msgSend IMP in R12, eq already set for nonstret forwarding bx r12 _objc_msgSend // cache miss ldr r9, [r0] // r9 = self->isa GetClassFromIsa // r9 = class b __objc_msgSend_uncached LNilReceiver: // r0 is already zero mov r1, #0 mov r2, #0 mov r3, #0 FP_RETURN_ZERO bx lr // equivalent to mov PC,lr, i.e. jump returns END_ENTRY _objc_msgSendCopy the code

Cached if not, go to __objc_msgSend_uncached

STATIC_ENTRY __objc_msgSend_uncached // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band r9 is the class to search MethodTableLookup NORMAL // returns IMP in R12 Search method list bx R12 END_ENTRY __objc_msgSend_uncachedCopy the code

This method calls MethodTableLookup to search

.macro MethodTableLookup stmfd sp! , {r0-r3,r7,lr} add r7, sp, #16 sub sp, #8 // align stack FP_SAVE // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) .if $0 == NORMAL // receiver already in r0 // selector already in r1 .else mov r0, r1 // receiver mov r1, r2 // selector .endif mov r2, r9 // class to search mov r3, #3 // LOOKUP_INITIALIZE | LOOKUP_INITIALIZE blx _lookUpImpOrForward mov r12, r0 // r12 = IMP .if $0 == NORMAL cmp r12, r12 // set eq for nonstret forwarding .else tst r12, r12 // set ne for stret forwarding .endif FP_RESTORE add sp, #8 // align stack ldmfd sp! , {r0-r3,r7,lr} .endmacroCopy the code
  • The dynamic analysis

This method actually calls _lookUpImpOrForward to find the IMP, and returns a forward IMP if the IMP does not exist

IMP lookUpImpOrForward(id inst, SEL SEL, Class CLS, int behavior) */) {const IMP forward_imp = (IMP)_objc_msgForward_impcache; // message forwarding IMP IMP = nil; Class curClass; runtimeLock.assertUnlocked(); // Optimistic cache lookup if (fastpath(behavior & LOOKUP_CACHE)) { imp = cache_getImp(cls, sel); if (imp) goto done_nolock; } runtimeLock.lock(); checkIsKnownClass(cls); If (slowPath (!) slowPath (!) cls->isRealized())) { cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock); } if (slowpath((behavior & LOOKUP_INITIALIZE) && ! cls->isInitialized())) { cls = initializeAndLeaveLocked(cls, inst, runtimeLock); } runtimeLock.assertLocked(); curClass = cls; Reasonableclasscount ();) { // curClass method list. Method meth = getMethodNoSuper_nolock(curClass, sel); Method if (meth) {imp = meth->imp; goto done; If (slowPath ((curClass = curClass->superclass) == nil)) {forward_imp imp = forward_imp; break; } // huan cun imp = cache_getImp(curClass, sel); if (slowpath(imp == forward_imp)) { // Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method // resolver for this class first. break; } if (fastpath(imp)) { // Found the method in a superclass. Cache it in this class. goto done; If (slowpath(behavior & LOOKUP_RESOLVER)) {behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); } done: log_and_fill_cache(cls, imp, sel, inst, curClass); // Cache it in clAS's own cache, whether the method is runtimelock.unlock () of this class or its parent; done_nolock: if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) { return nil; } return imp; }Copy the code

LookUpImpOrForward attempts to call cache_getImp to imp, which returns done_NOLock. If the class is not initialized, the class is initialized, including the RW, etc. The CLS and the list of superclass methods are losed over. If the resolver method is not found, imp is assigned fordward. If the resolver method has not been called, it will be called only once, and the reason for invoking behavior is that it adopts the bit value of behavior. Behavior ^= LOOKUP_RESOLVER, and here’s how to call resolve (metaclass and class 2 methods)

static void resolveInstanceMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); SEL resolve_sel = @selector(resolveInstanceMethod:); if (! LookUpImpOrNil (CLS, resolve_sel, CLS ->ISA())) {return lookUpImpOrNil(CLS, resolve_sel, CLS ->ISA())); BOOL (* MSG)(Class, SEL, SEL) = (typeof(MSG))objc_msgSend; bool resolved = msg(cls, resolve_sel, sel); // If Resolver returns yes or no, the result will only be used to print sel. IMP IMP = lookUpImpOrNil(INST, SEL, CLS); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn't add anything? _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); } } } static void resolveClassMethod(id inst, SEL sel, Class cls) { runtimeLock.assertUnlocked(); ASSERT(cls->isRealized()); ASSERT(cls->isMetaClass()); if (! lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) { // Resolver not implemented. return; } Class nonmeta; { mutex_locker_t lock(runtimeLock); nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst); // +initialize path should have realized nonmeta already if (! nonmeta->isRealized()) { _objc_fatal("nonmeta class %s (%p) unexpectedly not realized", nonmeta->nameForLogging(), nonmeta); } } BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel); // Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveClassMethod adds to self->ISA() a.k.a. cls IMP imp = lookUpImpOrNil(inst, sel, cls); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn't add anything? _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); }}}Copy the code

It can be seen from the code that it does not matter whether the resolver method returns yes or no. After the call, it will look for sel implementation again. If the rsolver method, We’ve added it dynamically, so we’ll cache the implementation.

  • forward

Finally, if none is found, the message will be forwarded. If we do not implement the forward method, an error message will be reported that the method cannot be found. Below is the flow chart