很多人都知道類别、分類的用法,但是對于一些細節就不是很清楚了,本文主要梳理下這3個概念的細節
類别(Category)
檔案特征
- 類别檔案有2個,分别為 .h 和 .m
- 命名為: “類名+類别名.h”和“類名+類别名.m”
檔案内容格式
.h 檔案格式
#import "類名.h"
@interface 類名 (類别名)
// 在此處聲明方法
@end
.m 檔案格式
#import "類名+類别名.h"
@implementation 類名 (類别名)
// 在此處實作聲明的方法
@end
類别的作用
拓展目前類,為類添加方法
類别的局限性
- 無法向現有的類添加執行個體變量(編譯器報“instance variables may not be placed in categories”)。Category 一般隻為類提供方法的拓展,不提供屬性的拓展。但是利用 Runtime 可以在 Category 中添加屬性
- 方法名稱沖突的情況下,如果 Category 中的方法與目前類的方法名稱重名,Category 具有更高的優先級,類别中的方法将完全取代現有類中的方法(調用方法的時候不會去調用現有類裡面的方法實作)。
- 當現有類具有多個 Category 的時候,如果每個 Category 都有同名的方法,那麼在調用方法的時候肯定不會調用現有類的方法實作。系統根據編譯順序決定調用哪個 Category 下的方法實作。(可以在 Targets -> Build phases -> Compile Sources 下給多個 Category 更換順序看看到底在執行哪個方法)
Category 的使用和注意
- Category 中的方法如果和現有類方法一緻,工程中任何調用目前類的方法的時候都會去調用 Category 裡面的方法(比如:UIViewCtroller、UITableView這些)的方法時要慎重。因為用Category重寫類中的方法會對子類造成很大的影響。比如:用Category 重寫了 UIViewCtroller 的方法 A,那麼如果你在工程中用到的所有繼承自 UIViewCtroller 的子類,去調用方法 A 時,執行的都是 Category 中重寫的方法 A,如果不幸的是,你寫的方法 A 有 Bug,那麼會造成整個工程中調用該方法的所有 UIViewCtroller 子類的不正常。除非你在子類中重寫了父類的方法 A,這樣子類調用方法 A 時是調用的自己重寫的方法 A,消除了父類 Category 中重寫方法對自己的影響
- Category拓展方法按照有沒有重寫目前類中的方法,分為未重寫的拓展方法和重寫拓展方法。且類引用自己的 Category 時,隻能在 .m 檔案中引用(.h 檔案引用自己的類别會報錯)。子類引用父類的 Category 在 .h 或 .m 都可以。如果類調用 Category 中重寫的方法,不用引入 Category 頭檔案,系統會自動調用 Category 中的重寫方法
- Category 中如果重寫了 A 類從父類繼承來的某方法,不會影響與 A 同層級的 B 類
- 子類會不會繼承父類的 Category: Category 中重寫的方法會對子類造成影響,但是子類不會繼承非重寫的方法(現有類中沒有的方法)。但是在子類中引入父類 Category 的聲明檔案後,子類就會繼承 Category 的非重寫方法。繼承的表現是:當子類的方法和父類 Category 中的方法名完全相同,那麼子類裡的方法會覆寫掉父類 Category,相當于子類重寫了繼承自父類的方法
- Category 的作用是向下有效的。即隻會影響到該類的所有子類。比如 A 類和 B 類是繼承自 Super 類的2個子類,當給 A 類添加一個 Category sayHello 方法,僅有A 類的子類才可以使用 sayHello 方法
拓展(Extension)
檔案特征
- 隻存在一個檔案
- 命名方式:“類名_拓展名.h”
#import "類名.h"
@interface 類名 ()
// 在此添加私有成員變量、屬性、聲明方法
@end
拓展的作用
- 為類增加額外的屬性、成員變量、方法聲明
- 一般将類拓展直接寫到目前類的 .m 檔案中。不單獨建立
- 一般私有的屬性和方法寫到類拓展中
- 和 Category 類似,但是小括号裡面沒有拓展的名字
拓展的局限性
- Extension 中添加的屬性、成員變量、方法屬于私有(隻可以在本類的 .m 檔案中通路、調用。在其他類裡面是無法通路的,同時子類也是無法繼承的)。假如我們有這樣一個需求,一個屬性對外是隻讀的,對内是可以讀寫的,那麼我們可以通過 Extension 實作。
- 通常 Extension 都寫在 .m 檔案中,不會單獨建立一個 Extension 檔案。而且 Extension 必須寫到 @implementation 上方,否則編譯報錯
- 類拓展定義的方法和屬性必須在類的實作檔案中實作。如果單獨定義類擴充的檔案并且隻定義屬性的話,也需要将類實作檔案中包含進類擴充檔案,否則會找不到屬性的 setter 和 getter 方法。
//Web.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Web : Person
@end
NS_ASSUME_NONNULL_END
//Web.m
#import "Web.h"
#import "Web+H5.h"
@interface Web ()
@property (nonatomic, strong) NSString *skillStacks;
@end
@implementation Web
- (void)test {
self.skills = @"iOS && Web && Node && Hybrid";
self.skillStacks = @"iOS && Web && Node && Hybrid";
}
- (void)show {
NSLog(@"%@",self.skillStacks);
}
@end
總結
- Category 隻能拓充方法,不能拓展屬性和成員變量(包含成員變量會報錯。屬性雖然不可以直接拓展,利用 Runtime 可以實作)
- 如果 Category 中聲明了1個屬性,那麼 Category 隻會生成 setter 和 getter 的聲明,不會有實作
- Extension 也被成為匿名的 Category
- 分類的方法本質是追加在目前類方法清單後,是以分類的方法會覆寫目前類的方法。
「小插曲」:為 Category 實作屬性的 Setter 和 Getter
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Student)
/**< 學号*/
@property (nonatomic, strong) NSString *studyNumber;
@end
NS_ASSUME_NONNULL_END
#import "Person+Student.h"
#import <objc/message.h>
@implementation Person (Student)
- (void)sayHi {
NSLog(@"大家好,我叫%@,我今年%zd歲了",self.name,self.age);
}
/*
* 傳統的做法是在 setter 裡面這樣寫
_studyNumber = studyNumber;
ARC 自動管理記憶體
MRC
[_studyNumber release];
[studyNumber retain];
_studyNumber = studyNumber;
但是在 Category裡面不會生成對應的執行個體變量,是以我們可以利用 Runtime 為我們的 category 關聯屬性的值
setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
getter:objc_getAssociatedObject(self, @selector(firstView));
}
*/
- (void)setStudyNumber:(NSString *)studyNumber {
objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
, OBJC_ASSOCIATION_RETAIN);
}
//@selector(studyNumber)
- (NSString *)studyNumber {
return objc_getAssociatedObject(self, @selector(studyNumber));
}
@end
說明:
objc_setAssociatedObject
的第二個參數是
const void * _Nonnull key
是以可以用 “studyNumber” 或者利用
@selector()
的特性傳回的資料類型也滿足,是以示例代碼選用第二種方式
給分類添加屬性的時候,為了避免多人開發對于屬性添加造成的覆寫,我們需要為屬性起一個獨特的名字。比如我們的工程是元件化、子產品化開展的工程,那麼我們可以為屬性命名的時候在前面添加目前子產品的字首。
比如我們在 Login-Register-Module 子產品為 NSURL 的 Category 添加一個 title 的屬性的時候,可以這樣命名 LR_Title。請檢視下面的代碼
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (Title)
@property (nonatomic, copy) NSString *LR_title;
@end
NS_ASSUME_NONNULL_END
#import "NSURL+Title.h"
#import <objc/runtime.h>
@implementation NSURL (Title)
- (void)setLR_title:(NSString *)LR_title
{
objc_setAssociatedObject(self, @selector(LR_title), LR_title
, OBJC_ASSOCIATION_RETAIN);
}
- (NSString *)LR_title
{
return objc_getAssociatedObject(self, @selector(LR_title));
}
@end