MJiOS底層筆記--Runtime

本文屬筆記性質,主要針對本身理解不太透徹的地方進行記錄。ios

推薦系統直接學習小碼哥iOS底層原理班---MJ老師的課確實不錯,強推一波。緩存


簡介

  • 支撐Object-C的動態性
  • C語言提供結構,C\C++\彙編編寫實現

isa

isa做爲runtime底層中最經常使用的一個數據結構。bash

class的isa

在arm64架構以前,isa就是一個普通的指針,存儲着Class、Meta-Class對象的內存地址數據結構

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
複製代碼

union的isa

從arm64架構開始,對isa進行了優化,變成了一個共用體(union)結構,使用位運算來得到更多的信息,但依舊是8位字符。架構

共用體與結構體相似,但內部全部成員將會共用(首元素的)內存。對整塊內存經過位運算來進行擴展操做。ide

union isa_t 
{
    Class cls;
    uintptr_t bits; //決定了共用體所佔的內存大小。可使用位運算

# if __arm64__ //位運算用掩碼 bits & MASK \\ bits | MSAK
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
    // 位域負責可讀性展現.從右向左
    struct {
        uintptr_t nonpointer        : 1; //佔1位
        uintptr_t has_assoc         : 1;//佔1位
        uintptr_t has_cxx_dtor      : 1;//佔1位
        
        //因爲這種運算除了中間33位都是0,因此arm64下,全部類對象地址的64位中除了這33位必定都是0
        uintptr_t shiftcls          : 33; //佔33位  //class對象的地址  bits&ISA_MASK
        
        uintptr_t magic             : 6;//佔6位
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
    };
    
    .....
};
複製代碼


class_wr_t

存放類信息的可讀寫列表函數

基本介紹能夠查閱OC對象本質中關於class_wr_t的部分。post

有一些東西不過重要的東西能夠補充:學習

從源碼上看,類在初始化時最開始的data() -> ro_t,在初始化過程當中纔將data() -> rw_t而且將ro_t賦值給rw_t -> ro_t優化


method_t

方法(函數)封裝

SEL name; //選擇器(函數名 )
    const char *types; //編碼(返回值,參數類型)
    IMP imp; //函數指針,存放函數地址

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
複製代碼

SEL

方法名,基本等同於C語言字符串 char*

typedef struct objc_selector *SEL;
複製代碼

不一樣類中的同名方法,其SEL相同

types

函數返回值,參數類型

能夠經過@encode(type)函數查看類型對應的編碼

// "i24@0:8i16f20"  數字表明參數/返回值所佔字節
// 0id 8SEL 16int 20float  == 24  
- (int)test:(int)age height:(float)height;
複製代碼

須要注意的是,在構建方法簽名(NSMethodSignature)時,即便簡化成i@:if也是沒有問題的。


cache_t

Class內部結構中有個方法緩存(cache_t),用散列表(哈希表)來緩存曾經調用過的方法,能夠提升方法的查找速度

struct cache_t {
    struct bucket_t *_buckets; //散列表
    mask_t _mask; //散列表長度-1
    mask_t _occupied; //已緩存的方法數量
}
複製代碼
struct bucket_t {
    cache_key_t _key;  //SEL
    IMP _imp; //IMP
}
複製代碼

查找的順序

本類 -> 父類

在其中任一位置查找到IMP,都將會寫入本類(在查找到的那一刻,直接經過cls寫入)緩存中。(父類自己則不會被緩存)

//LLDB中
p (IMP)0x0000123 能夠打印內存地址所指的方法
複製代碼

objc_msgSend

OC的消息機制。內部會經歷消息發送、動態方法解析、消息轉發三個階段

OC中的方法調用,其實都是轉換爲objc_msgSend函數的調用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        MJPerson *person = [[MJPerson alloc] init];
        [person personTest];
        
        // 在編譯是將會轉化成
        objc_msgSend(person, @selector(personTest));

    }
    return 0;
}
複製代碼

objc_msgSend底層,由彙編實現

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	MESSENGER_START
    //x0:寄存器 消息接收者 receiver
	cmp	x0, #0 // nil check and tagged pointer check
	
	//一旦消息接收者爲0 跳轉(b.le)到 LNilOrTagged
	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:
    //返回0
	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

	END_ENTRY _objc_msgSend
複製代碼

objc_msgSend的消息機制,(在查找方法時)能夠分爲三個階段:

消息發送、動態方法解析、消息轉發。最後崩潰unrecognized selector sent to instance

消息發送階段

在本類以及父類中查找方法

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    ....
    ....
    ....
    
 retry:    
    runtimeLock.assertReading();
    // 查找本類緩存

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 查找本類方法列表
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 去查找父類
    {
        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; } } } .... .... .... } 複製代碼

幾個源碼方面的點

  1. objc_msgSend自己由彙編編寫

在未查找到緩存後進入正常代碼lookUpImpOrForward進行方法查找以及後續階段操做。

  1. 本類會經歷兩次緩存查找

第一次是在彙編中直接查找緩存,第二次是在lookUpImpOrForward方法中查找本類re_t以前再查找一次。

  1. 方法列表的查找分爲兩種

已經排序的,二分查找。沒有排序的,遍歷查找

  1. 只要查詢到方法存在,就會寫入調用者類緩存。

  2. 全部父類查找完畢仍未找到方法。進入動態方法解析。

動態方法解析

若是未查找到指定方法,runtime容許開發者進行一次方法動態添加。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    ...
    ...
    ...
    
    
    // 進行一次動態解析`triedResolver`

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        
        //內部會根據cls是不是元類調用不一樣的邏輯
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 不會進行緩存
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        
        //從新走一遍方法查找,但若是尚未也不會再次被動態解析了。
        goto retry;
    }

    // No implementation found, and method resolver didn't help. // Use forwarding. //消息轉發 .... .... done: runtimeLock.unlockRead(); return imp; } 複製代碼
  1. resolveInstanceMethod的返回值其實沒什麼用

runtime裏只作了打印返回值的工做而已

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;
    }

    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));
        }
    }
}
複製代碼
  1. 每次發送消息,未實現的都有一次動態解析的機會

消息轉發

若是經歷動態解析依舊沒有肯定方法。runtime容許開發者從新指定target,甚至修改方法調用的各類參數再次調用。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    
    ....
    ....
    
    ....
    
    //查找失敗,動態解析失敗

    //進行消息轉發.內部由彙編實現
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}
複製代碼
  1. forwardingTargetForSelector

若是forwardingTargetForSelector指定了一個新的target,會對其調用objc_msgSend,從新走一遍以前的邏輯。

  1. methodSignatureForSelector

有一種簡便的調用方式,不須要本身編寫type

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test:)) {
//        return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
//        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
        return [[[MJCat alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
複製代碼
  1. forwardInvocation

若是一個方法調用能夠進入forwardingTargetForSelector,那麼你能夠對他進行任何操做。即便不invoke也不會出現崩潰。

  1. 類方法 OC中沒有直接暴露消息轉發的+方法,可是編寫是能夠調用,而且對類方法進行操做的。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [MJPerson test];
        
        
    }
    return 0;
}


//由於是objc_msgSend是對MJPerson對象發送的消息,因此只有+方法才能被調用
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"1123");
}
複製代碼
  1. 讓類方法被實例對象調用

只要將target對象修改爲實例對象便可

objc_msgSend並不區分類方法與對象方法,兩者只有消息接受者的區別而已。

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) return [[MJCat alloc] init];
    return [super forwardingTargetForSelector:aSelector];
}
複製代碼

super

super

@implementation MJStudent
- (void)run {
    [super run];
}

/cpp


struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接收者
    __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父類
};


static void _I_MJStudent_run(MJStudent * self, SEL _cmd) {
    
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJStudent"))}, sel_registerName("run"));
    
    //化簡一下
    objc_msgSendSuper((__rw_objc_super){
                        (id)self, 
                        (id)class_getSuperclass(objc_getClass("MJStudent"))}, 
                        sel_registerName("run"));
                        


    //繼續化簡
    struct objc_super arg = {self, [MJPerson class]};
    objc_msgSendSuper(arg, @selector(run));
}
複製代碼

可見在進行super調用時,使用的是objc_msgSendSuper方法。在objc_super結構體內部的receiver依舊是self,只是在查找IMP時,從父類開始查找IMP實現而已。


isMemberOfClass && isKindOfClass

對象方法是比對[self class],類方法是比對object_getClass(self)

因此在對類對象使用isMemberOfClass或者isKindOfClass時,須要對元類進行對比。 不過對於元類NSObject,其父類是NSObject因此會有點不一樣結果。

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}


+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}


+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
@end

複製代碼

結果以下:

// 這句代碼的方法調用者不論是哪一個類(只要是NSObject體系下的),都返回YES
NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1

NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); // 0
NSLog(@"%d", [MJPerson isKindOfClass:[MJPerson class]]); // 0
NSLog(@"%d", [MJPerson isMemberOfClass:[MJPerson class]]); // 0
複製代碼

方法交換

交換兩個方法的IMP,而且清空cache

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil);//清空類對象的方法緩存

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

複製代碼

LLVM編譯器的中間代碼

如今的LLVM編譯器在將代碼轉成彙編以前,再也不由CPP進行過分。而改成經過一種LLVM編譯器專屬的語言。

可使用如下命令行指令生成中間代碼

// 不須要指定架構
clang -emit-llvm -S main.m
複製代碼

以後會生成一個.ll文件。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [super forwardInvocation:anInvocation];
    
    int a = 10;
    int b = 20;
    int c = a + b;
    test(c);
}



define internal void @"\01-[MJPerson forwardInvocation:]"(%0*, i8*, %1*) #1 {
  %4 = alloca %0*, align 8
  %5 = alloca i8*, align 8
  %6 = alloca %1*, align 8
  %7 = alloca %struct._objc_super, align 8
  %8 = alloca i32, align 4
  %9 = alloca i32, align 4
  %10 = alloca i32, align 4
  store %0* %0, %0** %4, align 8
  store i8* %1, i8** %5, align 8
  store %1* %2, %1** %6, align 8
  %11 = load %0*, %0** %4, align 8
  %12 = load %1*, %1** %6, align 8
  %13 = bitcast %0* %11 to i8*
  %14 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %7, i32 0, i32 0
  store i8* %13, i8** %14, align 8
  %15 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
  %16 = bitcast %struct._class_t* %15 to i8*
  %17 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %7, i32 0, i32 1
  store i8* %16, i8** %17, align 8
  %18 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
  call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*, %1*)*)(%struct._objc_super* %7, i8* %18, %1* %12)
  store i32 10, i32* %8, align 4
  store i32 20, i32* %9, align 4
  %19 = load i32, i32* %8, align 4
  %20 = load i32, i32* %9, align 4
  %21 = add nsw i32 %19, %20
  store i32 %21, i32* %10, align 4
  %22 = load i32, i32* %10, align 4
  call void @test(i32 %22)
  ret void
}
複製代碼

一些語法

@ - 全局變量
% - 局部變量
alloca - 在當前執行的函數的堆棧幀中分配內存,當該函數返回到其調用者時,將自動釋放內存
i32 - 32位4字節的整數
align - 對齊
load - 讀出,store 寫入
icmp - 兩個整數值比較,返回布爾值
br - 選擇分支,根據條件來轉向label,不根據條件跳轉的話相似 goto
label - 代碼標籤
call - 調用函數
複製代碼

一些實操注意點

利用關聯對象(AssociatedObject)給分類添加屬性

遍歷類的全部成員變量(修改textfield的佔位文字顏色、字典轉模型、自動歸檔解檔)

交換方法實現(交換系統的方法)

利用消息轉發機制解決方法找不到的異常問題

如何給int類型的變量賦值

MJPerson *person = [[MJPerson alloc] init];
object_setIvar(person, nameIvar, @"123");
//先將10,轉化爲指針類變量指向10這個值。再將指針轉化成id
object_setIvar(person, ageIvar, (__bridge id)(void *)10); 
NSLog(@"%@ %d", person.name, person.age);
複製代碼

幾個LLDB命令

相關文章
相關標籤/搜索