1. 分類是什么
分類是利用 OC 的動態(tài)運行時機制,實現(xiàn)對一個現(xiàn)有類的功能進行擴展而不必知道該類的源碼奖唯。所以分類不僅可以擴展自定義類,也可以擴展系統(tǒng)類如 NSString 等糜值;分類提供了一種比繼承更簡潔的方法對類進行功能擴展丰捷,無需創(chuàng)建子類就能為現(xiàn)有類添加新的方法。
底層結構
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 對象方法
struct method_list_t *classMethods; // 類方法
struct protocol_list_t *protocols; // 協(xié)議
struct property_list_t *instanceProperties; // 屬性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
從源碼基本可以看出平時使用categroy的方式寂汇,對象方法病往,類方法,協(xié)議骄瓣,和屬性都可以找到對應的存儲方式停巷。并且發(fā)現(xiàn)分類結構體中是不存在成員變量的,因此分類中是不允許添加成員變量的榕栏。
由源碼可知畔勤,分類的方法和屬性列表是存儲在分類結構體中的,在運行時扒磁,會檢查現(xiàn)有類是否存在分類庆揪,存在的話會遍歷分類列表,將各個分類下對應的方法及屬性列表插入到現(xiàn)有類的方法和屬性列表前面妨托,這樣做的目的是為了保證分類方法優(yōu)先調(diào)用缸榛,我們知道當分類重寫本類的方法時,會覆蓋本類的方法兰伤。其實本質(zhì)上并不是覆蓋内颗,而是優(yōu)先調(diào)用。本類的方法依然在內(nèi)存中的敦腔。
- 以下示例均以該類為原有類:
Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
{
NSString *_name;
NSInteger _age;
}
- (void)speak;
@end
NS_ASSUME_NONNULL_END
Person.m
#import "Person.h"
@implementation Person
-(void)speak
{
NSLog(@"Person---speak");
}
-(void)walk
{
NSLog(@"Person---walk");
}
@end
2.category的作用
(1)將類的實現(xiàn)分散到多個不同的文件中均澳,方便代碼管理,也可以對框架提供的類進行擴展会烙。(可以減少單個類的體積负懦,把不同的功能組織到不同的 category 里筒捺,可以由多個開發(fā)者共同完成一個類柏腻,可以按需加載想要的 category);
Person + Test1.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Test1)
- (void)eat;
@end
NS_ASSUME_NONNULL_END
Person+Test1.m
#import "Person+Test1.h"
@implementation Person (Test1)
- (void)eat{
NSLog(@"Person+Test1---eat");
}
@end
(2)創(chuàng)建對私有方法的前向引用:如果原有類中有未在頭文件中聲明的私有方法,你在訪問該私有方法的時候編譯器就會報錯系吭,這時就可以使用分類五嫂,在分類中聲明該私有方法將其公開(不必提供方法實現(xiàn));
Person + Test2.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Test2)
- (void)walk;
@end
NS_ASSUME_NONNULL_END
Person + Test2.h
#import "Person+Test2.h"
@implementation Person (Test2)
@end
在其他類中訪問原有類的私有方法:
#import "ViewController.h"
#import "Person.h"
#import "Person+Test1.h"
#import "Person+Test2.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc]init];
//原有類方法
[person speak];
//分類擴展方法
[person eat];
//利用方法創(chuàng)建對私有方法的前向引用
[person walk];
}
@end
(3)向?qū)ο筇砑臃钦絽f(xié)議:創(chuàng)建一個 NSObject 的分類稱為“創(chuàng)建一個非正式協(xié)議”,因為可以作為任何類(NSObject 為任意類的父類)的委托對象使用(聲明私有方法)沃缘。
NSObject+Test3.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (Test3)
- (void)sleep;
@end
NS_ASSUME_NONNULL_END
NSObject+Test3.m
#import "NSObject+Test3.h"
@implementation NSObject (Test3)
-(void)sleep
{
NSLog(@"NSObject + Test3 --- sleep");
}
@end
Person *person = [[Person alloc]init];
//原有類方法
[person speak];
//分類擴展方法
[person eat];
//利用方法創(chuàng)建對私有方法的前向引用
[person walk];
[person sleep];
3.category的局限性
- category 只能給某個已有類擴充方法躯枢,不能擴充成員變量;
- category中也可以添加屬性槐臀,只不過@property只會生成setter和getter的聲明锄蹂,不會生成setter和getter的實現(xiàn)(.調(diào)用會崩潰,找不到方法實現(xiàn)水慨〉妹樱可以使用runtime去實現(xiàn)Category為已有的類添加新的屬性并生成getter和setter方法)以及成員變量。
- 果category中的方法和類中原有方法同名晰洒,運行時會優(yōu)先調(diào)用category中的方法朝抖。
-
如果多個category中存在同名的方法,運行時到底調(diào)用哪個方法由編譯器決定谍珊,最后一個參與編譯的方法會被調(diào)用治宣。屏幕快照 2018-10-21 16.09.33.png
4.常見問題總結
-
Category的實現(xiàn)原理,以及Category為什么只能加方法不能加屬性?
答:分類的實現(xiàn)原理是將category中的方法砌滞,屬性侮邀,協(xié)議數(shù)據(jù)放在category_t結構體中,然后將結構體內(nèi)的方法列表拷貝到類對象的方法列表中贝润。
Category可以添加屬性豌拙,但是并不會自動生成成員變量及set/get方法。因為category_t結構體中并不存在成員變量题暖。通過之前對對象的分析我們知道成員變量是存放在實例對象中的按傅,并且編譯的那一刻就已經(jīng)決定好了。而分類是在運行時才去加載的胧卤。那么我們就無法再程序運行時將分類的成員變量中添加到實例對象的結構體中唯绍。因此分類中不可以添加成員變量。 -
Category中有l(wèi)oad方法嗎枝誊?load方法是什么時候調(diào)用的况芒?load 方法能繼承嗎?
答:Category中有l(wèi)oad方法叶撒,load方法在程序啟動裝載類信息的時候就會調(diào)用绝骚。load方法可以繼承。調(diào)用子類的load方法之前祠够,會先調(diào)用父類的load方法压汪。
-
load、initialize的區(qū)別古瓤,以及它們在category重寫的時候的調(diào)用的次序止剖。
答:區(qū)別在于調(diào)用方式和調(diào)用時刻
調(diào)用方式:load是根據(jù)函數(shù)地址直接調(diào)用腺阳,initialize是通過objc_msgSend調(diào)用
調(diào)用時刻:load是runtime加載類、分類的時候調(diào)用(只會調(diào)用1次)穿香,initialize是類第一次接收到消息的時候調(diào)用亭引,每一個類只會initialize一次(父類的initialize方法可能會被調(diào)用多次)調(diào)用順序:先調(diào)用類的load方法,在調(diào)用load之前會先調(diào)用父類的load方法皮获。分類中l(wèi)oad方法不會覆蓋本類的load方法焙蚓,先編譯的分類優(yōu)先調(diào)用load方法。initialize先初始化父類洒宝,之后再初始化子類主届。如果子類沒有實現(xiàn)+initialize,會調(diào)用父類的+initialize(所以父類的+initialize可能會被調(diào)用多次)待德,如果分類實現(xiàn)了+initialize君丁,就覆蓋類本身的+initialize調(diào)用。