關於iOS Class Category的整理

weixin_33670713發表於2017-12-12

參考

作用

  • 把一個類中的方法實現分成幾個檔案,這樣做的好處:
    a)可以減小單個檔案的體積。
    b)把不同功能的方法,分類在不同的檔案中。
    c)可由不同的開發者負責不同的分類。
    d)按需載入不同的分類。
  • 宣告私有方法(建立私有方法的前向引用)。

分類中的非私有方法,也是屬於原來那個類的方法,呼叫的時候,也是用原來的類去呼叫,而不是新的其它的類。
例如:Person

@interface Person : NSObject
@property(retain,nonatomic)NSString* name;
-(void)printName;
@end

@implementation Person
-(void)printName{
    NSLog(@"%@",self.name);
}
@end

以及它的分類Person+Skill

@interface Person (Skill)
-(void)eatSkill;
@end

@implementation Person (Skill)
-(void)eatSkill{
    NSLog(@"人的技能之一:吃飯");
}
@end

那麼呼叫分類的方法,其實就是Person呼叫自己的方法一樣:

#import "Person.h"
#import "Person+Skill.h"

Person *person = [Person alloc]init];
person.name = @"wxx";
[person printName];
[person eatSkill];

宣告私有方法(建立私有方法的前向引用)

我們都知道,在Objective-C中,建立一個方法,如果在.h檔案中宣告瞭(.m中實現),那麼這個方法屬於公有方法,也就是public。如果只是在Extension(類擴充套件)中宣告(關於Extension點這裡),或者不宣告,那麼就是屬於私有方法,也就是private。在Extension中宣告方法如下:

@interface Person(){
}
-(void)privateMothod;
@end

預設情況下,私有方法是不能在其他類中使用的,強制使用,Xcode編譯器會報錯。如果非要訪問私有方法,可以新建該方法所屬類的分類,在分類的.h中宣告該方法,.m中不用實現。嚴格意義上來說,Objective-C中,是沒有私有方法的。後面會說到為什麼
下面是一個例子:

在類中建立一個私有方法,擴充套件類可宣告可不宣告

#import "Person.h"

@interface Person(){
}
-(void)privateMethod;
@end


@implementation Person

-(void)privateMethod{
    NSLog(@"this is class private method");
}

@end

分類中.h宣告該私有方法:

#import "Person.h"

@interface Person (PrivateMethod)
//只宣告,不實現
-(void)privateMethod;
@end


#import "Person+PrivateMethod.h"
@implementation Person (PrivateMethod)
@end

最終執行:

#import "Person.h"
#import "Person+PrivateMethod.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc]init];
    [person privateMethod];
}
@end

執行結果:

this is class private method

通過上面的方法,實現了呼叫類中的私有方法,由此得到以下結論

Objective-C中,是沒有私有方法的

  • Q:為什麼說Objective-C中,是沒有私有方法的?
    A:因為通過執行時,我們可以獲取到一個類中的所有方法名;而有了方法名,可以如上新增分類的方式,呼叫私有方法!

  • Q:如何獲取一個類中的所有方法名?
    A:如下所示:

unsigned int count;
Method *methods = class_copyMethodList([Person class], &count);
for (int i = 0; i < count; i++) {
    Method method = methods[i];
    SEL selector = method_getName(method)
    NSString *name = NSStringFromSelector(selector);
    NSLog(@"%@",name);
}

列印結果:

2017-12-11 17:19:41.386635+0800 WxxDemo[2068:829821] eatSkill
2017-12-11 17:19:41.386780+0800 WxxDemo[2068:829821] haha
2017-12-11 17:19:41.386912+0800 WxxDemo[2068:829821] setHaha:
2017-12-11 17:19:41.387020+0800 WxxDemo[2068:829821] printName
2017-12-11 17:19:41.387116+0800 WxxDemo[2068:829821] privateMethod
2017-12-11 17:19:41.387252+0800 WxxDemo[2068:829821] .cxx_destruct
2017-12-11 17:19:41.387404+0800 WxxDemo[2068:829821] name
2017-12-11 17:19:41.387495+0800 WxxDemo[2068:829821] setName:

可以看到,私有方法名稱(例如:privateMethod),是可以獲取到的!如何呼叫私有方法,相信大家已經很清楚。

分類中新增屬性

直接在分類.h中新增屬性(嚴格意義上來講,不是新增,而是宣告。):

@interface Person (Skill)
@property (nonatomic,copy)NSString *haha;
@end

在分類的.m檔案中,就會出現如下的警告:


1237662-f68747e1b428ee06.png
分類中宣告一個屬性的警告.png

以上警告,告訴我們:需要手動新增方法,或者使用@dynamic在執行時動態實現關聯屬性

如果沒有按照警告的做法來實現,直接呼叫該屬性:

person.haha = @"哈哈";

就會報出如下的crach,果然上面的警告不能忽略啊:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person setHaha:]: unrecognized selector sent to instance 0x60000001f7a0'

@property分析

@interface Person : NSObject
@property (nonatomic,copy)NSString *haha;
@end

實際上,@property可以看成是Objective-C 程式設計中的“巨集”,它有超程式設計的思想。上面這段程式碼,實際上會做三件事:

  • 生成例項變數 _haha
  • 生成 getter 方法 - haha
  • 生成 setter 方法 - setHaha:
@implementation DKObject {
    NSString * _haha;
}

- (NSString *) haha {
    return _haha;
}

- (void)setHaha:(NSString *) haha {
    _haha = haha;
}

@end

Objective-C的.語法,是一種語法糖,.haha用於=左邊是賦值,也就是set;.haha用於=右邊,是取值,也就是get。
當然,這三件事都是編譯器幫你生成程式碼,雖然沒有看到,但是確實是存在的。而分類中,就不會自動生成例項變數,所以需要手動關聯屬性。

而通過檢視category的定義,我們也可以看出一些端倪:

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

我們可以看到,category中,有例項方法列表instance_methods,類方法列表class_methods,屬性列表protocols,唯獨沒有例項變數列表:ivars。因此,我們只能通過其他的途徑去實現。

使用關聯物件

既然分類中,編譯器無法自動為屬性生成例項變數,也無法自動生成get/set方法,那麼我們只能在執行時動態的關聯屬性
如下:


#import "Person+Skill.h"
#import <objc/runtime.h>

static void *hahaKey = &hahaKey;
@implementation Person (Skill)

-(NSString*)haha{
    return objc_getAssociatedObject(self, hahaKey);
}

-(void)setHaha:(NSString *)haha{
    objc_setAssociatedObject(self, hahaKey, haha, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

如此一來,不會再有警告,也能夠正常的使用該屬性的例項變數,而不會crach:

person.haha = @"哈哈";
NSLog(@"%@",person.haha);

動態關聯的相關方法中,key值的幾種寫法,利用靜態變數地址唯一不變的特性

  • 1、static const void *hahaKey = & hahaKey;
  • 2、static const NSString * hahaKey = @"hahaKey";
  • 3、static const char hahaKey;

或者這樣:

  • 4、@selector(haha)

關聯策略是個列舉:

  • OBJC_ASSOCIATION_ASSIGN = 0, //關聯物件的屬性是弱引用
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //關聯物件的屬性是強引用並且關聯物件不使用原子性
  • OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //關聯物件的屬性是copy並且關聯物件不使用原子性
  • OBJC_ASSOCIATION_RETAIN = 01401, //關聯物件的屬性是copy並且關聯物件使用原子性
  • OBJC_ASSOCIATION_COPY = 01403 //關聯物件的屬性是copy並且關聯物件使用原子性

總結

  • Category可以為主類新增方法,可以按照功能劃分不同的分類。
  • Category不能為主類新增例項變數,但是可以通過objc association的方式來動態關聯屬性,達到同樣的效果。
  • Category可以用來建立私有方法的前向引用,使得主類中的私有方法暴露無遺。
  • Category可以依賴於主類,但是主類一定不依賴於Category,也就是說,移除Category,對主類沒有任何影響。就像為MacBook買了一個鍵盤,即插即用。

本文屬用法整理,而關於Category的底層實現沒有進行分析,暫時沒那個大神的能力(可通過文章開頭的參考進行檢視,裡面有關於底層的實現分析)。但是對於使用Category,本文應該是具有一定的參考作用的。如果您覺得本文對您有一定的幫助,請隨手點個喜歡,十分感謝!???

相關文章