iOS底層原理 runtime- objc_msgSend拾遺基礎篇--(7)

fgyong發表於2019-07-18

上篇我們講過了iOS底層原理 runtime-object_class拾遺基礎,arm64之後isa是使用聯合體使用更少的空間儲存更多的資料,以及如何自定義和使用聯合體,objc_class->cache_t cache是一個是快取最近呼叫class的方法,當快取剩餘空間小余1/4則進行擴容,擴容為原來的兩倍,擴容之後,已儲存的method_t擴容之後之後被清空。今天我們在瞭解runtime的訊息轉發機制。

基礎知識

OC中的方法呼叫,其實都是轉換為objc_msgSend函式的呼叫

objc_msgSend的執行流程可以分為3大階段

  1. 訊息傳送
  2. 動態方法解析
  3. 訊息轉發

那麼我們根據這三塊內容進行原始碼解讀。原始碼執行的順序大概如下

objc-msg-arm64.s
ENTRY _objc_msgSend
b.le	LNilOrTagged //<0則返回
CacheLookup NORMAL //快取查詢 未命中則繼續查詢
.macro CacheLookup// 通過巨集 查詢cache,命中直接call or return imp
.macro CheckMiss //miss 則跳轉__objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncached 
.macro MethodTableLookup//方法中查詢
__class_lookupMethodAndLoadCache3//跳轉->__class_lookupMethodAndLoadCache3 在runtime-class-new.mm 4856行


objc-runtime-new.mm
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
getMethodNoSuper_nolock、search_method_list、log_and_fill_cache
cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache
_class_resolveInstanceMethod
_objc_msgForward_impcache


objc-msg-arm64.s
STATIC_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward

Core Foundation
__forwarding__(不開源)
複製程式碼

訊息傳送

objc_msgSend是彙編寫的,在原始碼objc-msg-arm64.s304行,是objc_msgSend的開始,_objc_msgSend結束是351行, 進入到objc_msgSend函式內部一探究竟:

	ENTRY _objc_msgSend // _objc_msgSend 開始
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// 檢查p0暫存器是否是0  _objc_msgSend()第一個引數:self
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		// if le < 0 ->  跳轉到標籤  LNilOrTagged
#else
	b.eq	LReturnZero // if le == 0 ->  跳轉到標籤  LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// 如果==0 -> LReturnZero

	// 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 //return 返回結束掉

	END_ENTRY _objc_msgSend // _objc_msgSend 結束
複製程式碼

objc_msgSend(id,SEL,arg)id為空的時候,跳轉標籤LNilOrTagged,進入標籤內,當等於0則跳轉LReturnZero,進入到LReturnZero內,清除資料和return。不等於零,獲取isa和class,呼叫CacheLookup NORMAL,進入到CacheLookup內部

.macro CacheLookup //.macro 是一個巨集 使用 _cmd&mask 查詢快取中的方法
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp 命中 呼叫或者返回imp
	
2:	// not hit: p12 = not-hit bucket 沒有命中
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// wrap: p12 = first bucket, w11 = mask
	add	p12, p12, w11, UXTW #(1+PTRSHIFT)
		                        // p12 = buckets + (mask << 1+PTRSHIFT)

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0
	
.endmacro
複製程式碼

彙編程式碼左邊是程式碼,右邊是註釋,大概都可以看懂的。 當命中則return imp,否則則跳轉CheckMiss,進入到CheckMiss內部:

.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
複製程式碼

剛才傳的值是NORMAL,則跳轉__objc_msgSend_uncached,進入到__objc_msgSend_uncached內部(484行):

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves
	MethodTableLookup
	TailCallFunctionPointer x17
	END_ENTRY __objc_msgSend_uncached
複製程式碼

呼叫MethodTableLookup,我們檢視MethodTableLookup內部:

.macro MethodTableLookup
	// push frame
	SignLR
	stp	fp, lr, [sp, #-16]!
	mov	fp, sp

	// save parameter registers: x0..x8, q0..q7
	sub	sp, sp, #(10*8 + 8*16)
	stp	q0, q1, [sp, #(0*16)]
	stp	q2, q3, [sp, #(2*16)]
	stp	q4, q5, [sp, #(4*16)]
	stp	q6, q7, [sp, #(6*16)]
	stp	x0, x1, [sp, #(8*16+0*8)]
	stp	x2, x3, [sp, #(8*16+2*8)]
	stp	x4, x5, [sp, #(8*16+4*8)]
	stp	x6, x7, [sp, #(8*16+6*8)]
	str	x8,     [sp, #(8*16+8*8)]

	// receiver and selector already in x0 and x1
	mov	x2, x16
	bl	__class_lookupMethodAndLoadCache3//跳轉->__class_lookupMethodAndLoadCache3 在runtime-class-new.mm 4856行

	// IMP in x0
	mov	x17, x0
	
	// restore registers and return
	ldp	q0, q1, [sp, #(0*16)]
	ldp	q2, q3, [sp, #(2*16)]
	ldp	q4, q5, [sp, #(4*16)]
	ldp	q6, q7, [sp, #(6*16)]
	ldp	x0, x1, [sp, #(8*16+0*8)]
	ldp	x2, x3, [sp, #(8*16+2*8)]
	ldp	x4, x5, [sp, #(8*16+4*8)]
	ldp	x6, x7, [sp, #(8*16+6*8)]
	ldr	x8,     [sp, #(8*16+8*8)]

	mov	sp, fp
	ldp	fp, lr, [sp], #16
	AuthenticateLR
.endmacro

複製程式碼

最終跳轉到__class_lookupMethodAndLoadCache3,去掉一個下劃線就是c函式,在runtime-class-new.mm 4856行, 呼叫了函式lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/);,第一次會初始化clsresolver的值, 中最終跳轉到c/c++函式lookUpImpOrForward,該函式是最終能看到的c/c++,現在我們進入到lookUpImpOrForward內部檢視:

/***********************************************************************
* lookUpImpOrForward.
* initialize==NO 儘量避免呼叫,有時可能也會呼叫。
* cache==NO 跳過快取查詢,其他地方可能會不調過
* 大多數人會傳值 initialize==YES and cache==YES
*   如果cls是非初始化的元類,則非Non-nil會快點
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
* 如果你不想用forwarding,則呼叫lookUpImpOrNil()代替
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();
    // Optimistic cache lookup
    if (cache) { //從彙編過來是NO
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
		//當cls需要初始化和沒有初始化的時候 進行cls初始化,
		//初始化會加入到一個執行緒,同步執行,先初始化父類,再初始化子類
		//資料的大小最小是4,擴容規則是:n*2+1;
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }

    
 retry:    
    runtimeLock.assertLocked();

//再次獲取imp
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    //嘗試在本類中查詢method
    {//從cls->data()->methods查詢method
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {//找到新增到cache中
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
	//從cls->superclass->data()->methods查詢methd,supercls沒有查詢出來,再查詢父類的父類。
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
					//將父類新增到 子類的快取中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

	//如果還沒有找到imp,進入動態方法解析階段
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }

    //如果沒找到resolveInstanceMethod 和resolveClassMethod,
//	進行訊息轉發 階段
    imp = (IMP)_objc_msgForward_impcache;
	//填充 cache
    cache_fill(cls, sel, imp, inst);
 done:
    runtimeLock.unlock();
    return imp;
}

複製程式碼

SUPPORT_INDEXED_ISA是在arm64LP64 還有arm_arch_7k>2為1,iphone屬於arm64mac os屬於LP64,所以SUPPORT_INDEXED_ISA = 1.

// Define SUPPORT_INDEXED_ISA=1 on platforms that store the class in the isa 
// field as an index into a class table.
// Note, keep this in sync with any .s files which also define it.
// Be sure to edit objc-abi.h as well.
// __ARM_ARCH_7K__ 處理器架構指令集版本
//__arm64__ 架構
//__LP64__ uinx 和uinx  mac os
#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif
複製程式碼

lookUpImpOrForward函式的 大概思路如下:

首次已經從快取中查過沒有命中所以不再去快取中查了,然後判斷cls是否已經實現,cls->isRealized(),沒有實現的話進行實現realizeClass(cls),主要是將初始化read-write data和其他的一些資料,後續會細講。然後進行cls的初始化_class_initialize(),當cls需要初始化和沒有初始化的時候進行cls初始化,初始化會加入到一個執行緒,同步執行,先初始化父類,再初始化子類,資料的大小最小是4,擴容規則是:n*2+1;然後再次獲取impcache_getImp,然後在cls方法中查詢該method,然後就是在superclass中查詢方法,直到父類是nil,找到的話,獲取imp並將clssel加入到cache中,否則進入到訊息解析階段_class_resolveMethod,在轉發階段,不是元類的話,進入到_class_resolveInstanceMethod是元類的話呼叫_class_resolveClassMethod,這兩種分別都會進入到lookUpImpOrNil,再次查詢IMP,當沒找到的話就返回,找到的話用objc_msgSend傳送訊息實現呼叫SEL_resolveInstanceMethod並標記triedResolver為已動態解析標誌。然後進入到訊息動態轉發階段_objc_msgForward_impcache,至此runtime傳送訊息結束。

借用網上找一個圖, 可以更直觀的看出流程運轉。

iOS底層原理 runtime- objc_msgSend拾遺基礎篇--(7)

realizeClass()解析

realizeClass是初始化了很多資料,包括cls->ro賦值給cls->rw,新增元類version為7,cls->chooseClassArrayIndex()設定cls的索引,supercls = realizeClass(remapClass(cls->superclass)); metacls = realizeClass(remapClass(cls->ISA()))初始化superclasscls->isa,後邊針對沒有優化的結構進行賦值這裡不多講,然後協調例項變數偏移佈局,設定cls->setInstanceSize,拷貝flagsrorw中,然後新增subclassrootclass,最後新增類別的方法,協議,和屬性。

/***********************************************************************
* realizeClass
 cls第一次初始化會執行,包括cls->rw->data(),返回真實的cls 結構體
 runtimelock 必須有呼叫者把寫入鎖鎖起來
**********************************************************************/
static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();

    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?
//首先將tw賦值給to,因為資料結構一樣可以直接強制轉化
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {//是否已經初始化過,初始化過的哈 則 cls->rw 已經初始化過
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // 正常情況下 申請class_rw_t空間
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;//cls->rw->ro 指向現在的ro
        rw->flags = RW_REALIZED|RW_REALIZING;//realized = 1 and  realizing = 1
        cls->setData(rw);//賦值
    }

    isMeta = ro->flags & RO_META;//是否是元類
	

    rw->version = isMeta ? 7 : 0;  // 元類版本是7,舊版的6,否就是0


    // Choose an index for this class.
//設定cls的索引
	cls->chooseClassArrayIndex();

    if (PrintConnecting) {
        _objc_inform("CLASS: realizing class '%s'%s %p %p #%u", 
                     cls->nameForLogging(), isMeta ? " (meta)" : "", 
                     (void*)cls, ro, cls->classArrayIndex());
    }

    // 如果父類沒有初始化則進行初始化
    // root_class 做完需要設定RW_REALIZED=1,
    // root metaclasses 需要執行完.
	//從NXMapTable 獲取cls ,然後進行初始化
	//從NXMapTable 獲取cls->isa ,然後進行初始化
    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()));
//沒有經過優化的isa執行的,現在已經是version=7,在arm64上是優化過的,這個先不看了。
#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);

	// 協調例項變數偏移/佈局
	//可能重新申請空間 class_ro_t,更新我們的class_ro_t
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // 設定setInstanceSize 從ro->instanceSize
    cls->setInstanceSize(ro->instanceSize);

	//拷貝flags 從ro到rw中
    if (ro->flags & RO_HAS_CXX_STRUCTORS) {
        cls->setHasCxxDtor();
        if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
            cls->setHasCxxCtor();
        }
    }
//新增superclass指標
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    // Attach categories
	//類別的方法 在編譯的時候沒有新增到二進位制檔案中,在執行的時候新增進去的
    methodizeClass(cls);

    return cls;
}
複製程式碼

這裡最後新增類別的資料是呼叫了methodizeClass函式,這個函式首先新增method_list_t *list = ro->baseMethods()rw->methods.attachLists(&list, 1),然後將屬性property_list_t *proplist=ro->baseProperties新增到rw->properties.attachLists(&proplist, 1),最後將協議列表protocol_list_t *protolist = ro->baseProtocols追加到rw->protocols.attachLists(&protolist, 1),如果是metaclass則新增SEL_initialize,然後從全域性NXMapTable *category_map刪除已經載入的category_list,最後呼叫attachCategories(cls, cats, false /*don't flush caches*/)將已經載入的cats的方法新增到cls->rw上面並且不重新整理caches

/***********************************************************************
* methodizeClass
 修復cls方法列表想,協議列表和屬性列表
* 加鎖
**********************************************************************/
static void methodizeClass(Class cls)
{
    runtimeLock.assertLocked();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

    // Methodizing for the first time
    if (PrintConnecting) {
        _objc_inform("CLASS: methodizing class '%s' %s", 
                     cls->nameForLogging(), isMeta ? "(meta)" : "");
    }

	//方法列表
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
	//將物件的方法追加到cls->rw->methods後面
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
	//將物件的屬性追加到rw->properties後面
        rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
	//將物件的協議追加到rw->protocols後面
        rw->protocols.attachLists(&protolist, 1);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
	//類別 從全域性NXMapTable *category_map 已經載入過了。
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
	//收集所有的cats到cls -> rw中
    attachCategories(cls, cats, false /*don't flush caches*/);

    if (PrintConnecting) {
        if (cats) {
            for (uint32_t i = 0; i < cats->count; i++) {
                _objc_inform("CLASS: attached category %c%s(%s)", 
                             isMeta ? '+' : '-', 
                             cls->nameForLogging(), cats->list[i].cat->name);
            }
        }
    }
    
    if (cats) free(cats);//釋放cats

#if DEBUG
    // Debug: sanity-check all SELs; log method list contents
    for (const auto& meth : rw->methods) {
        if (PrintConnecting) {
            _objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(meth.name));
        }
        assert(sel_registerName(sel_getName(meth.name)) == meth.name); 
    }
#endif
}
複製程式碼

attachCategories()解析

methodizeClass之前rw初始化的時候並沒有將其他資料都都複製給rw,現在methodizeClass實現了將本來的ro資料拷貝給rw,然後attachCategories將 分類的方法,屬性,協議追加到cls->data->rw,我們進入attachCategories內部

static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
	//方法陣列[[1,2,3],[4,5,6],[7,8,9]]
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
	//屬性陣列
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
	//協議陣列
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
		//取出某個分類
        auto& entry = cats->list[i];
//取出分類 的 instance方法或者class方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist; //mlists 接受所有分類方法
            fromBundle |= entry.hi->isBundle();
        }
//proplist 接受所有分類屬性
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
//proplist 接受所有協議方法
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
//收集了所有協議 分類方法
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
	//追加所有分類方法
    rw->methods.attachLists(mlists, mcount);
	//釋放陣列
    free(mlists);
	//重新整理該類的快取
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
//追加所有分類屬性
    rw->properties.attachLists(proplists, propcount);
    free(proplists);//釋放陣列
//追加所有分類協議
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);//釋放陣列
}
複製程式碼

rw->list->attachLists()解析

新增attachLists函式規則是後來的卻新增到記憶體的前部分,這裡就清楚為什麼後編譯類別能後邊的類別覆蓋前邊的類別的相同名字的方法。

 void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
			//一共需要的數量
            uint32_t newCount = oldCount + addedCount;
			//分配記憶體 記憶體不夠用了,需要擴容
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
			//賦值count
            array()->count = newCount;
			// array()->lists:原來的方法列表向後移動 oldCount * sizeof(array()->lists[0]個長度
            memmove(array()->lists + addedCount/*陣列末尾*/, array()->lists/*陣列*/,
                    oldCount * sizeof(array()->lists[0])/*移動的大小*/);
			//空出來的 記憶體使用addedLists拷貝過去 大小是:addedCount * sizeof(array()->lists[0])
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
			/*
			圖示講解:
			array()->lists:A->B->C->D->E
		addedCount:3
		addedLists:P->L->V
			memmove之後:nil->nil->nil->A->B->C->D->E
			然後再講addedLists插入到陣列前邊,最終array()->lists的值是:
			P->L->V->A->B->C->D->E
			 */
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }
複製程式碼

class初始化完成了,然後再次嘗試獲取imp = cache_getImp,由於快取沒有中間也沒新增進去,所以這裡也是空的,然後從getMethodNoSuper_nolock獲取該cls的方法列表中查詢,沒有的話再從superclass查詢cachemethod,找到的話,進行log_and_fill_cache至此訊息傳送完成。

訊息動態解析

動態解析函式_class_resolveMethod(cls, sel, inst),如果不是元類呼叫_class_resolveInstanceMethod,如果是的話呼叫_class_resolveClassMethod

/***********************************************************************
* _class_resolveMethod
* 呼叫 +resolveClassMethod 或者 +resolveInstanceMethod
* 如果存在了則不檢查
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {//不是元類則呼叫 例項的
	//首先呼叫
		_class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
		//尋找classMethod
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製程式碼

resolveInstanceMethod,查詢SEL_resolveInstanceMethod,傳值不用初始化,不用訊息解析,但是cache要查詢。沒有找到的直接返回,找到的話使用objc_msgSend傳送訊息呼叫SEL_resolveInstanceMethod

/***********************************************************************
* _class_resolveInstanceMethod
* 呼叫 class新增的函式 +resolveInstanceMethod
* 有可能是元類
* 如果方法存在則不檢查
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
//如果找到SEL_resolveInstanceMethod 則使用objc_msgSend函式
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
複製程式碼

_class_resolveClassMethod中,第一步先去lookUpImpOrNil查詢+SEL_resolveClassMethod方法,沒找到的就結束,找到則呼叫objc_msgsend(id,sel)

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
複製程式碼

動態解析至此完成。

訊息轉發

_objc_msgForward_impcache是轉發的函式地址,在搜尋框搜尋發現,這個函式除了.s檔案中有,其他地方均只是呼叫,說明這個函式是彙編實現,在objc-msg-arm64.s 531 行發現一點蹤跡

STATIC_ENTRY __objc_msgForward_impcache //開始__objc_msgForward_impcache
	// No stret specialization.
	b	__objc_msgForward//跳轉->__objc_msgForward
	END_ENTRY __objc_msgForward_impcache // 結束__objc_msgForward_impcache

	
	ENTRY __objc_msgForward // 開始 __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]//p17= x17 和 __objc_forward_handler@PAGEOFF的和
	TailCallFunctionPointer x17 //跳轉-> TailCallFunctionPointer

	END_ENTRY __objc_msgForward//結束 __objc_msgForward
複製程式碼

當跳轉到adrp x17, __objc_forward_handler@PAGE這一行,搜搜尋函式_objc_forward_handler,看到只是個列印函式,並沒有其他函式來替代這個指標,那麼我們用其他方法來探究。

__attribute__((noreturn)) void 
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製程式碼

網上有大神總結的點我們先參考下

// 虛擬碼
int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);

    // 呼叫 forwardingTargetForSelector:
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            if (isStret == 1) {
                int ret;
                objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
                return ret;
            }
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }

    // 殭屍物件
    const char *className = class_getName(receiverClass);
    const char *zombiePrefix = "_NSZombie_";
    size_t prefixLen = strlen(zombiePrefix); // 0xa
    if (strncmp(className, zombiePrefix, prefixLen) == 0) {
        CFLog(kCFLogLevelError,
              @"*** -[%s %s]: message sent to deallocated instance %p",
              className + prefixLen,
              selName,
              receiver);
        <breakpoint-interrupt>
    }

    // 呼叫 methodSignatureForSelector 獲取方法簽名後再呼叫 forwardInvocation
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature) {
            BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
            if (signatureIsStret != isStret) {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",
                      selName,
                      signatureIsStret ? "" : not,
                      isStret ? "" : not);
            }
            if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
                NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

                [receiver forwardInvocation:invocation];

                void *returnValue = NULL;
                [invocation getReturnValue:&value];
                return returnValue;
            } else {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
                      receiver,
                      className);
                return 0;
            }
        }
    }

    SEL *registeredSel = sel_getUid(selName);

    // selector 是否已經在 Runtime 註冊過
    if (sel != registeredSel) {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
              sel,
              selName,
              registeredSel);
    } // doesNotRecognizeSelector
    else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }
    else {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
              receiver,
              className);
    }

    // The point of no return.
    kill(getpid(), 9);
}
複製程式碼

驗證動態解析

我們簡單定義一個test函式,然後並執行這個函式。

@interface Person : NSObject
- (void)test;
@end
@implementation Person
+(BOOL)resolveInstanceMethod:(SEL)sel{
	NSLog(@"%s",__func__);
	if (sel == @selector(test)) {
		Method me = class_getInstanceMethod(self, @selector(test2));
		class_addMethod(self, sel,
						method_getImplementation(me),
						method_getTypeEncoding(me));
		return YES;
	}
	return [super resolveInstanceMethod:sel];
}
-(void)test2{
	NSLog(@"來了,老弟");
}
@end

Person *p = [[Person alloc]init];
[p test];
[p test];
 //輸出
+[FYPerson resolveInstanceMethod:]
 -[FYPerson test3]
 -[FYPerson test3]
複製程式碼

[p test]在第一次執行的時候會走到訊息動態解析的這一步,然後通過objc_msgsend呼叫了test,並且把test新增到了快取中,所以輸出了+[FYPerson resolveInstanceMethod:],在第二次呼叫的時候,會從快取中查到imp,所以直接輸出了-[FYPerson test3]

+resolveInstanceMethod可以攔截掉例項方法的動態解析,在+resolveClassMethod可以攔截類方法。


@interface Person : NSObject
+ (void)test;
@end

+ (void)test3{
	NSLog(@"來了,老弟");
}
+ (BOOL)resolveClassMethod:(SEL)sel{
	NSLog(@"%s",__func__);
	if (sel == @selector(test)) {
		Method me = class_getClassMethod(self, @selector(test3));//獲取method
		//給sel 新增方法實現 @selecter(test3)
		class_addMethod(object_getClass(self), sel,
						method_getImplementation(me),
						method_getTypeEncoding(me));
		return YES;
	}
	return [super resolveInstanceMethod:sel];
}

[Person test];

//輸出
+[Person resolveClassMethod:]
來了,老弟
複製程式碼

攔截+resolveClassMethod,在條件為sel==@selector(test)的時候,將函式實現+test3()IMP使用class_addMethod新增到Person上,待下次呼叫test的時候直接通過imp = cache_getImp(cls, sel);獲取到imp函式指標並且執行。 我們也可以通過新增c函式的imp來實現給class新增函式實現。

+(BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s",__func__);
    if (sel == @selector(test)) {
//        Method me = class_getInstanceMethod(self, @selector(test3));
//        class_addMethod(self.class, sel, method_getImplementation(me), method_getTypeEncoding(me));
        class_addMethod(self.class, sel, (IMP)test3, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void test3(id self,SEL sel){
    NSLog(@"test3:%s",NSStringFromSelector(sel).UTF8String);
}

//輸出
+[FYPerson resolveInstanceMethod:]
test3:test
test3:test
複製程式碼

v16@0:8是返回值為void引數佔用16位元組大小,第一個是從0開始,第二個從8位元組開始。 這段程式碼和上面的其實本質上是一樣的,一個是給class新增函式實現,使selimp對應起來,這個是將c函式的impsel進行關聯,新增快取之後,使用objc_msgsend()效果是一樣的。

驗證訊息轉發

訊息轉發可分為3步,第一步根據- (id)forwardingTargetForSelector:(SEL)aSelector返回的類物件或者元類物件,將方法轉發給該物件。假如第一步沒實現,則第二步根據返回的-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector函式簽名,在第三步(void)forwardInvocation:(NSInvocation *)anInvocation呼叫函式[anInvocation invoke]進行校驗成功之後進行呼叫函式。

@interface Person : NSObject
- (void)test;
@end

#import "Person.h"
#import "Student.h"

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector{
	if (aSelector == @selector(test)) {
		//objc_msgSend([[Struent alloc]init],test)
		return [[Struent alloc]init];
	}
	return [super forwardingTargetForSelector:aSelector];
}
@end
//輸出
-[Student test]
複製程式碼

我們定義了一個Person只宣告瞭test沒有實現,然後在訊息轉發第一步forwardingTargetForSelector將要處理的物件返回,成功呼叫了Studenttest方法。

第一步沒攔截,可以在第二步攔截。

//訊息轉發第二步 沒有物件來處理方法,那將函式簽名來實現
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
	if (aSelector == @selector(test)) {
		NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
		return sign;
	}
	return [super methodSignatureForSelector:aSelector];
}
// 函式簽名已返回,到了函式呼叫的地方
//selector 函式的sel
//target   函式呼叫者
//methodSignature 函式簽名
//NSInvocation  封裝資料的物件
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
}
//輸出
-[Person forwardInvocation:]
複製程式碼

列印出了-[Person forwardInvocation:]而且沒有崩潰,在forwardInvocation:(NSInvocation *)anInvocation怎麼操作看開發者怎麼處理了,探究下都可以做什麼事情。 看到NSInvocation的屬性和函式,seltarget是讀寫,函式簽名是必須的,所以(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector必須將函式簽名返回。

@property (readonly, retain) NSMethodSignature *methodSignature;//只讀
- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;
@property (nullable, assign) id target;//讀寫
@property SEL selector;//讀寫
複製程式碼

當攔截方法是類方法的時候,可以用+ (id)forwardingTargetForSelector:(SEL)aSelecto攔截,

//class 轉發
// 訊息轉發第一步 攔截是否有轉發的class物件處理方法
+ (id)forwardingTargetForSelector:(SEL)aSelector{
	if (aSelector == @selector(test3)) {
		//objc_msgSend([[Struent alloc]init],test)
		return [Student class];
	}
	return [super forwardingTargetForSelector:aSelector];
}

+ (void)test3{
//	NSLog(@"+[Student test3]");
//當[Person test3]上一行寫這麼一行,Person *p = [[Person alloc]init] 這句報錯
//暫時不懂為什麼不能呼叫NSLog,但是已經進來了所以呼叫了test2。
//註釋掉 [[Person alloc]init],一切正常。 有大佬瞭解嗎
}
- (void)test2{
	NSLog(@"%s",__func__);
}

// 輸出
-[Student test2]
複製程式碼

也可以用返回return [[Student alloc]init];class類方法轉化成例項方法,最後呼叫了Student的物件方法test3。其實本質上都是objc_msgSend(id,SEL,...),我們修改的只是id的值,id型別在這段程式碼中本質是物件,所以我們可以return instance也可以reurn class

+ (id)forwardingTargetForSelector:(SEL)aSelector{
	if (aSelector == @selector(test3)) {
		//objc_msgSend([[Struent alloc]init],test)
		return [[Student alloc]init];
	}
	return [super forwardingTargetForSelector:aSelector];
}

- (void)test3{
	NSLog(@"%s",__func__);
}
//輸出
-[Student test3]
複製程式碼

將剛才寫的methodSignatureForSelectorforwardInvocation改成類方法,也是同樣可以攔截類方法的。我們看下

//訊息轉發第二步 沒有class來處理方法,那將函式簽名來實現
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
	if (aSelector == @selector(test3)) {
		NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
		return sign;
	}
	return [super methodSignatureForSelector:aSelector];
}
// 函式簽名已返回,到了函式呼叫的地方
//selector 函式的sel
//target   函式呼叫者
//methodSignature 函式簽名
//NSInvocation  封裝資料的物件
+ (void)forwardInvocation:(NSInvocation *)anInvocation{
//	anInvocation.selector = @selector(test2);
//此處換成[Student class]同樣可以
//	anInvocation.target = (id)[[Student alloc]init];

//	[anInvocation invoke];
	NSLog(@"%s",__func__);
}

//輸出
+[Person forwardInvocation:]
複製程式碼

測過其實物件方法和類方法都是用同樣的流程攔截的,物件方法是用-方法,類方法是用+方法。

總結

  • objc_msgSend傳送訊息,會首先在cache中查詢,查詢不到則去方法列表(順序是cache->class_rw_t->supclass cache ->superclass class_rw_t ->動態解析)
  • 第二步是動態解析,能在resolveInstanceMethod或+ (BOOL)resolveClassMethod:(SEL)sel來來攔截,可以給class新增實現函式,達到不崩潰目的
  • 第三步是訊息轉發,轉發第一步可以在+ (id)forwardingTargetForSelector:(SEL)aSelector- (id)forwardingTargetForSelector:(SEL)aSelector攔截類或例項方法,能將物件方法轉發給其他物件,也能將物件方法轉發給類方法,也可以將類方法轉發給例項方法
  • 第三步訊息轉發的第二步可以在+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector實現攔截類和例項方法並返回函式簽名
  • 第三步訊息轉發的第三步可以+ (void)forwardInvocation:(NSInvocation *)anInvocation- (void)forwardInvocation:(NSInvocation *)anInvocation實現類方法和例項方法的呼叫和獲取返回值

資料下載

本文章之所以圖片比較少,我覺得還是跟著程式碼敲一遍,印象比較深刻。


最怕一生碌碌無為,還安慰自己平凡可貴。

廣告時間

iOS底層原理 runtime- objc_msgSend拾遺基礎篇--(7)

相關文章