What is the runtime

Official description: The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Not in compilation and linking.”

Runtime is a C library that contains many of the underlying pure C apis. OC code is usually written, the program runs, in fact, is ultimately converted to runtime C code, Runtime is the OC behind the scene.

  • The characteristics of

OC differs from other languages in that function calls use a message forwarding mechanism, but messages are not bound to any methods until the program runs. Only when the function is actually run will the function name be used to determine the function to be called.

Runtime comes in two versions:

Legacy was used in Objective-C 1.0 and Modern was used in 2.0. Nowadays runtime generally refers to modern.

1. The isa pointer

Start by understanding some of the common data structures underlying it, such as isa Pointers.

When a new object is created, a chunk of memory is allocated to it, and the instance variables of the object are initialized. The first variable isa pointer to its class (isa). Through isa Pointers, an object can access its classes and all its parent classes through its classes.

  • An instance object, represented by a structure in Runtime
Typedef struct objc_method *Method; // typedef struct objc_ivar *Ivar; // Category typedef struct objc_category *Category; // typedef struct objc_property *objc_property_t;Copy the code

View the Runtime source code to see the ISA structure.

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

    Class cls;
    uintptr_t bits;
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
};
Copy the code

The following code makes a bit-field declaration on the structure in ISA_t, sorting the addresses from nonpointer to extra_rc from lowest to highest. Bitfields are also a declaration of the memory layout of a structure, which can be manipulated directly through the following structure member variables. The bitfields are 8 bytes in total, and all the bitfields add up to exactly 64 bits.

Tip: The bits in a union can operate on the entire memory area, whereas the bitfield can only operate on the corresponding bit.

define ISA_BITFIELD \ uintptr_t nonpointer : 1; // Uintptr_t has_assoc: 1; Uintptr_t has_cxx_dtor: 1; // does C++ destruct (.cxx_destruct) exist, if not, it will be faster to release \ uintptr_t shiftcls: 33; Uintptr_t magic: 6; // Uintptr_t magic: 6; Uintptr_t Weakly_referenced: 1; // Is there a weak reference pointing to it? If not, it will be faster to release. Uintptr_t has_sidetable_rc: 1; 1 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18)Copy the code
  • nonpointer

0: indicates a common pointer that stores the memory address of Class and meta-class objects. 1: indicates that bitfields are optimized to store more information.

  • has_assoc

Check whether the associated object is set. If not, it will be released faster.

  • has_cxx_dtor

Does C++ destructor exist? Cxx_destruct will be released faster if it does not.

  • shiftcls

Store the memory address information of Class and meta-class objects

  • magic

Used during debugging to tell if an object has not been initialized

  • weakly_referenced

Is there a weak reference to point to. If not, it will be released faster

  • deallocating

Whether the object is being released

  • extra_rc

The value stored inside is the reference counter minus 1

  • has_sidetable_rc

Is the reference counter too large to be stored in ISA if it is 1, then the reference count is stored in an attribute of a class called SideTable

2. The structure of the class

The structure of the body

struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; struct objc_class : objc_object { // Class ISA; Class superclass; // Parent cache_t cache; // Method cache class_data_bits_t bits; // To get information about specific classes}Copy the code

View the source code (only the main code is preserved)

  • class_rw_t
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; method_array_t methods; // Method list property_array_t properties; // Protocol_array_t protocols; // Protocol list Class firstSubclass; Class nextSiblingClass; char *demangledName; }Copy the code

Methods, properties and protocols are two-dimensional arrays, which are readable and writable, and contain the initial content and classified content of classes.

  • method_array_t
class method_array_t : public list_array_tt<method_t, method_list_t> { typedef list_array_tt<method_t, method_list_t> Super; public: method_list_t **beginCategoryMethodLists() { return beginLists(); } method_list_t **endCategoryMethodLists(Class cls); method_array_t duplicate() { return Super::duplicate<method_array_t>(); }};Copy the code

The list of methods holds many one-dimensional arrays of method_list_t, and each of them holds method_T. Method_t is the imp pointer, name, type and other method information of the corresponding method.

  • method_t
struct method_t { SEL name; // const char *types; // Code (return value type, parameter type) MethodListIMP IMP; // Pointer to function (function address) struct SortBySELAddress: public std::binary_function<const method_t&, const method_t&, bool> { bool operator() (const method_t& lhs, const method_t& rhs) { return lhs.name < rhs.name; }}; };Copy the code

IMP: represents the specific implementation of the function SEL: represents the method, function name, generally called selector. Types: String containing function return values and parameter encoding

On the SEL:

You can use @selector() and sel_registerName() to get methods that can be converted by sel_getName() and NSStringFromSelector() to different classes of strings with the same name, and the corresponding method selector is the same. That is, the same SEL of different classes is the same object.

  • class_ro_t
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; // Class name method_list_t * baseMethodList; // List of methods protocol_list_t * baseProtocols; // protocol list const ivar_list_t * ivars; // List of member variables const uint8_t * weakIvarLayout; property_list_t *baseProperties; method_list_t *baseMethods() const { return baseMethodList; }};Copy the code

BaseMethodList, baseProtocols, Ivars, and baseProperties in class_ro_t are one-dimensional arrays that are read-only and contain the initial contents of the class

3. Type Encoding

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

+(int)testWithNum:(int)num{
    return num;
}
Copy the code

The above method can be expressed as i20@0:8i16:

I indicates that the return value is of type int, and 20 is the argument total of 20 bytes

@ indicates that the first argument is of type ID, and 0 indicates that the first argument starts from the 0th byte: indicates that the second argument is of type SEL. 8 indicates that the second argument starts at the eighth byte. I indicates that the third argument is of type int, 16 indicates that the third argument starts at the 16th byte, and is of type int, occupying 4 bytes. Total 20 bytes

Method caching

Using hash tables to cache previously called methods can speed up method lookups. Structure cache_t

struct cache_t { struct bucket_t *_buckets; // hash mask_t _mask; // The length of the hash table is -1 mask_t _occupied; Struct bucket_t {MethodCacheIMP _imp; // Function memory address cache_key_t _key; //SEL as Key}Copy the code
  • How do I find methods in cache_t
Bucket_t * cache_t::find(cache_key_t k, id receiver) {assert(k! = 0); bucket_t *b = buckets(); mask_t m = mask(); mask_t begin = cache_hash(k, m); mask_t i = begin; do { if (b[i].key() == 0 || b[i].key() == k) { return &b[i]; } } while ((i = cache_next(i, m)) ! = begin); // hack Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); }Copy the code

Where, the subscript key & mask is calculated according to the key and hash table length minus 1 mask. If the retrieved value is the same as the original key, it means that it has been found. Otherwise, you don’t have the method you’re looking for, you have a hash conflict, you add I by 1, and you keep counting. The following code:

Static inline mask_t cache_hash(cache_key_t key, mask_t mask) {return (mask_t)(key & mask); } static inline mask_t cache_next(mask_t I, mask_t mask) {return (I +1) &mask; }Copy the code
  • The expanded cache_t

When the method cache becomes too large, exceeding its capacity by 3/4s, it needs to be expanded. Expansion is doubling the original capacity.

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { ... if (cache->isConstantEmptyCache()) { // Cache is read-only. Replace it. cache->reallocate(capacity, capacity ? : INIT_CACHE_SIZE); } else if (newOccupied <= capacity /4 * 3) {// Cache is less than 3/4 full. Use it as.} else {// ->expand(); }... } // Capacity expansion enum {INIT_CACHE_SIZE_LOG2 = 2, INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)}; / / cache_t expansion void cache_t: : expand () {cacheUpdateLock. AssertLocked (); uint32_t oldCapacity = capacity(); // Uint32_t newCapacity = oldCapacity? oldCapacity*2 : INIT_CACHE_SIZE; if ((uint32_t)(mask_t)newCapacity ! = newCapacity) { // mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); }Copy the code

5. Message forwarding mechanism

OC method calls are essentially message forwarding mechanisms. For example, the object instance calls the dotest method [instance1 dotest]; Objc_msgSend (instance1, sel_registerName(“dotest”)); OC method calls are converted to objc_msgSend.

Instance objects hold isa Pointers and instance variables. The ISA pointer finds the class object (which is also an object) to which the instance object belongs. Class holds a list of instance methods. In this list, methods are saved as SEL keys and IMP values.

This generates SEL unique identifier at compile time based on the method name, IMP is actually a function pointer to the final function implementation.

The core of the Runtime is the objc_msgSend(receiver, @selector (message)) function, which sends SEL to the class to pass the message, find the matching IMP and get the final implementation.

The execution process can be divided into three stages: message sending -> dynamic method resolution -> message forwarding

  • Message sending phase:

First check whether the receiver is null. If not, find the method from the receiverClass cache. If not, look for the method from receiverClass class_rw_t. If not, look in the cache of receiverClassd’s parent class. If not, look for the method in the parent class class_rw_t. If you don’t find it, see if there are any more superclasses, and if there are, continue to look at the superclass’s cache, the list of methods.

If you do not find the implementation of this method, you must raise an exception:

. Due to uncaught exception NSInvalidArgumentException, reason: '- [XXX XXXX] : Unrecognized selector sent to instance 0x100f436c0 '

If there is no parent class, the message sending phase is over, and the second phase, dynamic method resolution, is entered.

  • Dynamic method parsing:

Here, you can dynamically bind method implementations to methods that are not found. Or redirect a method.

Source:

Void _class_resolveMethod(Class CLS, SEL SEL, id inst) {if (! CLS - > isMetaClass ()) {/ / if not yuan class object / / try/CLS resolveInstanceMethod: sel _class_resolveInstanceMethod (CLS, sel, inst); } else {/ / is a metaclass object / / try [nonMetaClass resolveClassMethod: sel] / / and/CLS resolveInstanceMethod: sel _class_resolveClassMethod(cls, sel, inst); if (! lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); }}}Copy the code

ResolveClassMethod and resolveInstanceMethod return NO by default

+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
Copy the code
  • In the dynamic parsing phase, it can be overriddenresolveInstanceMethodAnd add the implementation of the method.

If the run method is not found:

+ (BOOL)resolveInstanceMethod:(SEL) SEL {if (SEL == @selector(run)) { Method Method = class_getInstanceMethod(self, @selector(test)); // Dynamically add an implementation of the test method class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method)); // Return YES indicates that there is a dynamic addition method. In fact, it is ok to return NO, returning YES just adds some printing to return NO; } return [super resolveInstanceMethod:sel]; }Copy the code

This code is equivalent to calling test when we call run.

If the method has not been processed in the previous message sending and dynamic parsing phases, we have one final phase. The following

  • forward

____forwarding___ describes the logic of message forwarding. But not open source.

First determine forwardingTargetForSelector return values. If yes, send a message to the return value to invoke the method. If it returns nil, and then call methodSignatureForSelector method, have call forwardInvocation.

The argument is an NSInvocation object and the full properties of the message are logged. The NSInvocation object includes the Selector, target, and other parameters. The implementation simply changes the target pointing so that the message is guaranteed to be invoked.

If you find that this class can’t handle it, you keep looking for the parent class, all the way to NSObject. If methodSignatureForSelector method returns nil, call doesNotRecognizeSelector: method.

Application Examples:

Scenario 1:

The Person class only defines but does not implement the method run, and the Car class implements the method run.

Now in Person, rewrite forwardingTargetForSelector returned to the Car

/ / message forwarding - (id) forwardingTargetForSelector aSelector: (SEL) {if (aSelector = = @ the selector (run)) {return [[Car alloc] init]; } return [super forwardingTargetForSelector:aSelector]; }Copy the code

At this point, when the Person instance calls the run method, the car instance calls the run method.

Prove forwardingTargetForSelector return value is not null, then sends a message to the the return value is objc_msgSend (return value, SEL).

Scenario 2:

If the previous forwardingTargetForSelector returns null. The bottom is called methodSignatureForSelector after obtain the method signature, call forwardInvocation again.

Therefore: we can override these two methods:

// Method signature: Return value type and parameter types - (NSMethodSignature *) methodSignatureForSelector: (SEL) aSelector {the if (aSelector = = @ the selector (run)) {return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"]; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation{ [anInvocation invokeWithTarget:[[Car alloc] init]]; }Copy the code

This way, the run method of car can still be called.

NSInvocation encapsulates a method call, including the method caller, method name, and method parameters

Method name [anInvocation Invocation getArgument:NULL atIndex:0]

Supplement: 1, message forwarding forwardingTargetForSelector, methodSignatureForSelector, forwardInvocation support instance method, not only supports class methods. But the system doesn’t tell you, you need to write instance method, and then change the front – to +.

+(IMP)instanceMethodForSelector:(SEL)aSelector{
    
}

-(IMP)methodForSelector:(SEL)aSelector{
    
}
Copy the code

2. You can only add Ivars to classes that are dynamically created at runtime. You cannot add ivars to existing classes. This is because the read-only structure class_ro_t is determined at compile time and cannot be changed at run time.