IOS底層原理 -5.運行時(1)

OC是一種動態性比較強的語言,所有的函數調用都是基於消息機制;簡介參照:

1. isa指針

1.1 簡述

 ***注意:以下的分析都是基於arm64***

isa在前面介紹過,實例對象可以通過 isa找到類對象,類對象通過isa可以找到原類對象;在arm64後isa並不直接是Class類型,而是union,同時用位域位域(w3c)來存儲更多信息(struct test{uintptr_r nonpointer :1; }),

1.2 在看isa之前先熟悉兩個知識點位域共用體union

  char _bool;//將所有BOOL值存儲到一個字節中
  -(void)setTall:(BOOL)tall{
   	if(tall){
      	 _bool |=  (1<<1)}else{
    	 _bool &=  ~(1<<1)}
  }
   -(void)setRich:(BOOL)rich{
      if(rich){
      	 _bool |=  (1<<0)}else{
    	 _bool &=  ~(1<<0)}
      
  }
  - (BOOL)isRich{
     return !!(_bool&(1<<0));//最高位存儲rich
  }
 - (BOOL)isTall{
   	return !!(_bool&(1<<1));//第二位存儲Tall
  }

接下來看下位域中怎麼實現:

  @interface LYMPerson()
{
    // 位域
    struct {
        char tall : 1;
        char rich : 1;
    } _bool;
}
@end

@implementation LYMPerson

- (void)setTall:(BOOL)tall
{
    _bool.tall = tall;
}

- (BOOL)isTall
{
    return !!_bool.tall;
}

- (void)setRich:(BOOL)rich
{
    _bool.rich = rich;
}

- (BOOL)isRich
{
    return !!_bool.rich;
}
@end
//簡化的源碼
	union isa_t //共用體
	{
	    isa_t() { }
	    isa_t(uintptr_t value) : bits(value) { }
	
	    Class cls;
	    uintptr_t bits;//存放所有的數據
	
	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;
	    };
	
	};

1.3 isa結構體的成員的含義:

  • 結構體基本成員介紹
nonpointer has_assoc
0 代表普通指針,存儲着Class、Meta-Class對象的內存地址;1 代表優化過,使用位域存儲更多的信息 是否設置過關聯對象,如果沒有,釋放時更快
has_cxx_dtor shiftcls
是否有c++的析構函數(cxx_destruct),如果沒有,釋放時會更快 存儲着Class,Meta-Class對象的內存地址信息
magic weakly_referenced
用於在調試時分辨對象是否未完成初始化 是否有被弱引用指向如果沒有,釋放速度會更快
deallocating has_sidetable_rc
對象是否正在釋放 裏面存儲的值的引用計數器減1
extra_rc
引用計數器是否過大無法存儲在isa中,如果爲1那麼引用計數會存儲在一個叫SideTable的類屬性中
  • 下面來驗證一下
    在這裏插入圖片描述
    加上weak和關聯屬性後的值
#import "ViewController.h"

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

@interface LYMAnimal:NSObject
@property(nonatomic,assign,readwrite) NSInteger age;
@end
@implementation LYMAnimal

@end
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    LYMAnimal *animal = [[LYMAnimal alloc]init];
    __weak LYMAnimal *weakSelf = animal;
    
    objc_setAssociatedObject(animal, @"name", @"haha", OBJC_ASSOCIATION_COPY);
    id va = objc_getAssociatedObject(animal, @"name");
    NSLog(@"%@ %@",animal,va);
}


@end

在這裏插入圖片描述
通過對比兩張圖可以看出,加了weak和關聯對象後相應的位的值都有改變,weakly_refrenced的變成了1,has_assoc對應的位變成了1,這與表格的描述剛好符合;

1.4 isa擴展

  • 在之前實例對象/類對象通過isa找到類對象/原類對象前都必須的&ISA_MASK;而在源碼中的定義爲ISA_MASK 0x0000000ffffffff8ULL,在聯合體中shiftcls存儲真實地址只佔33位,因此&ISA_MASK剛好可以取出類對象或者原類對象的真實地址;同時也可以知道類對象或者原類對象的最後三位爲零,通過一段代碼驗證一下:

在這裏插入圖片描述

  • 從上圖的輸出可知最後一位全是0和8,而16進制的0和8 對應的二進制是 0000和1000,那麼可以驗證實例對象和類對象、原類對象的內存地址最後三位都是0

Class

  1. 照例看下圖:
    在這裏插入圖片描述
  2. 下面詳細介紹下這個結構體的成員:

class_rw_t:可讀可寫,裏面的methods和propeties及protocols都是二位數組,而ro指向的結構體class_ro_t(只讀)裏面的baseMethodList…是一維的,其中baseMethodList中是method_t類型;
method_t:結構體包含 IMP imp,指向函數地址的指針;SEL name 函數名;const chat *type;返回值類型,參數類型;

  • IMP代表函數的具體實現,typedef id _Nullable (*IMP)(id __Nonnull,SEL _nonnull ,...);

  • SEL代表方法/函數名,也叫做選擇器,底層結構和char類似,可以通過@selector()、sel_registerName()獲得,不同類中相同名字的方法,所對應的方法選擇器是相同的,它的底層是:
    typedef struct objc_selector *SEL

  • types 包含了函數的返回值,參數的編碼信息;v@:v代表返回值是void,@代表參數id類型,代表參數sel,@encode()指令,可以將具體的類型表示成字符串編碼,蘋果在其官方文檔中也有說明;這裏列舉部分如下圖:

在這裏插入圖片描述
3. 方法緩存(cache_t

  • 在之前研究objc_class結構體的時候,我們知道在Class結構體內部有一個方法緩存結構體(cache_t cache),使用散列表來緩存使用過的方法來提高方法的查找速度;

在這裏插入圖片描述

  • 散列表:是一個長度不固定的列表,當長度不夠存儲的時候會重新創建後將原來的釋放掉,主要是通過一定的算法將數據存入內存列表的指定索引位置,查找的時候根據同一算法算出的索引直接去列表中取出,當只有一個數據需要存儲的時候,算出的索引在什麼位置就存儲在什麼索引位置,該索引以外的其他索引存儲爲NULL;因此可以說散列表犧牲了部分內存換取查找速度;oc中如果算出的索引相同繼續比較key,如果key不相同則將值減1,依次查找直到找到;
  • 順序:實例對象isa–> 類對象–>類對象的cache中查找,沒有–>methodList中找,找到返回,同時加入cache中
    如果methodList找沒有找到–>superclass找到父類–>cache中查找–>找到返回,同時緩存到類對象的cache中
    如果在父類的cache找沒有找到–>methodList中查找–>找到返回,同時緩存到類對象的cache中; 即:如下圖
    在這裏插入圖片描述

2. objc_msgSend(id,SEL);OC中的方法調用

2.1 簡述

OC中的方法調用都是基於消息發送即:objc_msgSend(id,SEL);,看一個簡單的示例:

 LYMAnimal *animal = [[LYMAnimal alloc]init];
  [animal callEat];

在執行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m的c++文件裏最後裏(約32000多行)如下:

#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_v7_xv8f6lp50hbcxftgjf7yb_sw0000gn_T_main_773e9c_mi_0);
        LYMAnimal *animal = ((LYMAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)((LYMAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LYMAnimal"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("callEat"));
    }
    return 0;
}

名詞: receiver
從上述代碼可以看到在main函數裏[animal callEat];最終轉換成了objc_msgSend;objc_msgSend在蘋果開源的運行時代碼裏是以彙編實現的,因爲這部分代碼肯定是調用頻率非常的高;

2.2 執行階段:消息發送

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	MESSENGER_START
//x0寄存器:消息接收者,判斷消息接受者是不是(<0)空,是則跳轉到LNilOrTagged 然後LReturnZero最後返回(ret)
	cmp	x0, #0			// nil check and tagged pointer check
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative) //進行判斷如果上述成立就跳轉
	ldr	x13, [x0]		// x13 = isa
	and	x16, x13, #ISA_MASK	// x16 = class	
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached   //這裏開始查找緩存

LNilOrTagged:
	b.eq	LReturnZero		// nil check

	// tagged
	mov	x10, #0xf000000000000000
	cmp	x0, x10
	b.hs	LExtTag
	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]
	b	LGetIsaDone

LExtTag:
	// 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
	
LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	MESSENGER_END_NIL
	ret  //相當於return

	END_ENTRY _objc_msgSend

首先會判斷消息接受者(receiver)是不是空的,空的直接返回;如果不是空則在緩存中查找CacheLookup

.macro CacheLookup
	// x1 = SEL, x16 = isa
	ldp	x10, x11, [x16, #CACHE]	// x10 = buckets, x11 = occupied|mask
	and	w12, w1, w11		// x12 = _cmd & mask
	add	x12, x10, x12, LSL #4	// x12 = buckets + ((_cmd & mask)<<4)

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

3:	// wrap: x12 = first bucket, w11 = mask
	add	x12, x12, w11, UXTW #4	// x12 = buckets+(mask<<4)

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

	ldp	x9, x17, [x12]		// {x9, x17} = *bucket
1:	cmp	x9, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp 查找到返回IMP
	
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]!	// {x9, x17} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0//緩存中沒有找到跳轉
	
.endmacro

在上述彙編中進行查找,沒有找到跳轉到JumpMiss

.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached//這個是一個C語言的函數__class_lookupMethodAndLoadCache3
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

接着便是在這個函數中執行:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)//彙編代碼比c代碼多一個"_"
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrForward中會重新查找一次緩存,因爲這裏有可能動態添加方法;這個方法也是最核心的方法:

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) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.read();

    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    
 retry:    
    runtimeLock.assertReading();

    // Try this class's cache.

    imp = cache_getImp(cls, sel);//會再次去緩存中查找
    if (imp) goto done;

    // Try this class's method lists. 去方法列表中查找
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists. 去父類的緩存和方法列表中查找
    {
        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;
            }
        }
    }

    // No implementation found. Try method resolver once.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

通過上述代碼可知:方法調用時候,底層轉換成消息發送(obj_msgSend),首先如果有緩存則在緩存中查找,緩存中沒有則在方法列表中查找,在方法列表中進行查找的時候會執行一次緩存查找避免該過程中動態加入的方法導致的問題;如果方法列表找那個也沒有則去父類的緩存和方法列表中查找;

2.3 執行階段:動態方法解析 (dynamic method resolution)

在其父類中還找不到方法且父類沒有父類,那麼就會進入動態方法解析,這裏會調用+(BOOL)resolveInstanceMethod:(SEL)sel,這裏可以動態添一次方法,然後會重新開始一次方法查找:

2.3.1 實例方法動態添加

在動態添加之前會判斷是不是已經動態添加過,如果添加過就不會添加;就是直接到消息轉發階段
void callEat(id self,SEL _cmd){
    NSLog(@"callEat 動態添加");
}
@implementation LYMAnimal
//-(void)callEat{
//    NSLog(@"eat======");
//}
struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};
//-(void)otherCall{
//    NSLog(@"%s",__func__);
//}
//+(BOOL)resolveInstanceMethod:(SEL)sel{
//    if (sel == @selector(callEat)) {
//        struct method_t *meth = (struct method_t *)class_getInstanceMethod(self, @selector(otherCall));
//        //動態添加方法
//        class_addMethod(self, sel, meth->imp, meth->types);
//        return YES;
//    }
//    return [super resolveInstanceMethod:sel];
//}
+(BOOL)resolveInstanceMethod:(SEL)sel{//對象方法
    if (sel == @selector(callEat)) {
        //動態添加方法
        class_addMethod(self, sel, (IMP)callEat, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

2.3.2 類方法動態添加:

+(BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(callEat1)) {
        //動態添加方法
        class_addMethod(object_getClass(self), sel, (IMP)callEat, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

如果這一步沒有處理,那麼就會到消息轉發;

2.4 執行階段:消息轉發

消息轉發有三個階段,各個階段關係如圖(圖來自:<<Effective-Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法>>):
在這裏插入圖片描述

-(id)forwardingTargetForSelector:(SEL)aSelector

先看一個經典錯誤:
在這裏插入圖片描述

消息轉發部分源碼是沒有開源,但是從上圖可以知道:

  • 先調用__forwarding__ 然後在該方法裏會調用forwardingTargetForSelector,該方法裏可以返回能處理該消息的對象
  • 如果上面的方法返回nil,則會調用methodSignatureForSelector
    補充一個知識點: NSInvocation 封裝了一個方法調用,包括方法調用者,方法名,方法參數,更改其target屬性可以修改其方法調用者;

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(callEat)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];//這裏不是空的就會調用forwardInvocation
    }
    return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    
}

注意:類對象消息轉發對應的方法:

//+(id)forwardingTargetForSelector:(SEL)aSelector
+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(callEat1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}
//NSInvocation 封裝了一個方法調用,包括方法調用者,方法名,參數,返回值
+(void)forwardInvocation:(NSInvocation *)anInvocation{
    
}

對應輸出:
在這裏插入圖片描述
在這裏插入圖片描述

2.4 最後

應用來自《Effective-Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》的一段總結:

接收者在每一步中均有機會處理消息。步驟越往後,處理消息的代價就越大。最好能在第一步就處理完,這樣的話,運行期系統就可以將此方法緩存起來了。如果這個類的實例稍後還收到同名選擇子,那麼根本無須啓動消息轉發流程。若想在第三步裏把消息轉給備援的接收者,那還不如把轉發操作提前到第二步。因爲第三步只是修改了調用目標,這項改動放在第二步執行會更爲簡單,不然的話,還得創建並處理完整的NSInvocation。

3. super關鍵字

3.1 一個網上經典的面試題:

        NSLog(@"self class = %@",[self class]);
        NSLog(@"self superclass = %@",[self superclass]);
        
        NSLog(@"super class = %@",[super class]);
        NSLog(@"super superclass =%@",[super superclass]);

輸出結果是:
在這裏插入圖片描述
super是一個編譯器標示,告訴方法調用時候,從父類的方法中去找方法的實現,但是消息的接收者還是自己(子類對象);
其詳細的底層實現解釋請參考:iOS-底層原理17

3.2 底層分析

先看一下轉換成c++文件中對應的結構:

struct __rw_objc_super { 
	struct objc_object *object; 
	struct objc_object *superClass; 
	__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};
// @implementation LYMWorker

static void _I_LYMWorker_eat(LYMWorker * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LYMWorker"))}, sel_registerName("eat"));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_v7_xv8f6lp50hbcxftgjf7yb_sw0000gn_T_LYMWorker_b54bca_mi_0,__func__);
}
// @end

簡化一下函數裏的方法調用就是:objc_msgSendSuper(rw_objc_super,@selector(eat));結構體objc_super在objc源碼裏定義爲:

struct objc_super{//這裏是簡化的寫法
  id receiver;
  Class super_class;
}

objc_msgSendSuper參數的註釋裏解釋,super_class只是告訴查找方法實現的時候是直接從父類的類對象開始查找;方法的接收者還是LYMWorker的實例;這裏有總結兩點:

  • 上述方法中調用的class方法在NSObject中實現,不論從哪個開始查找最終的實現都是在NSObject中的
  • class只是告訴類型是什麼,類型至於方法的調用者有關(也就是消息接收者);因此[super class]只是告訴編譯器從父類開始查找,所以最終返回的類型還是LYMWorker
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章