Write in the front: the exploration of the underlying principles of iOS is my usual development and learning in the continuous accumulation of a step forward road. I hope it will be helpful for readers to record my journey of discovery.Copy the code

The directory is as follows:

  1. Exploring the underlying principles of iOS alloc
  2. Exploration of the underlying principles of iOS structure in vivo alignment
  3. The nature of the object explored by iOS underlying principles & the underlying implementation of ISA
  4. The underlying principles of the ISA-class (part 1)
  5. The underlying principles of the ISA class (middle)
  6. The underlying principles of isA-Class (part 2)

Summary column for the above

  • Summary of the phase of iOS underlying principle exploration

Runtime

Objective-c deferred as many decisions as possible from compile time and link time to run time. Whenever possible, it does things dynamically. This means that the language requires not only a compiler, but also a runtime system to execute the compiled code. The runtime system acts as an operating system for objective-C; This is what makes language work.

Versions and platforms

There are two versions of objective-C runtime — Modern and Legacy. The modern version was introduced in Objective-C 2.0 and includes many new features. The programming interface for the older Runtime is described in the Objective-C 1 Runtime Reference; The programming interface for the modern version of the Objective-C runtime is described in the Objective-C Runtime Reference.

IPhone applications and 64-bit programs on OS X V10.5 and later use modern versions of the runtime. Other programs (32-bit programs on the OS X desktop) use older versions of the runtime.

Compile time

If there is a run time, there is a compile time, which as the name implies is compiling time. So what is compiling? The compiler translates your source code into machine-readable code.

Interact with the runtime

Objective-c programs interact with the runtime system at three different levels: through objective-C source code; Methods defined through classes in the NSObjectFoundation framework; And by calling the runtime function directly.

Objective – C source code

In most cases, the runtime system runs automatically behind the scenes. You can use it simply by writing and compiling objective-C source code.

When you compile code that contains objective-C classes and methods, the compiler creates data structures and function calls that implement the language’s dynamic features. Data structures capture information found in class and category definitions and protocol declarations; They include class and protocol objects discussed in defining classes and protocols in the Objective-C programming language, as well as method selectors, instance variable templates, and other information extracted from source code. The main runtime functions are those that send messages, as described in Messaging. It is invoked by a source code message expression.

NSObject methods

Most objects in Cocoa are subclasses of NSObject, so most objects inherit the methods it defines. (The notable exception is the NSProxy class; See Message forwarding for more information.) So, its methods establish behavior that is inherent to every instance and every class object. In rare cases, however, the NSObject class simply defines a template to say what should be done; It does not provide all the necessary code by itself.

For example, the NSObject class defines a Description instance method that returns a string describing the contents of the class. This is mainly used for debugging — gdbprint-object to print the string returned from this method. NSObject the implementation of this method doesn’t know what the class contains, so it returns a string with the name and address of the object. A subclass of NSObject can implement this method to return more details. For example, the Foundation class NSArray returns a description list of the objects it contains.

Some NSObject methods simply query information about the runtime system. These methods allow an object to perform introspection. An example of this method is the class method, which requires an object to recognize its class; IsKindOfClass: and isMemberOfClass:, the position of the test object in the inheritance hierarchy; RespondsToSelector:, which indicates whether an object can accept a particular message; ConformsToProtocol: Indicates whether an object claims to implement methods defined in a specific protocol. And methodForSelector:, which provides the address of the method implementation. Methods like this enable the object to reflect on itself.

Runtime function

The runtime system is a dynamic shared library with a common interface made up of /usr/include/objc, a set of functions and data structures in a header file in a directory. Many of these functions allow you to use plain C to copy what the compiler does when writing objective-C code. The rest forms the basis of the functionality exported through the methods of the NSObject class. These capabilities make it possible to develop additional interfaces to the runtime system and generate tools that enhance the development environment; You don’t need them to program in Objective-C. However, some runtime functions can sometimes be useful when writing objective-C programs. All of these functions are documented in the Objective-C runtime reference.

finishing

We can set up Runtime in these three ways. The invocation relationship in the figure above is a top-down invocation.

explore

Case Code 1

int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... SMPerson *person = [SMPerson alloc]; OC [person doWork:@"it"]; // Call the method Framework [Person performSelector:@selector(gotoTalk)]; } return 0; }Copy the code

After running xcrun, we get the following:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
        SMPerson *person = ((SMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("SMPerson"), sel_registerName("alloc"));

        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("doWork:"), (NSString *)&__NSConstantStringImpl__var_folders_w3_ptw7ymvs5pjf98xqvjnp86wc0000gn_T_main_2c42e2_mi_0);

        ((id (*)(id, SEL, SEL))(void *)objc_msgSend)((id)person, sel_registerName("performSelector:"), sel_registerName("say666"));
    }
    return 0;
}
Copy the code

We can see that at the OC level, the execution of any method, which at the bottom is a message sending process, calls objc_msgSend with two arguments, one is the method recipient and the other is the message body (sel + argument).

If we want to use objc_msgSend in our code we need the following Settings:

  1. Xcode: target -> Build Settings -> App Clang Perprocessing ->Enable Strict Checking of objc_msgSend callsSet toNOThis means that objc_msgSend is not checked by the compiler, leaving you free to use objc_msgSend in your code.
  2. Import the Runtime library in a file, i.e#import <objc/message.h>

Let’s look at another example:

Case Code 2

SMTeacher *teacher = [SMTeacher alloc];
        
[teacher gotoTalk];
        
struct objc_super sm_objc_super;
sm_objc_super.receiver = teacher;
sm_objc_super.super_class = SMPerson.class;
     
objc_msgSendSuper( &sm_objc_super, @selector(gotoTalk) );

Copy the code

Console output

SMObjcBuild[7319:365018] 12 it
SMObjcBuild[7319:365018] -[SMPerson gotoTalk]
SMObjcBuild[7319:365018] -[SMPerson gotoTalk]
SMObjcBuild[7319:365018] -[SMPerson gotoTalk]
Copy the code

In both cases, we called the method directly from the person instance, and we called the method using performSelector from the instance, Finally, objc_msgSendSuper is used to send a message to SMTeacher, a subclass of SMPerson, which can eventually output what the method prints.

conclusion

  1. The methods we call at the OC level, when compiled, correspond to calls to the objc_msgSend function;
  2. Super_class sets receiver, which is the first responder when the method is called.
  3. Through this process of exploration, we come to a conclusionThe essence of a method call is message sending.

extension

Objc_msgSend Assembler analysis

  1. CMP p0, #0: p0 is the recipient of the message and is compared with 0 to determine whether the recipient is 0. If there is no recipient, then objc_msgSend is meaningless.

  2. If support_tagged_sailors were of the support_tagged_sailors type, go to B.tagged. If support_tagged_sailors type is not tagged, go to B.tagged Zero. This message is returned as null.

  3. LDR p13, [x0] stores x0 to P13. X0 is receiver, that is, the class, that is, the first address of the class, that is, isa, that is, p13= ISA.

  4. GetClassFromIsa_p16 enter SRC =p13, needs_auth=1, auth_address=x0. If SUPPORT_INDEXED_ISA (32-bit ISA) is not met, it will go to the __LP64__ (Mac OS X in this source code) branch.

  5. Because _need_auth = 1, into the branch ExtractISA p16,, \ SRC \ auth_address, the macro ExtractISA operation is to \ SRC (isa), # ISA_MASK do with operation, obtained the Class, as a result, deposit to the p16.

  6. LGetIsaDone: Obtaining ISA is complete. Next, run CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

Next, we continue the objc_msgSend compilation analysis where we left off in Step 6.

The source code

_objc_msgSend

	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// 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
Copy the code

GetClassFromIsa_p16

.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 == 0 // _cache_getImp takes an authed class already mov p16, \src .else // 64-bit packed isa ExtractISA p16, \src, \auth_address .endif #else // 32-bit raw isa mov p16, \src #endif .endmacroCopy the code

ExtractISA

.macro ExtractISA
	and	$0, $1, #ISA_MASK
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
	xpacd	$0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
	mov	x10, $2
	movk	x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
	autda	$0, x10
#endif
.endmacro
Copy the code

CacheLookup

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart\Function 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\Function,
	//   then our PC will be reset to LLookupRecover\Function 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
	//

	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
#endif
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	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

	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// wrap-around:
	//   p10 = first bucket
	//   p11 = mask (and maybe other bits on LP64)
	//   p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	cmp	p9, #0				// } while (sel != 0 &&
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro
Copy the code

CacheHit

#define NORMAL 0 #define GETIMP 1 #define LOOKUP 2 // CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa .macro CacheHit .if $0 == NORMAL 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 .endmacroCopy the code