preface

In the lastIOS to explore the bottom – CAhCE_T primaryIn the article, we exploredcache_tThe basic structure and how to insert data, today’s article, we continue to explorecache_tThe cached data, when it was read. In the source code, search globally for insert methodscache_t::insertWe’ll find comments like thisYou can see that apple’s comments are very clear, and the cache reads using this methodobjc_msgSendandcache_getImpTwo methods. Before exploring them, let’s add some run-time knowledge

The Runtime Runtime

Compile time

As the name implies, while compiling. So what does compilation mean? In effect, the compiler translates the source code into code that the machine can recognize (Ps. Of course, this is only in the general sense, but may actually be translated into some intermediate language, such as assembly). That compilation is simply to do some translation work, such as check whether there are wrong keywords, lexical analysis, grammatical analysis and other processes, in fact, is static check whether you have typos and language problems. We often use Xcode, in the compilation of warnings and errors, are checked out by the compiler. This type of error is called a compile-time error, and the type checking in this process is also called compile-time type checking or static type checking. So sometimes some people say that the compile time also allocates memory and so on, must be wrong!

The runtime

The code is running, it’s loaded into memory. Runtime type checking is different from compile-time type checking (static type checking). It’s not simply scanning the code for problems, but doing some operations in memory, making some judgments.

Runtime comes in two versions:

  • Programming interfaces for earlier versions:Objective - 1.0 Cfor32-bit Mac OS X platformOn;
  • Programming interface of current version:Objective - 2.0 CforThe iPhone programandMac OS X V10.5 and later64-bit program in the system

(Reference Source Objective-C Runtime Programming Guide)

There are three ways to initiate Runtime

  1. Method calls through OC: for example[p sayBye]
  2. Apis provided via NSObject: for exampleisKindofClass
  3. Apis provided by the underlying layer: for exampleclass_getInstanceSize

Let’s take 🌰 and continue with our old friend DMPerson

#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface DMPerson : NSObject
- (void)sayBye;
- (void)eat;
@end

@implementation DMPerson

- (void)sayBye {
    NSLog(@"bye");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        DMPerson *p = [[DMPerson alloc] init];
        [p sayBye];
        [p eat];
    }
    return 0;
}
Copy the code

First DMPerson declares two methods sayBye and eat, and then we run it

libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DMPerson eat]: unrecognized selector sent to instance 0x10072c740'
terminating with uncaught exception of type NSException
Copy the code

Of course, it crashed because we didn’t implement EAT, we had no problem with type checking at compile time, but when we ran it, we found that we didn’t implement EAT, so we threw an exception. Next, let’s use clang to convert to the underlying C/C++ code to see the implementation at method call time.

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        DMPerson *p = ((DMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((DMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DMPerson"),
        sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayBye"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));
    }
    return 0;
}
Copy the code

From the underlying code, it is obvious that our method calls in OC end up being converted to objc_msgSend in the underlying code, so we arrive at a conclusion

The invocation of the OC method is essentially the process of sending messages

Now that we know what it is, we try to call the method with objc_msgSend, implement the eat method, and modify the code in the Main function as follows

We found that the compiler gave us an error,objc_msgSendMethod has too many parameters. Let’s change the compiler conditions

Change Build Settings Enable Strict Checking of objc_msgSend Calls to NO, default to YES;

In the arm64 architecture of real and M1 computers, it is useless to modify the parameters, so we cast objc_msgSend to ((void (*)(id, SEL))(void *)objc_msgSend).

After making the changes, let’s look at the print

The successful printing shows that we can call the method successfully using this method.

Method’s quick lookup process

Objc_msgSend = msgsend; objc_msgSend = msgsend = msgsend

We can see 644 results in 22 files that we were usingobjc_msgSendIs required to import the header filemessage.h, so we looked inside the first, the result found only the declaration of the function, and no function implementation, so we can only find in the lower level of the place, and finally we lockedobjc-msg-arm64.sFile, to.sThe end is all written in assembler files, andarm64It’s our real machine architecture

I found it at the end of itENTRY _objc_msgSendThat is to enter_objc_msgSendSo we go down, line by line

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
        //**p0 is the first argument we pass through the _objc_msgSend method, which is the caller itself **
	cmp	p0, #0			// nil check and tagged pointer check
        //** Check whether it is tagged pointer **
#if SUPPORT_TAGGED_POINTERS
        //** If it is tagged pointer, determine whether the CMP value is less than or equal to 0**
        //** directly jump to LNilOrTagged: this line, otherwise directly go down **
	b.le	LNilOrTagged		// (MSB tagged pointer looks negative)
#else
        //** If it is not tagged pointer, check whether the CMP value is 0**
        //** jump to LReturnZero if = LReturnZero, otherwise go down **
	b.eq	LReturnZero
#endif
        P13 = isa**; // p13 = isa**
	ldr	p13, [x0]	
        //** get the class from p13 or isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
        //** finds the method in the cache and calls ** if it finds it
        //** otherwise call __objc_msgSend_uncached **
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
        
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	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

This part of the code is actually checking if the caller we call _objc_msgSend is empty, and if it is empty, we’ll call the method directly and return it. Otherwise, the isa pointer to the caller is stored in register P13. The most important method is GetClassFromIsa_p16, which gets the class from ISA. Let’s focus on that

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if ! needs_auth */
#if SUPPORT_INDEXED_ISA
	// Indexed isa
	mov	p16, \src			// optimistically set dst = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa
	// isa in p16 is indexed
	adrp	x10, _objc_indexed_classes@PAGE
	add	x10, x10, _objc_indexed_classes@PAGEOFF
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:

// ** ** ** ** ** *
#elif __LP64__
// If needs_auth is equal to 0, forget what it means for the moment. In objc_msgSend, 1 is passed in
.if \needs_auth == 0 // _cache_getImp takes an authed class already
        / ** place SRC, our input parameter isa pointer, in register P16 **
	mov	p16, \src
.else
	// 64-bit packed isa
        / / * * called ExtractISA * *
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif
.endmacro

//** This method has two implementations, one is for mobile phones with A12 chip or higher, we look at ** below A12 chip
.macro ExtractISA
        //** isa bitwise sum of the isa and isa_mask parameters passed to the object, i.e. Class**
	and    $0, $1, #ISA_MASK
.endmacro
Copy the code

So this, in a very simple way, is just passing the object’s ISA to GetClassFromIsa_p16 and then this method does different things for different ISA types to get the Class object. And down here is the CacheLookup method, so let’s keep going

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
/ ** Save isa from x16 to x15 as a backup **
	mov	x15, x16			// stash the original isa
LLookupStart\Function:
CACHE_MASK_STORAGE ==CACHE_MASK_STORAGE_HIGH_16**
//** For easy reading, I have deleted the code for other schemas **
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        //** p1 = SEL, p16 = isa**
        //** shift x16 (isa) by #CACHE to p11 register **
        #define CACHE=(2 * __SIZEOF_POINTER__)**
        //** so the isa is shifted by 2*8 positions, which we know from the previous class structure is the first address of the cache, which is _bucketsAndMaybeMask **
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
        //** In real environment, CONFIG_USE_PREOPT_CACHES==1**
#if CONFIG_USE_PREOPT_CACHES
        //** Whether to use A12 chip or higher **
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
        //**_bucketsAndMaybeMask is bitted with the upper 0x0000FFFFfffffffe **
        // We talked about buckets in the previous article
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
        //** If the 0th bit of p11 is not equal to 0, jump to LLookupPreopt\Function, which is generally equal to 0**
	tbnz	p11, #0, LLookupPreopt\Function
#endif
        _cmd ^ (_cmd >> 7))**
	eor	p12, p1, p1, LSR #7
        //**p11 moves 48 bits to the right to obtain the higher 16 bits of the value, i.e., mask, and p12 x12 = x12&mask **
        // select * from index (*)
	and	p12, p12, p11, LSR #48 
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
Copy the code

We get buckets and masks from our cache_t, which we talked about earlier. And then you keep going down

        PTRSHIFT = buckets; buckets index = 16 bytes; buckets index = 15
        //** to find the location where we hash out the storage method **
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
        
	P17 =IMP,p9=SEL**
        //** then shift x13 to the left by BUCKET_SIZE bytes, pointing to the previous bucket**
                                                //do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
        //** Compare p9 with SEL** we passed in
	cmp	p9, p1				// if (sel ! = _cmd) {
        //** if not, jump to the 3 tag and continue to execute **
	b.ne	3f				// scan more
	//** cache hit ** // else {
2:	CacheHit \Mode				// hit: call or return imp
                                                / /}
	//** if p9's SEL is 0, then MissLabelDynamic** is executed
3:	cbz	p9, \MissLabelDynamic		// if (sel == 0) goto Miss;
        /** buckets(p13); // buckets(p10); // Buckets (p13)
	cmp	p13, p10			// } while (bucket >= buckets)
        Bucket (p10) = buckets(p13); // Buckets (p10) = buckets(p13)
	b.hs	1b

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        //**p11 moves 48-4 to the right (maskZeroBits), and the result is that the mask moves 4 ** to the right
        //** is actually a mask*16 bytes **
        Buckets (p10); // Buckets (p10)
        //** is the location of the last bucket in p13 **
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
        // p13 = buckets + (mask << 1+PTRSHIFT)
	// see comment about maskZeroBits
        //** Store the next buket into the p12 **
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
        // p12 = first probed bucket
        P17 =IMP,p9=SEL**
        //** then shift x13 to the left by BUCKET_SIZE bytes, pointing to the previous bucket**
						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
        //** Compare the obtained p9 (SEL) with the passed P1 (SEL)**
	cmp	p9, p1				// if (sel == _cmd)
        // If ** is equal, the cache hits **
	b.eq	2b				// goto hit
        //** if p9(SEL) is 0**
	cmp	p9, #0				// } while (sel ! = 0 &&
        //** Compare p13(the previous bucket of the current bucket) with P12 (the last bucket of the initial location)**
	ccmp	p13, p12, #0, ne		// bucket > first_probed)
        //** is not 0, and p13 is greater than p12, continue to mark bit 4, the cycle continues **
	b.hi	4b
Copy the code

This section is the core of the code for finding the cache, loping to find the location of the method in the cache, calling CacheHit \Mode if found, MissLabelDynamic otherwise. Let’s look at the implementation of CacheHit

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
/ / * * objs_msgSend $0 = = NORMAL * *
.if $0 == NORMAL
        // Call the found method
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, 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, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 ! = x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
.endmacro

//** still has two implementations, let's look at ** below A12
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
        //**$0(imp) ^ $3(isa)**
        //** is actually a decoding process **
	eor	$0, $0, $3
        //** jump to $0(imp) address, which is called imp **
	br	$0
.endmacro
Copy the code

Relatively simple implementation, is to call SEL to find the corresponding IMP, the implementation of the call method. At this point, objc_msgSend calls the method, and the process of finding the method in the cache is complete, which is also known as the quick method finding process

conclusion

In this article, we’ve moved from the cache_t method insertion in the class structure to method reads, where we learned about the objc_msgSend method, and about Runtime Runtime concepts and three ways to call it. Finally, we explored how to quickly find our method in the cache during the call to objc_msgSend. That is, how to quickly find our method in the cache