iOS runtime——看這一篇就夠了

語言: CN / TW / HK

theme: channing-cyan

本文篇幅比較長,創作的目的為了自己日後溫習知識所用,希望這篇文章能對你有所幫助。 如發現任何有誤之處,肯請留言糾正,謝謝。

一、深入程式碼理解 instance、class object、metaclass

1、instance物件例項

我們經常使用id來宣告一個物件,那id的本質又是什麼呢?檢視objc/objc.h檔案

``` /// An opaque type that represents an Objective-C class. typedef struct objc_class *Class;

/// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };

/// A pointer to an instance of a class. typedef struct objc_object *id;

```

我們建立的一個物件或例項其實就是一個struct objc_object結構體,而我們常用的id也就是這個結構體的指標。

這個結構體只有一個成員變數,這是一個Class型別的變數isa,也是一個結構體指標,isa指標就指向物件所屬的類

一個 NSObject 物件佔用多少記憶體空間? 一個NSObject例項物件只有一個isa指標,所以一個isa指標的大小,他在64位的環境下佔8個位元組,在32位環境上佔4個位元組。

``` NSObject *obj = [[NSObject alloc] init]; NSLog(@"class_getInstanceSize--%zd", class_getInstanceSize([NSObject class]));

```

輸出結果:

``` class_getInstanceSize--8

```

2、class object(類物件)/metaclass(元類)

看結構體objc_class的定義

``` struct objc_class { Class isa OBJC_ISA_AVAILABILITY;

if !OBJC2

Class super_class                                        OBJC2_UNAVAILABLE;
const char *name                                         OBJC2_UNAVAILABLE;
long version                                             OBJC2_UNAVAILABLE;
long info                                                OBJC2_UNAVAILABLE;
long instance_size                                       OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

endif

} OBJC2_UNAVAILABLE; / Use Class instead of struct objc_class * /

```

  • Class superclass;——用於獲取父類,也就是元類物件,它也是一個Class型別
  • cache_t cache;——是方法快取
  • class_data_bits_t bits;——用於獲取類的具體資訊,看到bits
  • class_rw_t data()函式,該函式的作用就是獲取該類的可讀寫資訊,通過class_data_bits_t的bits.data()方法獲得,class_rw_t後面會介紹*

``` class_rw_t data() { return (class_rw_t )(bits & FAST_DATA_MASK); }

```

該結構體的第一個成員變數也是isa指標,這就說明了Class本身其實也是一個物件,我們稱之為類物件。類物件中的元資料儲存的都是如何建立一個例項的相關資訊,那麼類物件和類方法應該從哪裡建立呢?就是從isa指標指向的結構體建立,類物件的isa指標指向的我們稱之為元類(metaclass),元類中儲存了建立類物件以及類方法所需的所有資訊。

3、isa指標與superclass相關邏輯圖

image.png

4、總結 + 程式碼校驗

  • 物件 的類(Superclass)是 類(物件) ;
  • 類(物件) 的類(Superclass)是 元類,和類同名;
  • 元類 的類(Superclass)是 根元類 NSObject;
  • 根元類 的類(Superclass)是 自己 ,還是NSObject;
  • 物件的isa指標指向類(物件) ;
  • 類物件的isa指標指向元類,和類同名;
  • 元類的isa指標指向跟根元類 NSObject;
  • 根元類 NSObject的isa指標指向自己。

isa驗證

``` NSString *string = @"字串"; Class class1 = object_getClass(string);//NSString類物件 Class metaClass = object_getClass(class1);//NSString元類 Class rootMetaClass = object_getClass(metaClass);//根元類 Class rootRootMetaClass = object_getClass(rootMetaClass);//根元類 NSLog(@"%p 例項物件 ",string); NSLog(@"%p 類 %@",class1,NSStringFromClass(class1)); NSLog(@"%p 元類 %@",metaClass,NSStringFromClass(metaClass)); NSLog(@"%p 根元類 %@",rootMetaClass,NSStringFromClass(rootMetaClass)); NSLog(@"%p 根根元類 %@",rootRootMetaClass,NSStringFromClass(rootRootMetaClass));

Class rootMetaClass_superclass = rootMetaClass.superclass;//根元類的superclass
NSLog(@"根根元類的superclass:%@",NSStringFromClass(rootMetaClass_superclass));

```

輸出結果:

``` 0x102d48078 例項物件 0x1d80e3d10 類 __NSCFConstantString 0x1d80e3cc0 元類 __NSCFConstantString 0x1d80c66c0 根元類 NSObject 0x1d80c66c0 根根元類 NSObject 根根元類的superclass:NSObject

```

superclass驗證

``` NSString *string = @"字串"; Class class1 = object_getClass(string);//NSString類物件 Class class2 = class1.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class1),NSStringFromClass(class2)); Class class3 = class2.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class2),NSStringFromClass(class3)); Class class4 = class3.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class3),NSStringFromClass(class4)); Class class5 = class4.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class4),NSStringFromClass(class5)); Class class6 = class5.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class5),NSStringFromClass(class6));

```

輸出結果:

``` __NSCFConstantString 的superclass是 __NSCFString __NSCFString 的superclass是 NSMutableString NSMutableString 的superclass是 NSString NSString 的superclass是 NSObject NSObject 的superclass是 (null)

```

二、class_rw_t 與 class_ro_t

1、class_ro_t 一"碼"當先:

``` struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize;

ifdef LP64

uint32_t reserved;

endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
    return baseMethodList;
}

};

```

  • uint32_t instanceSize;——instance物件佔用的記憶體空間
  • const char * name;——類名
  • const ivar_list_t * ivars;——類的成員變數列表

class_ro_t儲存了當前類在編譯期就已經確定的屬性、方法以及遵循的協議,裡面是沒有分類的方法的。那些執行時新增的方法將會儲存在執行時生成的class_rw_t中。 ro即表示read only,是無法進行修改的。

2、class_rw_t 一"碼"當先:

``` // 可讀可寫 struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version;

const class_ro_t *ro; // 指向只讀的結構體,存放類初始資訊

/*
 這三個都是二位陣列,是可讀可寫的,包含了類的初始內容、分類的內容。
 這三個二位陣列中的資料有一部分是從class_ro_t中合併過來的。
 */
method_array_t methods; // 方法列表(類物件存放物件方法,元類物件存放類方法)
property_array_t properties; // 屬性列表
protocol_array_t protocols; //協議列表

Class firstSubclass;
Class nextSiblingClass;

//...
}

```

3、class_rw_t生成時機

class_rw_t生成在執行時,在編譯期間,class_ro_t結構體就已經確定,objc_class中的bits的data部分存放著該結構體的地址。在runtime執行之後,具體說來是在執行runtime的realizeClass 方法時,會生成class_rw_t結構體,該結構體包含了class_ro_t,並且更新data部分,換成class_rw_t結構體的地址。

類的realizeClass執行之前:

image.png

然後在載入 ObjC 執行時的過程中在 realizeClass 方法中:

  • 從 class_data_bits_t 呼叫 data 方法,將結果從 class_rw_t 強制轉換為 class_ro_t 指標
  • 初始化一個 class_rw_t 結構體
  • 設定結構體 ro 的值以及 flag
  • 最後設定正確的 data。

``` const class_ro_t ro = (const class_ro_t )cls->data(); class_rw_t rw = (class_rw_t )calloc(sizeof(class_rw_t), 1); rw->ro = ro; rw->flags = RW_REALIZED|RW_REALIZING; cls->setData(rw);

```

但是,在這段程式碼執行之後 class_rw_t 中的方法,屬性以及協議列表均為空。這時需要 realizeClass 呼叫 methodizeClass 方法來將類自己實現的方法(包括分類)、屬性和遵循的協議載入到 methods、 properties 和 protocols 列表中。

realizeClass 方法執行過後的類所佔用記憶體的佈局:

image.png

細看兩個結構體的成員變數會發現很多相同的地方,他們都存放著當前類的屬性、例項變數、方法、協議等等。區別在於:class_ro_t存放的是編譯期間就確定的;而class_rw_t是在runtime時才確定,它會先將class_ro_t的內容拷貝過去,然後再將當前類的分類的這些屬性、方法等拷貝到其中。所以可以說class_rw_t是class_ro_t的超集,當然實際訪問類的方法、屬性等也都是訪問的class_rw_t中的內容。

4、method_t

上面我們剖析了class_rw_t、class_ro_t這兩個重要部分的結構,並且主要關注了其中的方法列表部分,而從上面的分析,可發現裡面最基本也是重要的單位是method_t,這個結構體包含了描述一個方法所需要的各種資訊。

``` struct method_t { SEL name; const char *types; IMP imp; };

```

變數介紹可以參考之前文章:iOS 程式碼注入—— hook 實踐

三、Runtime 初始化函式

1、一"碼"當先

``` /********** * _objc_init * Bootstrap initialization. Registers our image notifier with dyld. * Called by libSystem BEFORE library initialization time ***********/

void _objc_init(void) { static bool initialized = false; if (initialized) return; initialized = true;

// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

}

```

_dyld_objc_notify_register(&map_images, load_images, unmap_image)。這個函式裡面的三個引數分別是另外三個函式:

  • map_images -- Process the given images which are being mapped in by dyld.(處理那些正在被dyld對映的映象檔案)
  • load_images -- Process +load in the given images which are being mapped in by dyld.(處理那些正在被dyld對映的映象檔案中的+load方法)
  • unmap_image -- Process the given image which is about to be unmapped by dyld.(處理那些將要被dyld進行去對映操作的映象檔案)

我們檢視一下map_images方法,點進去:

``` /********** * map_images * Process the given images which are being mapped in by dyld. * Calls ABI-agnostic code after taking ABI-specific locks. * Locking: write-locks runtimeLock **********/ void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) { mutex_locker_t lock(runtimeLock); return map_images_nolock(count, paths, mhdrs); }

```

四、分類底層原理

根據map_images函式,繼續點進去看,可以看到如下程式碼:

``` // Discover categories. for (EACH_HEADER) { category_t **catlist = _getObjc2CategoryList(hi, &count); bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    for (i = 0; i < count; i++) {
        category_t *cat = catlist[i];
        Class cls = remapClass(cat->cls);

        if (!cls) {
            // Category's target class is missing (probably weak-linked).
            // Disavow any knowledge of this category.
            catlist[i] = nil;
            if (PrintConnecting) {
                _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                             "missing weak-linked target class", 
                             cat->name, cat);
            }
            continue;
        }

        // Process this category. 
        // First, register the category with its target class. 
        // Then, rebuild the class's method lists (etc) if 
        // the class is realized. 
        bool classExists = NO;
        if (cat->instanceMethods ||  cat->protocols  
            ||  cat->instanceProperties) 
        {
            addUnattachedCategoryForClass(cat, cls, hi);
            if (cls->isRealized()) {
                remethodizeClass(cls);
                classExists = YES;
            }
            if (PrintConnecting) {
                _objc_inform("CLASS: found category -%s(%s) %s", 
                             cls->nameForLogging(), cat->name, 
                             classExists ? "on existing class" : "");
            }
        }

        if (cat->classMethods  ||  cat->protocols  
            ||  (hasClassProperties && cat->_classProperties)) 
        {
            addUnattachedCategoryForClass(cat, cls->ISA(), hi);
            if (cls->ISA()->isRealized()) {
                remethodizeClass(cls->ISA());
            }
            if (PrintConnecting) {
                _objc_inform("CLASS: found category +%s(%s)", 
                             cls->nameForLogging(), cat->name);
            }
        }
    }
}

```

根據程式碼:

``` category_t *cat = catlist[i];

```

一開始的那個catlist是一個二維陣列,裡面的成員也是一個一個的陣列,也就是程式碼裡面的cat所指向的陣列,它的型別是category_t *,說明cat數組裡面裝的就是category_t,一個cat裡面裝的就是某個class所對應的所有category。

那麼什麼決定了這些category_t在cat陣列中的順序呢? 答案是category檔案的編譯順序決定的。先參與編譯的,就放在陣列的前面,後參與編譯的,就放在陣列後面。我們可以在xcode-->target-->Build Phases-->Compile Sources列表檢視和調整category檔案的編譯順序

載入分類的最後,執行方法:remethodizeClass(cls->ISA());

``` static void remethodizeClass(Class cls) { category_list *cats; bool isMeta;

runtimeLock.assertLocked();

isMeta = cls->isMetaClass();

// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
    if (PrintConnecting) {
        _objc_inform("CLASS: attaching categories to class '%s' %s", 
                     cls->nameForLogging(), isMeta ? "(meta)" : "");
    }

    attachCategories(cls, cats, true /*flush caches*/);        
    free(cats);
}

}

```

然後在這裡面找到一個方法attachCategories,看名字就知道,附著分類,也就是把分類的內容新增/合併到class裡面,感興趣的可以自己檢視一下這個方法,這個理就不做解釋了。

五、方法快取

1、資料結構

它的底層是通過散列表(雜湊表)的資料結構來實現的,用於快取曾經呼叫過的方法,可以提高方法的查詢速度。 首先,回顧一下正常情況下方法呼叫的流程。假設我們呼叫一個例項方法[obj XXXX];

  • obj -> isa -> obj的Class物件 -> method_array_t methods -> 對該表進行遍歷查詢,找到就呼叫,沒找到繼續往下走
  • obj -> superclass -> obj的父類 -> isa -> method_array_t methods -> 對父類的方法列表進行遍歷查詢,找到就呼叫,沒找到就重複本步驟
  • 直到NSObject -> isa -> NSObject的Class物件 -> method_array_t,如果還是沒有找到就會crash

如果XXXX方法在程式內會被頻繁的呼叫,那麼這種逐層便利查詢的方式肯定是效率低下的,因此蘋果設計了cache_t cache,當XXXX第一次被呼叫的時候,會按照常規流程查詢,找到之後,就會被加入到cache_t cache中,當再次被呼叫的時候,系統就會直接現到cache_t cache來查詢,找到就直接呼叫,這樣便大大提升了查詢的效率。

``` struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; }

```

  • struct bucket_t *_buckets; —— 用來快取方法的雜湊/雜湊表
  • mask_t _mask; —— 這個值 = 散列表長度 - 1
  • mask_t _occupied; —— 表示已經快取的方法的數量

_buckets散列表裡面的儲存單元是bucket_t,

``` struct bucket_t { private: cache_key_t _key; IMP _imp; }

```

  • cache_key_t _key; —— 這個key實際上就是方法的SEL,也就是方法名
  • IMP _imp; —— 這個就是方法對應的函式的記憶體地址

2、快取邏輯

  • (1) 當一個物件接收到訊息時[obj message];,首先根據obj的isa指標進入它的類物件class裡面。
  • (2) 在obj的class裡面,首先到快取cache_t裡面查詢方法message的函式實現,如果找到,就直接呼叫該函式。
  • (3) 如果上一步沒有找到對應函式,在對該class的方法列表進行二分/遍歷查詢,如果找到了對應函式,首先會將該方法快取到obj的類物件class的cache_t裡面,然後對函式進行呼叫。
  • (4) 在每次進行快取操作之前,首先需要檢查快取容量,如果快取內的方法數量超過規定的臨界值(設定容量的3/4),需要先對快取進行2倍擴容,原先快取過的方法全部丟棄,然後將當前方法存入擴容後的新快取內。
  • (5) 如果在obj的class物件裡面,發現快取和方法列表都找不到mssage方法,則通過class的superclass指標進入它的父類物件father_class裡面
  • (6) 進入father_class後,首先在它的cache_t裡面查詢mssage,如果找到了該方法,那麼會首先將方法快取到訊息接受者obj的類物件class的cache_t裡面,然後呼叫方法對應的函式。
  • (7) 如果上一步沒有找到方法,將會對father_class的方法列表進行遍歷二分/遍歷查詢,如果找到了mssage方法,那麼同樣,會首先將方法快取到訊息接受者obj的類物件class的cache_t裡面,然後呼叫方法對應的函式。需要注意的是,這裡並不會將方法快取到當前父類物件father_class的cache_t裡面
  • (8) 如果還沒找到方法,則會通過father_class的superclass進入更上層的父類物件裡面,按照(6)->(7)->(8)步驟流程重複。如果此時已經到了基類物件NSObject,仍沒有找到mssage,則進入步驟(9)

六、訊息轉發

第一步:Method resolution 方法解析處理階段 如果呼叫了物件方法首先會進行+(BOOL)resolveInstanceMethod:(SEL)sel判斷 如果呼叫了類方法 首先會進行 +(BOOL)resolveClassMethod:(SEL)sel判斷 兩個方法都為類方法;

``` + (BOOL)resolveClassMethod:(SEL)sel { ///這裡動態新增方法 return YES; } + (BOOL)resolveInstanceMethod:(SEL)sel { ///這裡動態新增方法 return YES; }

```

_class_resolveInstanceMethod原始碼解析

``` /********** * _class_resolveInstanceMethod * Call +resolveInstanceMethod, looking for a method to be added to class cls. * cls may be a metaclass or a non-meta class. * Does not check if the method already exists. ***********/ static void _class_resolveInstanceMethod(id inst, SEL sel, Class cls) { SEL resolve_sel = @selector(resolveInstanceMethod:);

if (! lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA())) {
    // Resolver not implemented.
    return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, 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 = lookUpImpOrNilTryCache(inst, sel, cls);

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

}

```

從runtime的原始碼,resolveInstanceMethod的返回值對於訊息轉發流程沒有任何意義,這個返回值只和debug的資訊相關。 這兩個方法是最先走到的方法,可以在這兩個方法中動態的新增方法,進行訊息轉發。這裡有一個需要特別注意的地方,類方法需要新增到元類裡面,原因這裡就不贅述了。

第二步:Fast forwarding 快速轉發階段

``` - (id)forwardingTargetForSelector:(SEL)aSelector { return [xxx new]; }

```

這個裡可以快速重定向成其他物件,已經讓備用的物件去響應了該物件本身無法響應的一個SEL

第三步:Normal forwarding 常規轉發階段

``` //返回方法簽名 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ if ([NSStringFromSelector(aSelector) isEqualToString:@"xxx"]) { return [[xxx new] methodSignatureForSelector:aSelector]; } return [super methodSignatureForSelector:aSelector]; }

//處理返回的方法簽名 -(void)forwardInvocation:(NSInvocation *)anInvocation{ if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"xxx"]) { [anInvocation invokeWithTarget:[xxx new]]; }else{ [super forwardInvocation:anInvocation]; } }

```

自動簽名

``` -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ //如果返回為nil則進行自動簽名 if ([super methodSignatureForSelector:aSelector]==nil) { NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"[email protected]:"]; return sign; } return [super methodSignatureForSelector:aSelector]; }

-(void)forwardInvocation:(NSInvocation *)anInvocation{ //建立備用物件 xxx * backUp = [xxx new]; SEL sel = anInvocation.selector; //判斷備用物件是否可以響應傳遞進來等待響應的SEL if ([backUp respondsToSelector:sel]) { [anInvocation invokeWithTarget:backUp]; }else{ // 如果備用物件不能響應 則丟擲異常 [self doesNotRecognizeSelector:sel]; } }

////觸發崩潰 - (void)doesNotRecognizeSelector:(SEL)aSelector {

}

```

七、super的本質

1、定義

  • super—— 是一個指向結構體指標struct objc_super *,它裡面的內容是{訊息接受者 recv, 訊息接受者的父類類物件 [[recv superclass] class]},objc_msgSendSuper會將訊息接受者的父類類物件作為訊息查詢的起點。

2、流程 [obj message] -> 在obj的類物件cls查詢方法 -> 在cls的父類物件[cls superclass]查詢方法 -> 在更上層的父類物件查詢方法 -> ... -> 在根類類物件 NSObject裡查詢方法

[super message] -> ~~在obj的類物件cls查詢方法~~(跳過此步驟) -> (直接從這一步開始)在cls的父類物件[cls superclass]查詢方法 -> 在更上層的父類物件查詢方法 -> ... -> 在根類類物件 NSObject裡查詢方法

3、例項

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

```

  • 接受者 當前class例項物件
  • 最終呼叫的方法:基類NSObject的-(Class)class方法

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

```

  • 接受者 當前class例項物件
  • 最終呼叫的方法:基類NSObject的-(Class)class方法

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

```

  • 接受者 當前class例項物件
  • 最終呼叫的方法:基類NSObject的-(Class) superclass方法

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

```

  • 接受者 當前class例項物件
  • 最終呼叫的方法:基類NSObject的-(Class) superclass方法

因此 [self class] [super class] [self superclass] [super superclass] 的值都相等

至此,runtime相關的知識點全部總結完畢,該文章將會持續更新迭代!! 看到就是緣分😁,如發現任何有誤之處,肯請留言糾正,謝謝。