Category 的一些事

新增實(shí)踐部分:偏方 Hook 進(jìn)某些方法來(lái)添加功能

Category - 簡(jiǎn)介

Category(類別)是 Objective-C 2.0 添加的新特性(十年前的新特性 ??)。其作用可以擴(kuò)展已有的類, 而不必通過(guò)子類化已有類,甚至也不必知道已有類的源碼,還有就是分散代碼,使已有類的體積大大減少,也利于分工合作。

蘋(píng)果開(kāi)源項(xiàng)目中,我們可以下載相關(guān)的源碼來(lái)查看 category 的資料。

在 AFNetworking 和 SDWebImage 中也大量用到 category 來(lái)擴(kuò)展已有類和分散代碼。

關(guān)于 category 的定義可以在 objc-runtime-new.h 中找到。由其定義可以看出 category 可以正常實(shí)現(xiàn)功能有:添加實(shí)例方法、類方法、協(xié)議、實(shí)例屬性。( 在后面的實(shí)踐中,發(fā)現(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;
    struct property_list_t *instanceProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta) {
        if (isMeta) return nil; // classProperties;
        else return instanceProperties;
    }
};

隨便說(shuō)一句,本文并不主要注重 category 的實(shí)現(xiàn)細(xì)節(jié)和工作原理。關(guān)于細(xì)節(jié)的方面可以看相關(guān)文章 深入理解Objective-C:Category結(jié)合 category 工作原理分析 OC2.0 中的 runtime


Category - 能做什么

首先,我們先來(lái)創(chuàng)建一個(gè) Person 類以及 Person 類的 category,可以看得出 category 的文件名就是 已有類名+自定義名

// Person.h
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

+ (void)run;
- (void)talk;

@end
// Person.m
@implementation Person

// 原實(shí)例方法
- (void)talk{
    NSLog(@"\n我是原實(shí)例方法\n我是%@",self.name);
}

// 原類方法
+ (void)run{
    NSLog(@"\n我是原類方法\n我是跑得很快的的香港記者");
}

@end
// Person+OtherSkills.h
@interface Person (OtherSkills){
    //?? instance variables may not be placed in categories
    //int i;
    //NSString *str;
}

// 添加實(shí)例屬性
@property (nonatomic, copy) NSString *otherName;
// 添加類屬性
@property (class, nonatomic, copy) NSString *clsStr;

// 重寫(xiě)已有類方法
+ (void)run;
- (void)talk;

// 為已有類添加方法
- (void)logInstProp;
+ (void)logClsProp;
// Person+OtherSkills.m
static NSString *_clsStr = nil;
static NSString *_otherName = nil;

@implementation Person (OtherSkills)

@dynamic otherName;

// 重寫(xiě)類方法
+ (void)run{
    // 警告?? Category is implementing a method which will also be implemented by its primary class
    NSLog(@"\n我是重寫(xiě)方法\n我是跑得很快的的香港記者");
}

// 重寫(xiě)實(shí)例方法
- (void)talk{
    // 警告?? Category is implementing a method which will also be implemented by its primary class
    NSLog(@"\n我是重寫(xiě)方法\n我是會(huì)談笑風(fēng)生的%@",self.otherName);
}

// 輸出實(shí)例屬性
- (void)logInstProp{
    NSLog(@"\n輸出實(shí)例屬性\n我是會(huì)談笑風(fēng)生的%@",self.otherName);
}

// 輸出類屬性
+ (void)logClsProp{
    NSLog(@"\n輸出類屬性\n我是會(huì)談笑風(fēng)生的%@",self.clsStr);
}

+ (NSString *)clsStr{
    return _clsStr;
}

+ (void)setClsStr:(NSString *)clsStr{
    _clsStr = clsStr;
}

- (NSString *)otherName{
    return _otherName;
}

- (void)setOtherName:(NSString *)otherName{
    _otherName = otherName;
}

創(chuàng)建完代碼之后,下面我們來(lái)看看 category 到底能干什么。

順便一提,我是在網(wǎng)上看到很多文章說(shuō) category 不能添加屬性,這是說(shuō)法是不對(duì)的,如 Person+OtherSkills.h 中就添加了一個(gè) otherName 的屬性。正確的說(shuō)法應(yīng)該是 category 不能添加實(shí)例變量,否則編譯器會(huì)報(bào)錯(cuò) instance variables may not be placed in categories。正常情況下,因?yàn)?category 不能添加實(shí)例變量,也會(huì)導(dǎo)致屬性的 setter & getter 方法不能正常工作。( 當(dāng)然,可以利用 Runtimecategory 動(dòng)態(tài)關(guān)聯(lián)屬性,最后會(huì)介紹兩種使 category 屬性正常工作的方法)

category 可以為已有類添加實(shí)例屬性。

Person+OtherSkills.h 中就添加了一個(gè) otherName 的屬性。可以出來(lái)能正常工作。

// 運(yùn)行代碼
Person *p1 = [[Person alloc] init];

// 實(shí)例屬性
p1.otherName = @"小花";
[p1 logInstProp];

p1.otherName = @"小明";
[p1 logInstProp];
// 輸出結(jié)果
2016-09-11 09:45:09.935 category[37281:1509791]
輸出實(shí)例屬性
我是會(huì)談笑風(fēng)生的小花
2016-09-11 09:45:09.936 category[37281:1509791]
輸出實(shí)例屬性
我是會(huì)談笑風(fēng)生的小明

category 可以為已有類添加類屬性。

雖然,category_t 中是沒(méi)有定義 clssProperties,但是根據(jù)實(shí)際操作卻顯示 category 的確可以為已有類添加類屬性并且成功執(zhí)行。我個(gè)人覺(jué)得是部分源碼沒(méi)有更新或者隱藏了??,如果有知道原因的同學(xué)可以說(shuō)一下

// 運(yùn)行代碼
Person.clsStr = @"小東";
[Person logClsProp];
// 輸出結(jié)果
2016-09-11 09:45:09.936 category[37281:1509791]
輸出類屬性
我是會(huì)談笑風(fēng)生的小東

category 可以為已有類添加實(shí)例方法和類方法。

在上面的兩個(gè)例子中已經(jīng)體現(xiàn)了 category 可以為已有類添加實(shí)例方法和類方法。這里將討論加入 category 重寫(xiě)了已有類的方法會(huì)怎么樣,在創(chuàng)建的代碼中我們已經(jīng)重寫(xiě)了 runtalk 方法,那這時(shí)我們來(lái)調(diào)用看看。

// 運(yùn)行代碼
// 調(diào)用類方法
[Person run];
// 調(diào)用實(shí)例方法    
Person *p1 = [[Person alloc] init];
[p1 talk];
// 輸出結(jié)果
2016-09-11 11:22:05.817 category[37733:1562534]
我是重寫(xiě)方法
我是跑得很快的的香港記者
2016-09-11 11:22:05.817 category[37733:1562534]
我是重寫(xiě)方法
我是會(huì)談笑風(fēng)生的(null)

可以看得出來(lái),這時(shí)候無(wú)論是已有類中的類方法和實(shí)例方法都可以被 category 替換到其中的重寫(xiě)方法,即使我現(xiàn)在是沒(méi)有導(dǎo)入 Person+OtherSkills.h 。這就帶來(lái)一個(gè)很嚴(yán)重的問(wèn)題,如果在 category 中不小心重寫(xiě)了已有類的方法將導(dǎo)致原方法無(wú)法正常執(zhí)行。所以使用 category 添加方法時(shí)候請(qǐng)注意是否和已有類重名了,正如 《 Effective Objective-C 2.0 》 中的第 25 條所建議的:

在給第三方類添加 category 時(shí)添加方法時(shí)記得加上你的專有前綴

然而,因?yàn)?category 重寫(xiě)方法是并不是替換掉原方法,而是往已有類中繼續(xù)添加方法,所以還是有機(jī)會(huì)去調(diào)用到原方法。這里利用 class_copyMethodList 獲取 Person 類的全部類方法和實(shí)例方法。

// 獲取 Person 的方法列表
unsigned int personMCount;
// 獲取實(shí)例方法
//Method *personMList = class_copyMethodList([Person class], &personMCount);
// 獲取類方法
Method *personMList = class_copyMethodList(object_getClass([Person class]), &personMCount);
NSMutableArray *mArr = [NSMutableArray array];

// 這里是倒序獲取,所以 mArr 第一個(gè)方法對(duì)應(yīng)的是 Person 類中最后一個(gè)方法
for (int i = personMCount - 1; i >= 0; i--) {

   SEL sel = NULL;
   IMP imp = NULL;

   Method method = personMList[i];
   NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
                                             encoding:NSUTF8StringEncoding];
   [mArr addObject:methodName];

   if ([@"run" isEqualToString:methodName]) {
       imp = method_getImplementation(method);
       sel = method_getName(method);
       ((void (*)(id, SEL))imp)(p1, sel); // 這里的 sel 有什么用呢 ?!
       //break;
   }
}

free(personMList);

其中輸出的類方法和實(shí)例方法分別如下,顯示原方法的確可以被調(diào)用。
不過(guò)我這里有個(gè)疑問(wèn),使用 imp 時(shí)第二個(gè)參數(shù) sel 到底有什么用呢?

2016-09-11 11:52:44.795 category[37893:1582677]
我是原類方法
我是跑得很快的的香港記者
2016-09-11 11:52:44.796 category[37893:1582677]
我是重寫(xiě)方法
我是跑得很快的的香港記者
2016-09-11 11:52:44.796 category[37893:1582677] (
    run, // 原方法
    run, // 重寫(xiě)方法
    "setClsStr:",
    logClsProp,
    clsStr
)
2016-09-11 11:54:14.545 category[37927:1584029]
我是原實(shí)例方法
我是(null)
2016-09-11 11:54:14.545 category[37927:1584029]
我是重寫(xiě)方法
我是會(huì)談笑風(fēng)生的(null)
2016-09-11 11:54:14.545 category[37927:1584029] (
    "setName:",
    name,
    ".cxx_destruct",
    "setOtherName:",
    logInstProp,
    tanxiaofengsheng,
    otherName,
    talk, //原方法
    talk  //重寫(xiě)方法

category 可以為已有類添加協(xié)議。

這里先添加一個(gè)新的 category,負(fù)責(zé)處理他談笑風(fēng)生的行為,和寫(xiě)個(gè)協(xié)議讓他上電視。

// Person+Delegate.h
#import "Person.h"

// 添加協(xié)議
@protocol PersonDelegate <NSObject>

- (void)showInTV;

@end

@interface Person (Delegate)

// 添加 delegate
@property (nonatomic, weak) id<PersonDelegate> delegate;

- (void)tanxiaofengsheng;

@end
// Person+Delegate.m
#import "Person+Delegate.h"
#import <objc/runtime.h>

@implementation Person (Delegate)

- (id<PersonDelegate>)delegate{
    return objc_getAssociatedObject(self, @selector(delegate));
}

- (void)setDelegate:(id<PersonDelegate>)delegate{
    objc_setAssociatedObject(self, @selector(delegate), delegate, OBJC_ASSOCIATION_ASSIGN);
}

- (void)tanxiaofengsheng{
    for (int i = 0 ; i < 10; i ++) {
        NSLog(@"談笑風(fēng)生...");
    }

    // 談笑風(fēng)生完就要上電視了
    if ([self.delegate respondsToSelector:@selector(showInTV)]) {
        [self.delegate showInTV];
    }
}

@end

在相應(yīng)的代理里面添加 showInTV 的方法

// 運(yùn)行代碼
Person *p1 = [[Person alloc] init];
p1.delegate = self;

// 開(kāi)始談笑風(fēng)生了
[p1 tanxiaofengsheng];

// ShowInTV 方法的實(shí)現(xiàn)
- (void)showInTV{
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 150, 150)];
    imageView.image = [UIImage imageNamed:@"naive.jpg"];
    [self.view addSubview:imageView];
}

這樣就利用 category 為已有類添加了協(xié)議。

關(guān)于 category 的基本應(yīng)用就介紹到這里了。下面就來(lái)分享一下 category 的實(shí)踐中的使用。


Category - 實(shí)踐

偏方:Hook 進(jìn)某些方法來(lái)添加功能

一般來(lái)說(shuō),為原方法添加功能都是利用 Runtime 來(lái) Method Swizzling。不過(guò)這里也有個(gè)奇淫技巧來(lái)實(shí)現(xiàn)同樣的功能,例如我要在所有 VC- (void)viewDidLoad 里面打印一個(gè)句話,就可以用 category 重寫(xiě)已有類的方法,因?yàn)?category 重寫(xiě)方法不是通過(guò)替換原方法來(lái)實(shí)現(xiàn)的,而是在原方法列表又增添一個(gè)新的同名方法,這就創(chuàng)造了機(jī)會(huì)給我們重新調(diào)用原方法了。

// 待 Hook 類
// ViewController.m 
// 待替換方法 無(wú)參
- (void)viewDidLoad {
    [super viewDidLoad];
    [self testForHook:@"Hello World"];
    NSLog(@"執(zhí)行原方法");
}

// 待替換方法 有參
- (void)testForHook:(NSString *)str1{
    NSLog(@"%@",str1);
}
// category 實(shí)現(xiàn)方法
// ViewController+HookOriginMethod.m 
// category 重寫(xiě)原方法
- (void)viewDidLoad {
    NSLog(@"HOOK SUCCESS! \n--%@-- DidLoad !",[self class]);
    IMP imp = [self getOriginMethod:@"viewDidLoad"];
    ((void (*)(id, SEL))imp)(self, @selector(viewDidLoad));
}

// category 重寫(xiě)原方法
- (void)testForHook:(NSString *)str1{
    NSLog(@"HOOK SUCCESS \n--%s-- 執(zhí)行",_cmd);
    IMP imp = [self getOriginMethod:@"testForHook:"];
    ((void (*)(id, SEL, ...))imp)(self, @selector(testForHook:), str1);
}

// 獲取原方法的 IMP
- (IMP)getOriginMethod:(NSString *)originMethod{
    // 獲取 Person 的方法列表
    unsigned int methodCount;
    // 獲取實(shí)例方法
    Method *VCMethodList = class_copyMethodList([self class], &methodCount);

    IMP imp = NULL;
    
    // 這里是倒序獲取,所以 mArr 第一個(gè)方法對(duì)應(yīng)的是 Person 類中最后一個(gè)方法
    for (int i = methodCount - 1; i >= 0; i--) {

        Method method = VCMethodList[i];
        NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
                                                  encoding:NSUTF8StringEncoding];

        if ([originMethod isEqualToString:methodName]) {
            imp = method_getImplementation(method);
            break;
        }
    }
    
    free(VCMethodList);
    return imp;
}
// 執(zhí)行代碼
// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self testForHook:@"Hello World"];
    NSLog(@"執(zhí)行原方法");
}
// 輸出結(jié)果
2016-09-12 23:00:15.887 category[63655:2375379] HOOK SUCCESS! 
--ViewController-- DidLoad !
2016-09-12 23:00:15.888 category[63655:2375379] HOOK SUCCESS 
--testForHook:-- 執(zhí)行
2016-09-12 23:00:15.889 category[63655:2375379] Hello World
2016-09-12 23:00:15.889 category[63655:2375379] 執(zhí)行原方法

查看輸出結(jié)果,可以看得出來(lái)我們的 Hook 掉 viewDidLoad 來(lái)實(shí)現(xiàn)打印成功了。


UIButton 實(shí)現(xiàn)點(diǎn)擊事件可以“傳參”。

一般創(chuàng)建UIButton的時(shí)候都會(huì)使用 addTarget ...這個(gè)方法來(lái)為button添加點(diǎn)擊事件,不過(guò)這個(gè)方法有個(gè)不好的地方就是無(wú)法傳自己想要的參數(shù)。例如下面代碼中聲明了str,我的意圖是點(diǎn)擊button就使控制臺(tái)或者屏幕顯示str的內(nèi)容。如果按照這樣來(lái)寫(xiě)的我想到的解決辦法就是將str設(shè)置為屬性或者成員變量,不過(guò)這樣都是比較麻煩而且不直觀的(代碼分散)。

NSString *str = @"hi";
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 250, 150, 100)];
button.backgroundColor = [UIColor redColor];
[button addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchDown];
[self.view addSubview:button];

// 點(diǎn)擊事件
- (void)click:(UIButton *)button{
    ...    
}

我想到較好的解決辦法應(yīng)該在創(chuàng)建button,就為它設(shè)置具體的點(diǎn)擊響應(yīng)事件。實(shí)現(xiàn)方法就是為 UIButton 添加 block 屬性或者添加可傳入 block 的方法。具體代碼如下:

// UIButton+Category.h
#import <UIKit/UIKit.h>

typedef void(^ActionHandlerBlock)(void);

@interface UIButton (Category)

// 點(diǎn)擊響應(yīng)的 block
@property (nonatomic, copy) ActionHandlerBlock actionHandlerBlock;

// 設(shè)置 UIButton 的點(diǎn)擊事件
- (void)kk_addActionHandler: (ActionHandlerBlock )actionHandlerBlock ForControlEvents:(UIControlEvents )controlEvents;

@end
// UIButton+Category.m
#import "UIButton+Category.h"
#import <objc/runtime.h>

static const void *kk_actionHandlerBlock = &kk_actionHandlerBlock;

@implementation UIButton (Category)

- (void)kk_addActionHandler:(ActionHandlerBlock)actionHandler ForControlEvents:(UIControlEvents)controlEvents{

    // 關(guān)聯(lián) actionHandler
    objc_setAssociatedObject(self, kk_actionHandlerBlock, actionHandler, OBJC_ASSOCIATION_COPY_NONATOMIC);

    // 設(shè)置點(diǎn)擊事件
    [self addTarget:self action:@selector(handleAction) forControlEvents:controlEvents];
}

// 處理點(diǎn)擊事件
- (void)handleAction{

    ActionHandlerBlock actionHandlerBlock = objc_getAssociatedObject(self, kk_actionHandlerBlock);

    if (actionHandlerBlock) {
        actionHandlerBlock();
    }
}

- (ActionHandlerBlock)actionHandlerBlock{
    return objc_getAssociatedObject(self, @selector(actionHandlerBlock));
}

- (void)setActionHandlerBlock:(ActionHandlerBlock)actionHandlerBlock{
    objc_setAssociatedObject(self, @selector(actionHandlerBlock), actionHandlerBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

那現(xiàn)在我們來(lái)看看調(diào)用的結(jié)果,例如我現(xiàn)在想要的點(diǎn)擊事件是 button 顏色隨機(jī)變換。


UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 250, 150, 100)];
button.backgroundColor = [UIColor redColor];
[self.view addSubview:button];

// 1. 通過(guò)實(shí)例方法傳入 block 來(lái)修改  
UIButton *button2 = [[UIButton alloc] initWithFrame:CGRectMake(100, 400, 150, 100)];
button2.backgroundColor = [UIColor redColor];
[button2 kk_addActionHandler:^{
   button.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
} ForControlEvents:UIControlEventTouchDown];
[self.view addSubview:button2];

// 2. 通過(guò)修改 block 屬性來(lái)修改
UIButton *button3 = [[UIButton alloc] initWithFrame:CGRectMake(100, 550, 150, 100)];
button3.backgroundColor = [UIColor redColor];
button3.actionHandlerBlock = ^{
   button.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
};
[button3 addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button3];


// 響應(yīng)事件
- (void)click:(UIButton *)button{
    if (button.actionHandlerBlock) {
        button.actionHandlerBlock();
    }
}
實(shí)現(xiàn)效果圖

顯然,方法1和方法2在這個(gè)例子中實(shí)現(xiàn)的效果是相同的。不過(guò),在不同場(chǎng)合這兩個(gè)方法適用的范圍也不同。

  1. 直接調(diào)用實(shí)例方法傳入 block 會(huì)使代碼更加簡(jiǎn)潔和集中,但不適合 block 需要傳值的情景。
  2. 相反,設(shè)置 block 屬性要在 @selector() 中的方法中調(diào)用 block,比較麻煩,不過(guò)在需要的情況下可以傳入合適的參數(shù)。

p.s. 以后會(huì)繼續(xù)補(bǔ)充實(shí)踐部分。

最后說(shuō)一下,兩種使 category 屬性正常工作的方法:

  1. 因?yàn)?category 不能創(chuàng)建實(shí)例變量,那就直接使用靜態(tài)變量,如最開(kāi)始為 ohterNameclsStr 屬性設(shè)置 setter & getter的做法。

  2. 使用objc_setAssociatedObject,其中 key 的選擇有以下幾種,個(gè)人比較喜歡第四種。

    • static char *key1; // SDWebImage & AFNetworking 中的做法,比較簡(jiǎn)單,而且 &key1 肯定唯一。key 取 &key1
    • static const char * const key2 = "key2"; // 網(wǎng)上看到的做法,指針不可變,指向內(nèi)容不可變,但是這種情況必須在賦值確保 key2 指向內(nèi)容的值是唯一。key 取 key2。
    • static const void *key3 = &key3; // 最取巧的方法,指向自己是為了不創(chuàng)建額外空間,而 const 修飾可以確保無(wú)法修改 key3 指向的內(nèi)容。key 取 key3。
    • key 取 @selector(屬性名),最方便,輸入有提示,只要你確保屬性名添加上合適的前綴就不會(huì)出問(wèn)題。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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