OC is a dynamic language, which has three characteristics of dynamic language: dynamic typing, dynamic binding and dynamic loading. The underlying support for all of this is the mysterious and familiar Runtime! All methods sent in OC are based on message mechanism. What is message mechanism? How is the method implemented? How does Runtime implement dynamic decision making? Let’s explore!

Links to the Runtime messaging series

[I] Analysis of the process of quick search for Runtime messages

[2] Analysis of Runtime message slow search process

[c] Runtime dynamic method resolution and message forwarding

What is Runtime?

Runtime is Runtime by name. He provides the underlying dynamic support of Objective-C language, including dynamic typing, dynamic binding, dynamic loading features, so that the code has run time, dynamic characteristics.

1, Runtime features (official documentation introduction translation)

  • ① Defer as many decisions as possible from compile time and link time to run time
  • ② The runtime system acts as the operating system for object-C, which enables the language to work

2. What is the use of Runtime?

2.1, How to use Runtime in object-c

Objective-c programs interact with the runtime system at three different levels:

  • Interact with methods defined in the NSObject class, such as performSelector:with…
  • Interaction via object-c code for example: [Person say]
  • By directly calling Runtime functions (Runtime API), for example: objc_msgSend(……)

2.2 Basic usage methods of Runtime

  • In the process of program running, dynamically create classes, dynamically add, modify the attributes and methods of this class;
  • Iterate over all member variables, attributes, and methods in a class
  • Message passing and forwarding

3. Usage scenarios of Runtime

3.1 In development, some common scenarios of Runtime are as follows:

  • Add attributes and methods to system classes

  • Methods exchange

  • Gets the properties, private properties of the object

  • Dictionary transformation model

  • KVC, KVO

  • Archiving (encoding, decoding)

  • NSClassFromString class interconverts with string

  • block

  • Class self-detection

  • .

Runtime and Objective-C methods

Above we briefly introduced the concepts and basic usage of Runtime. Given the large body of knowledge of Runtime, this is only an introduction to the nature of the method. What is the nature of ————?

Second, the nature of the method

In the nature of the class, we know that the underlying implementation of OC is C/C++ and assembly code, we can use apple to host LLVM clang compiler, restore our class files to CPP files, and then go to analyze the specific implementation, just do it, let’s start!

2.1. Introduce the low-level implementation of the class through a demo

  • ① First, we create a class that contains instance methods

@interface YYTeacher : NSObject
- (void)sayHello;
@end

@implementation YYTeacher
- (void)sayHello{
    NSLog(@"666 %s",__func__);
}
@end



@interface YYPerson : YYTeacher
- (void)sayHello;
- (void)sayNB;
@end

@implementation YYPerson
- (void)sayNB{
    NSLog(@"666");
}
@end
Copy the code
  • ② Then, in main.m, the class is called and the method is implemented.
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        YYPerson *person = [YYPerson alloc];
        YYTeacher *teach = [YYTeacher alloc];

        [person sayNB];
        [person sayHello];
        
       
        NSLog(@"Hello, World!");
    }
    return 0;
}
Copy the code
  • ③ Through the main function compiled by Clang
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        YYPerson *person = ((YYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YYPerson"), sel_registerName("alloc"));
        YYTeacher *teach = ((YYTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YYTeacher"), sel_registerName("alloc"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_vk_kp1ndygs685bhx1m2rpsrnx40000gn_T_main_4539a5_mi_2);
    }
    return 0;
}
Copy the code
  • ④ Let’s simplify the implementation
YYPerson *person = ((YYPerson *(*)(id, SEL))(void *)objc_msgSend)

((id)objc_getClass("YYPerson"), sel_registerName("alloc"));    

objc_msgSend)(person, sel_registerName("sayNB"));      

objc_msgSend)(person, sel_registerName("sayNB:"), __NSConstantStringImpl__var_folders_vk_kp1ndygs685bhx1m2rpsrnx40000gn_T_main_4539a5_mi_2);
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

Copy the code

2.2. Core method objc_msgSend

After the above analysis, we found that the message is sent through the objc_msgSend method, then we focus on the implementation process of objc_msgSend method.


objc_msgSend(Receiver of the message, body of the message (SEL + argument))Copy the code

Objc_msgSend Runtime source code

1. Objc_msgSend definition

In c++ source code, objc_msgSend is defined like this, but the implementation is not found. We imagine that it is implemented in assembly, and then we look for its implementation.

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... * / )
    OBJC_AVAILABLE(10.0.2.0.9.0.1.0.2.0);

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... * / )
    OBJC_AVAILABLE(10.0.2.0.9.0.1.0.2.0);
Copy the code

2, objc_msgSend core implementation assembly


	//---- message sending -- assembler entry --objc_msgSend is mainly to get the recipient's ISA information
ENTRY _objc_msgSend 
/ / -- no window
	UNWIND _objc_msgSend, NoFrame 
	
//---- p0 and null comparison to determine whether the receiver exists, where p0 is the first parameter of objc_msgSend - message receiver
	cmp	p0, #0			// nil check and tagged pointer check 
//---- le is less than -- supports taggedPointer (small object type) processes
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		// (MSB tagged pointer looks negative)
#else
//---- If p0 is 0, null is returned
	b.eq	LReturnZero 
#endif 
//---- p0 is the process that must exist at receiver
//---- fetch ISA according to the object, i.e. fetch ISA from the address pointed to by register x0 and store it in register p13
	ldr	p13, [x0]    	// p13 = isa 
P16 = isa (p13) & ISA_MASK (p16 = isa (p13) & ISA_MASK
	GetClassFromIsa_p16 p13		// p16 = class 
LGetIsaDone:
	// calls imp or objc_msgSend_uncached 
//---- if you have isa, go to CacheLookup, the CacheLookup process, also known as sel-imp quick lookup process
	CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//---- equals null, return null
	b.eq	LReturnZero		// nil check 

	// tagged
	adrp	x10, _objc_debug_taggedpointer_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
	ubfx	x11, x0, #60#,4
	ldr	x16, [x10, x11, LSL #3]
	adrp	x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
	add	x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
	cmp	x10, x16
	b.ne	LGetIsaDone

	// ext tagged
	adrp	x10, _objc_debug_taggedpointer_ext_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
	ubfx	x11, x0, #52#,8
	ldr	x16, [x10, x11, LSL #3]
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend


Copy the code

3, analysis of the above source code assembly, mainly through the following process:

3.1 [Step 1]

Check whether the objc_msgSend method’s first argument, receiver, is null

If tagged Pointer is supported, switch to LNilOrTagged

If the small object is empty, it returns null, LReturnZero.

If the small object is not empty, then the isa of the small object is processed. Go to step 2.

If neither is a small object, and receiver is not empty, there are two steps:

Fetch ISA from the receiver and store it in the P13 register

In GetClassFromIsa_p16, isa & ISA_MASK is used to obtain the shiftCls bit-field class information under arm64 architecture, namely, the assembly of GetClassFromIsa_p16 is implemented as follows, and then go to [second step].

.macro GetClassFromIsa_p16 /* src */
//---- used here for watchOS
#if SUPPORT_INDEXED_ISA 
	// Indexed isa
//---- stores the value of isa into register P16
	mov	p16, $0			// optimistically set dst = src 
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer ISA
	// isa in p16 is indexed
//---- reads the base address of the page where _objc_indexed_classes is located into the X10 register
	adrp	x10, _objc_indexed_classes@PAGE 
//---- x10 = x10 + _objc_indexed_classes(offset in page) --x10 base addresses are memory offset based on the offset
	add	x10, x10, _objc_indexed_classes@PAGEOFF
//---- from the ISA_INDEX_SHIFT bit of P16, extract the ISA_INDEX_BITS bits into register P16, and fill the remaining high bits with zeros
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index 
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:

//-- for 64-bit systems
#elif __LP64__ 
	// 64-bit packed isa
//---- p16 = class = isa & ISA_MASK
	and	p16, $0, #ISA_MASK 

#else
	// 32-bit raw isa ---- for 32-bit systems
	mov	p16, $0

#endif

.endmacro

Copy the code

3.2 [Step 2]

After obtaining ISA, the slow search process CacheLookup NORMAL is entered


//!!!!!!!!! Key !!!!!!!!!!!!
.macro CacheLookup 
	//
	// Restart protocol:
	//
	// As soon as we're past the LLookupStart$1 label we may have loaded
	// an invalid cache pointer or mask.
	//
	// When task_restartable_ranges_synchronize() is called,
	// (or when a signal hits us) before we're past LLookupEnd$1,
	// then our PC will be reset to LLookupRecover$1 which forcefully
	// jumps to the cache-miss codepath which have the following
	// requirements:
	//
	// GETIMP:
	// The cache-miss is just returning NULL (setting x0 to 0)
	//
	// NORMAL and LOOKUP:
	// - x0 contains the receiver
	// - x1 contains the selector
	// - x16 contains the isa
	// - other registers are set as per calling conventions
	//
LLookupStart$1:

//---- p1 = SEL, p16 = isa -- #define CACHE (2 * __SIZEOF_POINTER__), __SIZEOF_POINTER__
/ / - p11 = mask | buckets - from x16 16 bytes (isa) in translation, the cache is just remove the cache in the p11 register - isa distance 16 bytes: Isa (8 bytes) -superClass (8 bytes) - Cache (mask 16 bits lower than buckets 48 bits lower)
	ldr	p11, [x16, #CACHE]				
//---- 64-bit true machine
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 
// p11(cache) &0x0000FFFFFFFFFFFF, 16 bits of mask are erased, and buckets are stored in p10 register
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets 
	
P11 (cache), p11(cache), p11(cache), p11(cache) The index of sel-IMP is obtained and stored in P12 (sel & mask is used for cache insert).
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask 

//-- non-64-bit true machine
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11				// p11 = mask = 0xffff >> p11
	and	p12, p1, p11				// x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

P12 is the subscript p10 is the first address of buckets array. The subscript * 1<<4(that is, 16) is used to obtain the offset of the actual memory
// LSL #(1+PTRSHIFT); // LSL #(1+PTRSHIFT)
	add	p12, p10, p12, LSL #(1+PTRSHIFT)   
		             // buckets = buckets + ((_cmd & mask) << (1+PTRSHIFT)
		             
// select sel from p17 (select SEL from P9)
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket 
	
//-- compare sel with p1 (CMD)
1:	cmp	p9, p1			// if (bucket->sel ! = _cmd)
//-- If not, that is, not found, please jump to 2f
	b.ne	2f			// scan more
// if it is equal, a cacheHit is hit, and imp is returned
	CacheHit $0			// call or return imp 
	
2:	// not hit: p12 = not-hit bucket
// if not, go to __objc_msgSend_uncached because it is normal
	CheckMiss $0			// miss if bucket->sel == 0 
Check whether p12 (bucket) equals p10 (buckets). If so, go to step 3
	cmp	p12, p10		// wrap if bucket == buckets 
//-- Locate the last element (the first bucket)
	b.eq	3f 
/ / - from x12 (p12 buckets first address) - actual need translation memory size BUCKET_SIZE, get the second element of the bucket, imp - sel in p17 - p9 respectively, namely, look ahead
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket 
//-- jump to step 1 and continue to compare sel and CMD
	b	1b			// loop 

3:	// wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//-- manually set to the last element
If p11 (mask) is moved 44 bits to the right, it is the same as if mask is moved 4 bits to the left. P11 (mask) is directly located to the last element of buckets
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT)) 
					// p12 = buckets + (mask << 1+PTRSHIFT) 
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p12, p12, p11, LSL #(1+PTRSHIFT)
					// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.
//-- find the cache again ()
P17-p9 p17-SEL p17-P9 p17-SEL p17-P9
	ldp	p17, p9, [x12]		// {imp, sel} = *bucket 
	
//-- compare sel with p1 (CMD)
1:	cmp	p9, p1			// if (bucket->sel ! = _cmd)
// if not, go to the second step
	b.ne	2f			// scan more
//-- if the match is hit, return IMP directly
	CacheHit $0			// call or return imp  
	
2:	// not hit: p12 = not-hit bucket
// if not found, then CheckMiss
	CheckMiss $0			// miss if bucket->sel == 0 
P12 (bucket) = p10 (buckets); p10 (buckets) = p10 (buckets)
	cmp	p12, p10		// wrap if bucket == buckets 
	b.eq	3f // If equals, go to step 3
/ / - from x12 (p12 buckets first address) - actual need translation memory size BUCKET_SIZE, get the second element of the bucket, imp - sel in p17 - p9 respectively, namely, look ahead
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket 
//-- jump to step 1 and continue to compare sel and CMD
	b	1b			// loop 

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
// Jump to JumpMiss because it is normal and to __objc_msgSend_uncached

	JumpMiss $0 
.endmacro

// The following is the assembler function for the last jump
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x12, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x12, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x12, x1, x16	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP 
//-- if GETIMP, jump to LGetImpMiss
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL 
//--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncached
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP 
//-- If LOOKUP, skip to __objc_msgLookup_uncached
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro


Copy the code

3.3, CacheLookup CacheLookup assembly source code

It is mainly divided into the following steps

In objc_class, the header address is exactly 16 bytes away from the cache, i.e. isa is 8 bytes and superClass is 8 bytes. In the cache, the top 16 bits are used to store mask and the bottom 48 bits to store buckets. The p11 = cache

[Step 2] Fetch buckets and mask from the cache, and then mask calculates the hash subscript according to the hash algorithm

The & operation of cache and mask (0x0000FFFFFFFFFFFF) is used to erase the 16-bit mask to get the address of buckets pointer, that is, P10 = buckets

Move the cache 48 bits to the right to obtain mask, that is, P11 = mask

Select * from objc_msgSend (p1) and msak (_cmd) to hash the sel-IMP bucket index (p12 = index = _cmd & mask). Since sel-IMP is stored by calculating the hash subscript using the same hash algorithm, it also needs to be read in the same way, as shown below

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}
Copy the code

[Step 3] Fetch the bucket corresponding to index and buckets according to the first address of their hash indexes

Bucket_t: sel = 8 bytes; IMP = 8 bytes; PTRSHIFT = 3

Multiply the hash subscript index by the memory size occupied by a bucket to obtain the offset of the starting address of buckets in the actual memory

Obtain the bucket corresponding to hash index by starting address + actual offset

[Step 4] According to the obtained bucket, remove SEL and store p17, that is, P17 = SEL, remove IMP and store P9, that is, P9 = IMP

[Step 5] The first recursive loop

Compare sel in the obtained bucket with the _cmd(p1) of the second parameter of objc_msgSend

If they are equal, we jump directly to CacheHit, which is a CacheHit, and imp is returned

If they are not equal, there are two situations

If not, skip to CheckMiss, because $0 is normal, then skip to __objc_msgSend_uncached

If the bucket obtained according to index is equal to the first element of buckets, the bucket is set as the last element of Buckets (by moving the first address of buckets +mask 44 places to the right (the same as moving the mask 4 places to the left) to the last element of bucker. Then continue the recursive loop (the first recursive loop nested the second recursive loop), i.e. [Step 6]

If the current bucket is not equal to buckets’ first element, the search continues and the first recursive loop is entered

[Step 6] The second recursive loop: If the bucket is the same as the first element of buckets, we jump to JumpMiss ($0 = normal, __objc_msgSend_uncached). Enter the slow search process

3.4 The following is the change process of the value of the whole quick search process