runtime and memory management of iOS

Posted by nschmutz on Tue, 08 Mar 2022 20:46:54 +0100

runtime and memory management of iOS

What is it?

To understand what runtime is, the description of official documents should be the most authoritative. The official definition is as follows: from compilation time and link time to runtime, Objective-C language will postpone decisions as much as possible. Whenever possible, it performs operations dynamically. This means that the language needs not only a compiler, but also a runtime system to execute the compiled code. The operating system acts as an operating system for Objective-C language.

You can know that runtime is the execution environment of OC through official documents. In order to better understand runtime, it is necessary to first understand what OC is. Also according to the official instructions: Objective-C is the main programming language used when writing software for OS X and iOS. It is a superset of C programming language and provides object-oriented function and dynamic runtime. Objective-C inherits C's syntax, original types and flow control statements, and adds syntax for defining classes and methods. It provides dynamic typing and binding, deferring many responsibilities to runtime.

According to the two official instructions, I give my own definition. OC is a superset of C. It provides syntax features of object-oriented functions, as well as dynamic types and bindings. The reason why it can support these related features mainly depends on the joint action of compiler and runtime. The compiler converts OC to C, and runtime provides the basis for the implementation of various features. Runtime is a set of API s written in C language, which provides the running basis for OC.

Solve what

I know that runtime mainly serves OC and provides OC with the characteristics of object-oriented, dynamic type binding and memory management. Step by step, the three main characteristics of object-oriented are encapsulation, inheritance and polymorphism. OC also has a unique dynamic binding. The next step is to better understand how runtime can solve these problems. Generally, however, to understand how the Objective-C runtime system works and how to use it, there is little reason for you to understand and understand this system to write Cocoa applications, as the official documents say.

encapsulation

Generally speaking, encapsulation is to create a series of data and functions into a more usable data structure, and then we define the data structure in C language. Generally speaking, it is realized through structure, so similarly, the classification in OC is also structure after it is transformed into C language. You can find struct objc by looking at the runtime source code_ Class is 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
}

You can see in oc2 Structural inheritance objc of classes in 0_ Object. The main members are superclass, cache and bits. In terms of encapsulation, the main concern here is class_data_bits_t bits; The specific structure is as follows. In order to facilitate viewing, other codes have been deleted. You can see that there are classes inside_ rw_ t,class_ ro_ Structure of T

struct class_data_bits_t {
   public:
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    const class_ro_t *safe_ro() const {
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw is rw
            return maybe_rw->ro();
        } else {
            // maybe_rw is actually ro
            return (class_ro_t *)maybe_rw;
        }
    }
}

Further check as follows. You can see that the two structures mainly include method_list,property_list,protocol_list,ivar_list

struct class_rw_t {
    // Be wmearned that Symbolication knows the layout of this structure.
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
}

struct class_ro_t {
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    property_list_t *baseProperties;
}

To summarize, OC's classes are called objc_class, which inherits from objc_object, which mainly includes three members: one is used to cache methods, one is used to point to its parent class, and the other is class_data_bits_t bits contains more complex structures, namely class_rw_t,class_ro_t and these two structures contain member list, method list and protocol list. For the feature of object-oriented encapsulation, the above structure can be satisfied. runtime uses this multi-layer structure to define a class.

inherit

Knowing the general structure of the class is a good explanation for inheritance. From the structure, we can know that the class structure contains a member pointing to Class superclass. Through this structure, we can access the members, methods and protocols of the parent class, so as to realize the characteristics of inheritance. And objc_class structure, which inherits from objc_object´╝îobjc_ What is object, objc_object is the definition of instance object by runtime. Its internal structure is as follows, mainly including an isa_t isa member, which includes members of the series, the most critical of which is ISA_BITFIELD, a technology used to save memory, is mainly used for some marker bits in memory management.

struct objc_object {
private:
    isa_t isa;
}

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

    Class cls; 
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // This key
    };
#endif
};
#   define ISA_BITFIELD                                                      \
      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

  • nonpointer (stored in the 0th byte): whether it is an optimized isa flag. 0 stands for ISA before optimization, a pointer to a pure class or metaclass; 1 indicates the optimized ISA, which is more than a pointer. Isa contains class information, reference count of objects, etc.

  • has_assoc (stored in the first byte): associated object flag bit. Objects contain or have contained associated references. 0 means no, 1 means yes. Those without associated references can release memory faster.

  • has_cxx_dtor (stored in the second byte): destructor flag bit. If there is a destructor, destructor logic is required. If not, the object can be released more quickly.

  • shiftcls: (stored in bytes 3-35) the pointer of the storage class is actually the content pointed to by isa before optimization.

  • magic (stored in bytes 36-41): determine whether the initialization of the object is completed. It is the debugger that determines whether the current object is a real object or there is no initialization space.

  • weakly_referenced (stored in the 42nd byte): the object is pointed to or has pointed to an ARC weak variable. Objects without weak references can be released faster.

  • deallocating (stored in the 43rd byte): indicates whether the object is freeing memory.

  • has_sidetable_rc (stored in the 44th byte): judge whether the reference count of the object is too large. If it is too large, other hash tables are required for storage.

  • extra_rc (stored in bytes 45-63.): Store the result of subtracting 1 from the reference count value of the object. If the reference count of the object exceeds 1, it will exist in this. If the reference count is 10, extra_ The value of RC is 9.

It can be seen that when OC is object-oriented, classes, metaclasses and instance objects are objc_object structure.

polymorphic

For polymorphism, it is relatively simple. When an instance calls a method again, it will cache its own class object structure first_ T cache. If not, look it up in your own method list. If not, look it up in the parent class. In this way, the class object can implement the same method as the parent class. When calling, give priority to the cache, and then look it up in yourself and then in the parent class.

Summary

Through the analysis of runtime source code, we can know that runtime provides the basis for the implementation of OC object-oriented characteristics, and defines the structure of class, metaclass and instance object through a series of design. Class objects mainly store instance methods, metaclass objects mainly store class methods, and instance objects mainly store various member variables. Through this design, a structure, objc, is realized_ Object defines all objects of OC.

function

Through the above analysis, we can see that the definition of OC class is not enough, but also the operation process is comprehensive. For many designs, the basic functions of adding, deleting, modifying and querying are generally provided. Similarly, runtime also provides many API s for similar processing of classes. Please check the official documents for specific use details, There is not much discussion here. Next, I want to discuss the main things that runtime does from the perspective of the life cycle of an object.

establish

For the creation of objects, malloc method will be called, while for metaclass objects and class objects, it should be completed in the run-time initialization stage. The whole process should be complex. Initialize and register metaclass objects and class objects from the data segment of mach-o file to runtime, and the instance object manages the memory of the instance through the reference counting scheme, And point the corresponding ISA pointer to the corresponding class object.

Method calls the message in the OC

In other words, it is not bound to a c-objective method until it is called at runtime, that is, it is bound to an object at runtime. It mainly goes through the following stages: dynamic method binding, dynamic method parsing and message forwarding.

objc_msgSend function implements method dynamic binding

In Objective-C, object calling methods are finally converted to objc_ The call of msgsend. This function mainly has two parameters, one is the receiver and the other is the selector. For OC, these two are hidden parameters, which represent self respectively_ cmd. When the pointer of the class selector is passed to the structure of the isa class, the message will be sent to the function table. If the selector cannot be found in it, objc_msgSend follows the pointer to the superclass and tries to find the selector in its schedule. Continuous failures lead to objc_msgSend climbs the class hierarchy until it reaches the NSObject level. After finding the selector, the function will call the method entered in the table and pass the method to the data structure of the receiving object.

To speed up the messaging process, the runtime system caches the selectors and addresses of methods when they are used. Each class has a separate cache_t cache, and can contain inherited methods and selectors for methods defined in this class. Before searching the schedule table, the messaging routine first checks the cache of the class of the receiving object (according to theory, the method once used may be used again). If the method selector is in the cache, the message delivery is only a little slower than the function call. Once the program runs long enough to "warm up" its cache, almost all messages it sends will find a cache method. The cache can grow dynamically to accommodate new messages while the program is running.

runtime also provides a way to use methodForSelector: avoiding dynamic binding can save most of the time required for message delivery. However, this savings is significant only if a particular message is repeated many times.

Dynamic method analysis

In some cases, you may want to dynamically provide the implementation of the method, that is, what happens after the receiver does not find the relevant selector during the dynamic binding phase. You can implement these methods, resolveInstanceMethod: or resolveClassMethod: to dynamically provide an implementation for a given selector of an instance or class method. The Objective-C method is just a C function, which contains at least two parameters - self and_ cmd. You can use functions to add functions as methods to your class_addMethod, which can be implemented similar to the following.

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

This also provides an implementation basis for dynamic loading.

Opportunity to process messages before message forwarding and error reporting

If a method is called and the method selector is not found in the dynamic method binding and dynamic method parsing stages, the runtime also provides an opportunity to process messages, which will enter the message forwarding stage of message processing. At this stage, the runtime will send a forwardInvocation: message to the object, which contains that the NSInvocation object is its unique parameter - the NSInvocation object encapsulates the original message and the parameters passed with it. You can specify an object in this method to handle the message selector.

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

Message forwarding imitates inheritance, which enables OC to achieve the effect of multiple inheritance. The object that responds to the message by forwarding the message seems to borrow or "inherit" the method implementation defined in another class. In order for another object to accurately handle the receiver, methodSignatureForSelector: will return the exact method signature of the proxy class.

If the message cannot be processed in all three stages, it will eventually be cash ed.

Destroy from release to dealloc

Runtime maintains a sidetable to manage the reference count of each object for memory management. When the reference count is 0, the dealloc method of the object itself will be called. After execution, the dealloc method of the parent class will be called up level by level until it is called to the dealloc method of NSObject. The dealloc method of NSObject will call the object of runtime_ Dispose method, which will call the destructor of each C + + instance variable; The release method of strongly referencing OC instance variables, that is, the reference count of each instance variable is reduced by one; Disassociate all associated objects; Then all the weak references (all weak pointers pointing to the current OC object are emptied), and finally the free method is called to release the memory. There are class structures that we know in Isa_ The flag bit in bitfield represents the above related information.

The memory management of OC is realized by reference count. When the reference count is 0, the object is released. This step is realized when calling the release method. It is necessary to understand the implementation principle of release.

- (oneway void)release {
    _objc_rootRelease(self);
}

NEVER_INLINE void
_objc_rootRelease(id obj)
{
    ASSERT(obj);

    obj->rootRelease();
}

ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

Finally, execute to bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) function
The performDealloc flag executes dealloc when < = 0 after the reference count of - 1
handleUnderflow indicates that the reference count needs to be borrowed from the sidetable

inline bool 
objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}

dealloc internal call_ objc_rootDealloc(id obj), which internally calls obj - > rootdealloc();

//For architectures that do not support nonpointer isa
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;
    object_dispose((id)this);
}
//For supported
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        // The isa of the object has no release related that requires additional processing, so it is released directly
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

For architectures that do not support nonpointer isa

  • Direct object_dispose

For architectures that support nonpointer isa

  • TaggedPointer returns directly
  • If the non pointer isa object does not store tags that need additional processing in its ISA, it will be free directly
  • Otherwise, call object_dispose handles the release logic

     object_ The source code of dispose is as follows. Objc will be called internally_ Destructinstance for further release

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor(); // Is there a c + + destruct method
        bool assoc = obj->hasAssociatedObjects(); // Is there an associated object

        // This order is important.
        if (cxx) object_cxxDestruct(obj); // Call c + + destructor
        if (assoc) _object_remove_assocations(obj); // Remove associated objects
        obj->clearDeallocating(); // Clear weak references and records in sidetable
    }

    return obj;
}

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating(); // ponter isa takes the reference counter from the sidetable for processing
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow(); // nonpointer isa clears weak references or reference counts stored in sidetable
    }

    assert(!sidetable_present());
}

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this); // Find the record of this object from sidetable
    if (it != table.refcnts.end()) { // eureka
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) { // The flag bit of weak reference to determine whether there is a weak reference record
            weak_clear_no_lock(&table.weak_table, (id)this); // Clear records in weak reference tables
        }
        table.refcnts.erase(it); // Remove counter from sidetable
    }
    table.unlock();
}

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) { // Weak reference record
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) { // Count information is stored in sidetable
        table.refcnts.erase(this);
    }
    table.unlock();
}
}

summary

Through the analysis of some original codes of runtime, we can know that some functions of runtime are mainly about the implementation of oc object-oriented and memory management. Runtime defines and encapsulates classes by defining structures, and calls methods through the mechanisms of message dynamic binding, message dynamic parsing and message forwarding, Manage the memory of objects by reference counting. It also provides a large number of API s written in C language to directly operate classes or objects, mainly involving common operations such as addition, deletion, modification and query.

reference material

Apple official documents

Internal implementation of OC release

Topics: iOS objective-c