The introduction
We all know that Runtime is at the heart of the dynamic objective-C language, and understanding it will help us better understand how Objective-C works and become more comfortable with programming. Due to limited time and energy, I mainly want to read the Runtime source code from the following aspects, which will be gradually improved in the future. As the overall length is long, I will divide each part into an article for specific analysis.
directory
First, the nature of the object, understand ISA
The life cycle of the object
Object reference count
Object extension method
5. Application of Runtime
The nature of the object, understand ISA
Let’s start by looking at object and class definitions
/// Represents an instance of a class.
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA(a);
};
// There is an internal pointer to Class. What is Class
// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
Objc_class is also derived from objc_Object. Since objc_Object already defines an ISA pointer, and all members of the structure are public, So objc_class has an ISA and has access to it.
struct objc_class : objc_object {
// Class ISA; // The isa pointer in the class points to the metaClass metaClass
Class superclass; / / parent class
cache_t cache; // former cache Pointer and vtable class method cache // formerCache pointer and vtable class method cache
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
// class_data_bits_t <==> class_rw_t + one RR /alloc bit
};
Copy the code
So now we understand that, in fact, a class is also an object
-
It is important to remember that in Objective-C, methods of objects are not stored in the structure of the object (if each object has methods that it can execute, this has a huge impact on memory usage). When an instance method is called, the corresponding class is found through the ISA pointer and the method in the class is found through class_datA_bits_t. How to find it, we see 👇
// First there is a bit in class_datA_bits_t struct class_data_bits_t { // Values are the FAST_ flags above. uintptr_t bits; public: // Return class_rw_t* with bits and FAST_DATA_MASK, and return class_rw_t* with bits [3, 47]. class_rw_t* data() { return (class_rw_t*)(bits & FAST_DATA_MASK); }}// data() returns a pointer to class_rw_t*. // Attributes, methods, and protocols are stored in 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; property_array_t properties; protocol_array_t protocols; Class firstSubclass; Class nextSiblingClass; char *demangledName; #if SUPPORT_INDEXED_ISA uint32_t index; #endif }; // Class_rw_t contains some information about the class, such as flag, version number, method array, attribute array, etc. And there's a pointer to class_ro_t, so what's class_ro_t? Class_ro_t contains the properties, methods, and protocols that were determined at compile time for the current class. 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; method_list_t *baseMethods() const { returnbaseMethodList; }};Copy the code
-
So from this, we know that class_ro_t holds methods that were determined at compile time, so at compile time, class_data_bits_t will point directly to class_ro_t, and then at Runtime, Call class_data_bits_t data() to convert the result directly from class_rw_T to a class_ro_T pointer, and then initialize a class_rw_t pointer with null data. Then set its RO variable and flag, and finally set the correct data for it
/*********************************************************************** * realizeClass * Performs first-time initialization on class cls, * including allocating its read-write data. * Returns the real class structure for the class. * Locking: runtimeLock must be write-locked by the caller **********************************************************************/ static Class realizeClass(Class cls) { runtimeLock.assertWriting(); const class_ro_t *ro; class_rw_t *rw; Class supercls; Class metacls; bool isMeta; 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? // Force conversion to ro ro = (const class_ro_t *)cls->data(); if (ro->flags & RO_FUTURE) { // This was a future class. rw data is already allocated. rw = cls->data(); ro = cls->data()->ro; cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE); } else { // Normal class. Allocate writeable class data. // The rW is allocated memory space and initialized to 0 rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); // make rw point to ro rw->ro = ro; // Set the rW flag to initializing and initialized /** // class is realized - must never be set by compiler #define RO_REALIZED (1<<31) // Values for class_rw_t->flags // These are not emitted by the compiler and are never used in class_ro_t. // Their presence should be considered in future ABI versions. // class_t->data is class_rw_t, not class_ro_t #define RW_REALIZED (1<<31) */ rw->flags = RW_REALIZED|RW_REALIZING; cls->setData(rw); } MetaClass RO_META (1 << 0) isMeta = ro->flags & RO_META; rw->version = isMeta ? 7 : 0; // old runtime went up to 6 // Choose an index for this class. // Sets cls->instancesRequireRawIsa if indexes no more indexes are available cls->chooseClassArrayIndex(); if (PrintConnecting) { _objc_inform("CLASS: realizing class '%s'%s %p %p #%u", cls->nameForLogging(), isMeta ? " (meta)" : "", (void*)cls, ro, cls->classArrayIndex()); } // Realize superclass and metaclass, if they aren't already. // This needs to be done after RW_REALIZED is set above, for root classes. // This needs to be done after class index is chosen, for root metaclasses. supercls = realizeClass(remapClass(cls->superclass)); metacls = realizeClass(remapClass(cls->ISA())); #if SUPPORT_NONPOINTER_ISA // Disable non-pointer isa for some classes and/or platforms. // Set instancesRequireRawIsa. bool instancesRequireRawIsa = cls->instancesRequireRawIsa(); bool rawIsaIsInherited = false; static bool hackedDispatch = false; if (DisableNonpointerIsa) { // Non-pointer isa disabled by environment or app SDK version instancesRequireRawIsa = true; } else if(! hackedDispatch && ! (ro->flags & RO_META) &&0= =strcmp(ro->name, "OS_object")) { // hack for libdispatch et al - isa also acts as vtable pointer hackedDispatch = true; instancesRequireRawIsa = true; } else if (supercls && supercls->superclass && supercls->instancesRequireRawIsa()) { // This is also propagated by addSubclass() // but nonpointer isa setup needs it earlier. // Special case: instancesRequireRawIsa does not propagate // from root class to root metaclass instancesRequireRawIsa = true; rawIsaIsInherited = true; } if (instancesRequireRawIsa) { cls->setInstancesRequireRawIsa(rawIsaIsInherited); } // SUPPORT_NONPOINTER_ISA #endif // Update superclass and metaclass in case of remapping cls->superclass = supercls; cls->initClassIsa(metacls); // Reconcile instance variable offsets / layout. // This may reallocate class_ro_t, updating our ro variable. if(supercls && ! isMeta) reconcileInstanceVariables(cls, supercls, ro);// Set fastInstanceSize if it wasn't set already. cls->setInstanceSize(ro->instanceSize); // Copy some flags from ro to rw if (ro->flags & RO_HAS_CXX_STRUCTORS) { cls->setHasCxxDtor(); if(! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) { cls->setHasCxxCtor(); }}// Connect this class to its superclass's subclass lists if (supercls) { addSubclass(supercls, cls); } else { addRootClass(cls); } // Attach categories // Assign values to the rW method list, attribute list, and protocol list methodizeClass(cls); return cls; }; Copy the code
The data() method in the class_data_bits_t structure fetches the class_rw_t pointer, and the data() method in the class_data_bits_t structure fetches the class_rw_t pointer. The ro pointer in the class_rw_t structure points to class_ro_t. Figure is as follows:
-
But how are class methods found and called? Since we already know that in ObjC, classes are really special objects, and methods that find classes actually use the same mechanism as methods that find instances, how do we get them to use the same mechanism? This is where the metaclass comes into play.
- MetaClass ensures that the Class also has a pointer to the Class type, ensuring consistency between the Class and the object, and ensuring that the mechanism for finding the methods of the Class is in sync with the mechanism for finding the methods of the object.
- When the instance method is called, the object’s
isa
Gets the implementation of a method in a class - When a class method is called, it passes through the class
isa
Get the implementation of a method in a metaclass
- When the instance method is called, the object’s
- MetaClass ensures that the Class also has a pointer to the Class type, ensuring consistency between the Class and the object, and ensuring that the mechanism for finding the methods of the Class is in sync with the mechanism for finding the methods of the object.
And now, finally, our point,What exactly is isa?
-
We can see in the Runtime source code that the community allocates different bits of memory on different processors.
union isa_t { isa_t() {}isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; #if SUPPORT_PACKED_ISA // extra_rc must be the MSB-most field (so it matches carry/overflow flags) // nonpointer must be the LSB (fixme or get rid of it) // shiftcls must occupy the same bits that a real class pointer would // bits + RC_ONE is equivalent to extra_rc + 1 // RC_HALF is the high bit of extra_rc (i.e. half of its range) // future expansion: // uintptr_t fast_rr : 1; // no r/r overrides // uintptr_t lock : 2; // lock for atomic property, @synch // uintptr_t extraBytes : 1; // allocated with extra bytes # if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL 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; # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) }; # elif __x86_64__ # define ISA_MASK 0x00007ffffffffff8ULL # define ISA_MAGIC_MASK 0x001f800000000001ULL # define ISA_MAGIC_VALUE 0x001d800000000001ULL struct { uintptr_t nonpointer : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000 uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 8; # define RC_ONE (1ULL<<56) # define RC_HALF (1ULL<<7) }; }; Copy the code
Take x86_64_ as an example.
-
Take a deeper look at the isa structure from isa initialization
inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) { initIsa(cls, true, hasCxxDtor); } inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) { if(! indexed) { isa.cls = cls; }else { isa.bits = ISA_MAGIC_VALUE; isa.has_cxx_dtor = hasCxxDtor; isa.shiftcls = (uintptr_t)cls >> 3; }}// Since the isa of the object is initialized to a indexed value of true, it can be simplified to inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) { // ISA_MAGIC_VALUE is 0x000001a000000001ULL isa.bits = ISA_MAGIC_VALUE; isa.has_cxx_dtor = hasCxxDtor; isa.shiftcls = (uintptr_t)cls >> 3; } Copy the code
-
In this case, the isa.bits = ISA_MAGIC_VALUE command is executed. The post-ISA structure is, you can see that ISA_MAGIC_VALUE initializes both Magic and Indexed
-
Isa.has_cxx_dtor = hasCxxDtor; This bit sets the value of has_cxx_dtor, which if 1 indicates whether the current object has a destructor, and if not, it is quickly released
-
Isa.shiftcls = (uintptr_t) CLS >> 3; The main reason for moving the last three bits of the Class pointer to the shiftcls bits assigned to the current object is to clearly reduce memory consumption for the last three useless bits of the Class pointer, since the last three bits of the Class pointer are all meaningless zeros in order to align memory in bytes (8 bits). After the assignment is as follows
This confirms our previous analysis of the initIsa method when initializing ISA. It sets indexed, Magic, and Shiftcls.
-
Access to the isa
-
Since we are using the isa_t structure instead of a pointer to Class, we also need a pointer that returns the Class to which ISA refers, so we need an isa () method.
inlineClass objc_object::ISA() { assert(! isTaggedPointer());#if SUPPORT_INDEXED_ISA if (isa.nonpointer) { uintptr_t slot = isa.indexcls; return classForIndex((unsigned)slot); } return (Class)isa.bits; #else return (Class)(isa.bits & ISA_MASK); #endif } // Simplify as follows inlineClass objc_object::ISA() { assert(! isTaggedPointer());ISA() returns isa.bits with 0x0000000ffffffff8ULL, and does return the current class return (Class)(isa.bits & ISA_MASK); } Copy the code
conclusion
- At this point, the first part of the source analysis is over, if you find any problems and deficiencies welcome to discuss with me and advice.
The resources
- Apple source
- ObjC The past life of an object
- ObjC messaging mechanism, what happens behind the code