在Objective-C的Category中添加懒加载的属性

592 阅读2分钟

在Objective-C代码中, 使用Category文件可以很好地优化代码结构, 给一个类提供一些扩展功能. 如果Category中的方法与原来类中的方法重名, 则会将原来的方法覆盖.

而Category添加属性, 只能添加对应的getter/setter方法, 而不会添加真正的实例变量(ivar). 因为实例变量存在于类的结构中, 是在编译期就确定好的.

利用Category优化代码结构

在Category中使用属性, 只能采用关联对象来实现.

Category中定义的属性, 其实只会有getter/setter方法, 而没有包含实例变量.

@interface MyViewController (Timer)

@property (nonatomic, strong) NSTimer *timer;

- (void)addTimer;
- (void)removeTimer;

@end

实现懒加载

在实现文件中的getter方法中, 添加类似懒加载的实现代码即可:

#import <objc/runtime.h>

static void *kContextTimer = &kContextTimer;

@implemetation MyViewController (Timer)

// 关联对象

- (NSTimer *)timer {
    NSTimer *timer = objc_getAssociatedObject(self, kContextTimer);
    if (!timer) {
        timer = [NSTimer timerWithTimeInterval:1.f
                                        target:self
                                      selector:@selector(actionTimerOn:)  
                                      userInfo:nil
                                       repeats:YES];
        objc_setAssociatedObject(self, kContextTimer, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return timer;
}

- (void)setTimer:(NSTimer *)timer {
    objc_setAssociatedObject(self, kContextTimer, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)actionTimerOn:(NSTimer *)sender {
    NSLog(@"actionTimerOn : %@", sender);
}

- (void)addTimer {
    [[NSRunLoop currentRunLoop] addTimer:self.timer
                                 forMode:NSRunLoopCommonModes];
}

- (void)removeTimer {
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

@end

关联策略objc_AssociationPolicy

关联策略表明了关联的对象是通过值传递, 引用传递, 还是copy的方式进行关联.

/* Associative References */

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

采用的关联策略与属性内存管理关键字保持一致即可.

对于普通的OC对象, 通常采用OBJC_ASSOCIATION_RETAIN_NONATOMIC.

对于NSString等通常采用copy关键字的属性, 通常采用OBJC_ASSOCIATION_COPY_NONATOMIC.

对于枚举等通常采用assign关键字的属性, 通常采用OBJC_ASSOCIATION_ASSIGN.

关联键值key

参数key接收一个const void * _Nonnull的指针类型, 因此可以有多种传入方式:

static void *kContextTimer = &kContextTimer;

static void *kContextTimer = &kContextTimer;
objc_getAssociatedObject(self, kContextTimer);
objc_setAssociatedObject(self, kContextTimer, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

static char imageURLStorageKey;

在SDWebImage中,

static char imageURLStorageKey;
objc_getAssociatedObject(self, &imageURLStorageKey);
objc_setAssociatedObject(self, &imageURLStorageKey, storage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

直接使用@selector(af_activeImageDownloadReceipt)

在AFNetworking中, 有如下类似的写法:

@interface UIImageView (_AFNetworking)
@property (readwrite, nonatomic, strong, setter = af_setActiveImageDownloadReceipt:) AFImageDownloadReceipt *af_activeImageDownloadReceipt;
@end

@implementation UIImageView (_AFNetworking)

- (AFImageDownloadReceipt *)af_activeImageDownloadReceipt {
    return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, @selector(af_activeImageDownloadReceipt));
}

- (void)af_setActiveImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
    objc_setAssociatedObject(self, @selector(af_activeImageDownloadReceipt), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

根据state来区分一些有联系的key值

对于UIButton的扩展, 采用如下方式与UIButton的状态联系起来, 是一种非常巧妙的处理方式.

static char AFImageDownloadReceiptNormal;
static char AFImageDownloadReceiptHighlighted;
static char AFImageDownloadReceiptSelected;
static char AFImageDownloadReceiptDisabled;

static const char * af_imageDownloadReceiptKeyForState(UIControlState state) {
    switch (state) {
        case UIControlStateHighlighted:
            return &AFImageDownloadReceiptHighlighted;
        case UIControlStateSelected:
            return &AFImageDownloadReceiptSelected;
        case UIControlStateDisabled:
            return &AFImageDownloadReceiptDisabled;
        case UIControlStateNormal:
        default:
            return &AFImageDownloadReceiptNormal;
    }
}

- (AFImageDownloadReceipt *)af_imageDownloadReceiptForState:(UIControlState)state {
    return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, af_imageDownloadReceiptKeyForState(state));
}

- (void)af_setImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt
                           forState:(UIControlState)state
{
    objc_setAssociatedObject(self, af_imageDownloadReceiptKeyForState(state), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}