面試題
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)用
圖解佐證
解釋:[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; // 屬性列表
};
1.從結(jié)構(gòu)體可以知道,有
屬性列表
,所以分類可以聲明屬性
,但是分類只會生成該屬性
對應(yīng)的get
和set
的聲明,沒有去實(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ù)的前面
合并方法圖解
- 代碼例子佐證
// 原來的類和分類看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é)果
通過運(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)用)
- 1.先調(diào)用類的+load
代碼例子及圖解佐證
通過打印結(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方法
并不是每一個類都實(shí)現(xiàn)了+ initialize方法
先調(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)set
和get
方法的聲明,不會去實(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)錯
實(shí)例代碼 - 使用點(diǎn)語法賦值
Student *stu = [[Student alloc] init];
stu.name = @"韓雪";
運(yùn)行結(jié)果 - 奔潰
實(shí)例代碼 - 使用點(diǎn)語法取值
Student *stu = [[Student alloc] init];
NSLog(@"name = %@",stu.name);
運(yùn)行結(jié)果 - 奔潰
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_setIvarLayout
或class_setWeakIvarLayout
來處理strong
,weak
引用,所以不能向存在的類中增加實(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)成功
但是注意,以上代碼僅僅是手動實(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
編譯時警告
作用
- 為一個類添加額外的原來沒有變量,方法和屬性
- 一般的類擴(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