iOS-底層原理(6)-Category(+load +initialize)底層原理詳解

面試題
1. Category的使用場合是什么?
2. Category的實(shí)現(xiàn)原理
  • Category編譯之后的底層結(jié)構(gòu)是struct category_t,里面存儲著分類的對象方法、類方法、屬性、協(xié)議信息
  • 在程序運(yùn)行的時候,runtime會將Category的數(shù)據(jù),合并到類信息中(類對象、元類對象中)
3. Category和Class Extension的區(qū)別是什么?
  • Class Extension在編譯的時候,它的數(shù)據(jù)就已經(jīng)包含在類信息中
  • Category是在運(yùn)行時,才會將數(shù)據(jù)合并到類信息中
4. Category中有l(wèi)oad方法嗎?load方法是什么時候調(diào)用的?load 方法能繼承嗎?
  • 有l(wèi)oad方法

  • load方法在runtime加載類、分類的時候調(diào)用

  • load方法可以繼承,但是一般情況下不會主動去調(diào)用load方法,都是讓系統(tǒng)自動調(diào)用

  • 圖解佐證

image.png
image.png

解釋:[Student load];,即走objc_msgSend消息機(jī)制,先通過isa指針找到其類,發(fā)現(xiàn)類中沒有實(shí)現(xiàn)該方法,然后通過superclass找到其父類,發(fā)現(xiàn)父類有實(shí)現(xiàn),但是分類方法在前面,后面參與編譯的在前面,所以最終調(diào)用的是分類Person(Eat)方法。

5.Category能否添加成員變量?如果可以,如何給Category添加成員變量?
  • 不能直接給Category添加成員變量,但是可以間接實(shí)現(xiàn)Category有成員變量的效果
6. load、initialize方法的區(qū)別什么?它們在category中的調(diào)用的順序?以及出現(xiàn)繼承時他們之間的調(diào)用過程?

load、initialize方法的區(qū)別什么?
1.調(diào)用方式
1> load是根據(jù)函數(shù)地址直接調(diào)用
2> initialize是通過objc_msgSend調(diào)用

2.調(diào)用時刻
1> load是runtime加載類、分類的時候調(diào)用(只會調(diào)用1次)
2> initialize是類第一次接收到消息的時候調(diào)用,每一個類只會initialize一次(父類的initialize方法可能會被調(diào)用多次)

load、initialize的調(diào)用順序?
1.load
1> 先調(diào)用類的load
a) 先編譯的類,優(yōu)先調(diào)用load
b) 調(diào)用子類的load之前,會先調(diào)用父類的load

2> 再調(diào)用分類的load
a) 先編譯的分類,優(yōu)先調(diào)用load

2.initialize
1> 先初始化父類
2> 再初始化子類(可能最終調(diào)用的是父類的initialize方法)

Category的底層結(jié)構(gòu)
  • 定義在objc-runtime-new.h
struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;  // 對象方法列表
    const struct _method_list_t *class_methods;  // 類方法列表
    const struct _protocol_list_t *protocols;  // 協(xié)議列表
    const struct _prop_list_t *properties;  // 屬性列表
};
Category結(jié)構(gòu).png

1.從結(jié)構(gòu)體可以知道,有屬性列表,所以分類可以聲明屬性,但是分類只會生成該屬性對應(yīng)的getset的聲明,沒有去實(shí)現(xiàn)該方法。
2.結(jié)構(gòu)體沒有成員變量列表,所以不能聲明成員變量

  • 源碼解讀順序

  • objc-os.mm

    • _objc_init
    • map_images
    • map_images_nolock
  • objc-runtime-new.mm

    • _read_images
    • remethodizeClass
    • attachCategories
    • attachLists
    • realloc、memmove、 memcpy
Category的加載處理過程
  • 1.通過Runtime加載某個類的所有Category數(shù)據(jù)
  • 2.把所有Category的方法、屬性、協(xié)議數(shù)據(jù),合并到一個大數(shù)組中,后面參與編譯的Category數(shù)據(jù),會在數(shù)組的前面
  • 3.將合并后的分類數(shù)據(jù)(方法、屬性、協(xié)議),插入到類原來數(shù)據(jù)的前面
合并方法圖解
合并方法.png
  • 代碼例子佐證
// 原來的類和分類看Demo,這里就不列舉出來了
// 開始調(diào)用
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [[Person alloc] init];
        [person run];
        
//        objc_msgSend(person, @selector(run));
        [person test];
        
//        objc_msgSend(person, @selector(eat));
        [person eat];
        
        // 通過runtime動態(tài)將分類的方法合并到類對象、元類對象zhong
    }
    return 0;
}

運(yùn)行結(jié)果

image.png
image.png

通過運(yùn)行結(jié)果可知,分類方法會覆蓋原來類對象方法,并且最后參與編譯的會在調(diào)用順序最前面。

+load方法

  • +load方法會在runtime加載類、分類時調(diào)用

  • 每個類、分類的+load,在程序運(yùn)行過程中只調(diào)用一次

  • 調(diào)用順序

    • 1.先調(diào)用類的+load
      • 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
      • 調(diào)用子類的+load之前會先調(diào)用父類的+load
    • 2.再調(diào)用分類的+load
      • 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
  • 代碼例子及圖解佐證

image.png

通過打印結(jié)果,+load調(diào)用順序符合上述提到的調(diào)用順序

objc4源碼解讀過程:objc-os.mm
  • _objc_init

  • load_images

  • prepare_load_methods

    • schedule_class_load
    • add_class_to_loadable_list
    • add_category_to_loadable_list
  • call_load_methods

    • call_class_loads
    • call_category_loads
    • (*load_method)(cls, SEL_load)
  • +load方法是根據(jù)方法地址直接調(diào)用,并不是經(jīng)過objc_msgSend函數(shù)調(diào)用


+ initialize方法講解
  • +initialize方法會在類第一次接收到消息時調(diào)用
  • 調(diào)用順序
    • 先調(diào)用父類的+initialize,再調(diào)用子類的+initialize
    • (先初始化父類,再初始化子類,每個類只會初始化1次)
objc4源碼解讀過程
  • objc-msg-arm64.s

    • objc_msgSend
  • objc-runtime-new.mm

    • class_getInstanceMethod
    • lookUpImpOrNil
    • lookUpImpOrForward
    • _class_initialize
    • callInitialize
    • objc_msgSend(cls, SEL_initialize)

項(xiàng)目例子佐證 -

  • 每個類都實(shí)現(xiàn)了+ initialize方法
image.png
  • 并不是每一個類都實(shí)現(xiàn)了+ initialize方法
image.png
  • 先調(diào)用父類的+initialize,再調(diào)用子類的+initialize
  • (先初始化父類,再初始化子類,每個類只會初始化1次)

解析
1.[Student alloc]會調(diào)用+initialize方法,因?yàn)樗懈割怭erson,所以先調(diào)用Person的+initialize方法,又因?yàn)榉诸愒谇懊妫哉{(diào)用了Person(Test2)的+initialize方法。但是他自己本身沒有實(shí)現(xiàn)+initialize方法,所以會去父類查找,然后分類方法在前面,所以調(diào)用了Person(Test2)的+initialize方法。
2.[Teacher alloc]會調(diào)用+initialize方法,因?yàn)樗懈割怭erson,所以先調(diào)用Person的+initialize方法,但是前面已經(jīng)初始化過了,所以跳過,調(diào)用自己的+initialize方法,但是因?yàn)樗约簺]有實(shí)現(xiàn)+initialize方法,所以調(diào)用父類的+initialize方法,又因?yàn)榉诸惙椒ㄔ谇懊妫哉{(diào)用Person(Test) +initialize方法。
3.[Person alloc],因?yàn)榍懊嬉呀?jīng)初始化過了,所以不會再調(diào)+initialize方法,所以這里不打印。

+initialize和+load的區(qū)別
  • +initialize是通過objc_msgSend進(jìn)行調(diào)用的,所以有以下特點(diǎn)
    • 如果子類沒有實(shí)現(xiàn)+initialize,會調(diào)用父類的+initialize(所以父類的+initialize可能會被調(diào)用多次)
    • 如果分類實(shí)現(xiàn)了+initialize,就覆蓋類本身的+initialize調(diào)用
二 分類為啥不能添加屬性(成員變量)

注意:
1.分類是用于給原有類添加方法的,因?yàn)榉诸惖慕Y(jié)構(gòu)體指針中,沒有成員變量列表。所以< 原則上講它只能添加方法, 不能添加成員變量,實(shí)際上可以通過其它方式添加成員變量 ;
2.分類中的可以寫@property, 但只會生成對應(yīng)setget方法的聲明,不會去實(shí)現(xiàn)setter/getter方法以及私有的成員變量(編譯時會報(bào)警告);
3.可以在分類中訪問原有類中.h中的屬性;

實(shí)例代碼如下:

  • Student.h
@interface Student : NSObject
/** class */
@property(nonatomic, copy)NSString *className;
@end
  • Student+Extern.h
@interface Student (Extern)
/** name */
@property(nonatomic, copy)NSString *name;
@end

那么問題來了:
1.為什么在分類中聲明屬性時,運(yùn)行不會出錯呢?
2.既然分類不讓添加屬性,那為什么我寫了@property仍然還以編譯通過呢?

接下來我們探究下分類不能添加屬性的實(shí)質(zhì)原因:

1.我們知道在一個類中用@property聲明屬性,編譯器會自動幫我們生成_成員變量setter/getter,但分類的指針結(jié)構(gòu)體中,根本沒有成員變量列表。所以在分類中用@property聲明屬性,既無法生成_成員變量也無法生成setter/getter的實(shí)現(xiàn)。
2.因此結(jié)論是:我們可以用@property聲明屬性,編譯和運(yùn)行都會通過,只要不使用程序也不會崩潰。但如果調(diào)用了_成員變量setter/getter方法,報(bào)錯就在所難免了。

實(shí)例代碼 - 訪問_成員變量

  • Student.m
- (instancetype)init {
    self = [super init];
    if (self) {
        _className = @"大學(xué)";
        _name = @"";
    }
    return self;
}

直接報(bào)錯

image.png

實(shí)例代碼 - 使用點(diǎn)語法賦值

Student *stu = [[Student alloc] init];
stu.name = @"韓雪";

運(yùn)行結(jié)果 - 奔潰

image.png

實(shí)例代碼 - 使用點(diǎn)語法取值

Student *stu = [[Student alloc] init];
NSLog(@"name = %@",stu.name);

運(yùn)行結(jié)果 - 奔潰

image.png
2.2 面試題
2.2.1 能否向編譯后得到的類中增加實(shí)例變量?

不能
分析:因?yàn)榫幾g后的類已經(jīng)注冊在runtime中,類結(jié)構(gòu)體中的objec_ivar_list實(shí)例變量的鏈表和instance_size實(shí)例變量的內(nèi)存大小已經(jīng)確定,同時runtime會調(diào)用class_setIvarLayoutclass_setWeakIvarLayout來處理strongweak引用,所以不能向存在的類中增加實(shí)例變量。

2.2.1 能否向運(yùn)行時創(chuàng)建的類中增加實(shí)例變量?

可以
分析:運(yùn)行時創(chuàng)建的類是可以添加實(shí)例變量,調(diào)用class_addIvar函數(shù),但是得在調(diào)用objc_allocateClassPair之后,objc_registerClassPair之前,原因如上。

三 關(guān)聯(lián)對象給分類添加屬性

代碼實(shí)現(xiàn)如下

  • Student+Extern.m
#import "Student+Extern.h"
#import <objc/runtime.h>

static NSString *nameKey = @"nameKey";   //定義一個key值

@implementation Student (Extern)

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, &nameKey);
}

@end

外界調(diào)用

Student *stu = [[Student alloc] init];
stu.name = @"韓雪";
NSLog(@"name = %@",stu.name);

運(yùn)行結(jié)果 - 關(guān)聯(lián)成功

image.png

但是注意,以上代碼僅僅是手動實(shí)現(xiàn)了setter/getter方法,但調(diào)用_成員變量依然報(bào)錯。

三 類擴(kuò)展(Class Extension)

Extension是Category的一個特例。類擴(kuò)展與分類相比只少了分類的名稱,所以稱之為“匿名分類”。
其實(shí)開發(fā)當(dāng)中,我們幾乎天天在使用。對于有些人來說像是最熟悉的陌生人。

類擴(kuò)展格式:

@interface XXX ()
//私有屬性
//私有方法(如果不實(shí)現(xiàn),編譯時會報(bào)警,Method definition for 'XXX' not found)
@end
  • 實(shí)例代碼
@interface Student()
/** height */
@property(nonatomic, assign)int height;

- (void)printInfo;

@end

@implementation Student

@end

編譯時警告

image.png

作用

  • 為一個類添加額外的原來沒有變量,方法和屬性
  • 一般的類擴(kuò)展寫到.m文件中
  • 一般的私有屬性寫到.m文件中的類擴(kuò)展中
類別與類擴(kuò)展的區(qū)別:
  • 1 類別中原則上只能增加方法(能添加屬性的的原因只是通過runtime解決無setter/getter的問題而已);
  • 2 類擴(kuò)展不僅可以增加方法,還可以增加實(shí)例變量(或者屬性),只是該實(shí)例變量默認(rèn)是@private類型的(使用范圍只能在自身類,而不是子類或其他地方);
  • 3 類擴(kuò)展中聲明的方法沒被實(shí)現(xiàn),編譯器會報(bào)警,但是類別中的方法沒被實(shí)現(xiàn)編譯器是不會有任何警告的。這是因?yàn)?code>類擴(kuò)展是在編譯階段被添加到類中,而類別是在運(yùn)行時添加到類中
  • 4 類擴(kuò)展不能像類別那樣擁有獨(dú)立的實(shí)現(xiàn)部分(@implementation部分),也就是說,類擴(kuò)展所聲明的方法必須依托對應(yīng)類的實(shí)現(xiàn)部分來實(shí)現(xiàn)。
  • 5 定義在.m文件中的類擴(kuò)展方法為私有的,定義在.h 文件(頭文件)中的類擴(kuò)展方法為公有的類擴(kuò)展是在 .m 文件中聲明私有方法的非常好的方式

本文參考借鑒MJ的教程視頻,非常感謝.

iOS分類(category),類擴(kuò)展(extension)—史上最全攻略


項(xiàng)目連接地址 - iOS-Category-load
項(xiàng)目連接地址 - iOS-Category-initialize

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容