FaiChou

@property in Category

12 Sep 2016


前言

我们是可以在Category中添加属性的,就像以下代码

/** FaiChouView.h */
#import <UIKit/UIKit.h>

@interface FaiChouView : UIView

@end

@interface FaiChouView (fcHeight)
@property CGFloat fcHeight;
@end

/** FaiChouView.m */
#import "FaiChouView.h"

@implementation FaiChouView
@end
@implementation FaiChouView (fcHeight)

- (CGFloat)fcHeight {
    return self.fcHeight;
}
- (void)setFcHeight:(CGFloat)fcHeight {
    self.fcHeight = fcHeight;
}
@end

/** main.m */
#import "FaiChouView.h"
int main(int argc, char * argv[]) {
    @autoreleasepool {
    	FaiChouView *fcView = [[FaiChouView alloc] init];
        fcView.fcHeight = 20.; 
        NSLog(@"%f", fcView.fcHeight);
    }
}

毫无疑问,它会崩溃的。问题出在哪?如何修改?一步一步来。

到底可不可以添加属性到Category

我们可以在苹果官方文档中找到以下说明:

Categories can be used to declare either instance methods or class methods but are not usually suitable for declaring additional properties. It’s valid syntax to include a property declaration in a category interface, but it’s not possible to declare an additional instance variable in a category. This means the compiler won’t synthesize any instance variable, nor will it synthesize any property accessor methods. You can write your own accessor methods in the category implementation, but you won’t be able to keep track of a value for that property unless it’s already stored by the original class.

文档中明确指出It’s valid syntax to include a property declaration in a category interface,我们可以在接口文件中声明属性,但是编译器不会自动合成实例变量(Ivars)和存取方法。 我们应该自己实现存取方法。

我们的代码到底错在什么地方?

won’t be able to keep track of a value for that property

我们的代码return self.fcHeight;已经无法无天了。

如何修改代码让其正常获取view的高度?

@implementation FaiChouView (fcHeight)

- (CGFloat)fcHeight {
    // return self.fcHeight;
    
    return self.frame.size.height;
}
- (void)setFcHeight:(CGFloat)fcHeight {
    
    // self.fcHeight = fcHeight;
    
    CGRect newframe = self.frame;
    newframe.size.height = fcHeight;
    self.frame = newframe;
}

@end

官方文档的说明,我们在Category中的属性是不能够保存其值的,但是我们可以自定义存取方法,fcHeight返回的是view本身的高度, setter方法也不将值赋值给fcHeight,而间接的给view.frame,这样在main.m中的fcView.fcHeight = 20.; 只是借助了fcHeight的存取方法给view本身的赋值。

通过以上,我们验证了那句古话

不能在Category中添加实例变量

学而不思则罔,思而不学则殆

只要挖到runtime,任何东西都是一目了然的。 有些博客看了一遍看不懂,那就多看几遍,博客讲的只是一个方面,上面有许多许多知识点,这一面映射出来的所有点没有必要全部掌握,发现问题的关键,带着问题考虑,总会学到很多知识的。

少谈些主义,多研究些问题。 ———— 胡适

objc所有类和对象都是c结构体,category当然也一样,下面是runtime中Category的结构:

struct _category_t {
	const char *name; // 类名
	struct _class_t *cls; //
	const struct _method_list_t *instance_methods; // 实例方法 -
	const struct _method_list_t *class_methods; // 类方法 +
	const struct _protocol_list_t *protocols; // 
	const struct _prop_list_t *properties; //
};

properties这个category所有的property,这也是category里面可以定义属性的原因,不过这个property不会合成实例变量和存取方法。

一个普通的属性(fcTestProperty),经过编译器编译过后,会添加以下:

  1. _ivar_list_t中添加了_fcTestProperty变量
  2. _method_list_t中添加了fcTestPropertysetFcTestProperty两个方法
  3. _prop_list_t中添加了fcTestProperty这个property

一个Category中的属性(fcTestProperty),经过编译器编译后,只会在_prop_list_t中增加fcTestProperty这个属性。

如何在Category中添加实例变量呢?

MJRefresh中我们可以找到以下代码(摘要):

/** UIScrollView+MJRefresh.h */

#import <UIKit/UIKit.h>
#import "MJRefreshConst.h"

@class MJRefreshHeader, MJRefreshFooter;

@interface UIScrollView (MJRefresh)
/** 下拉刷新控件 */
@property (strong, nonatomic) MJRefreshHeader *mj_header;

...

@end


/** UIScrollView+MJRefresh.m */

#pragma mark - header
static const char MJRefreshHeaderKey = '\0';
- (void)setMj_header:(MJRefreshHeader *)mj_header
{
    if (mj_header != self.mj_header) {
        // 删除旧的,添加新的
        [self.mj_header removeFromSuperview];
        [self insertSubview:mj_header atIndex:0];
        
        // 存储新的
        [self willChangeValueForKey:@"mj_header"]; // KVO
        objc_setAssociatedObject(self, &MJRefreshHeaderKey,
                                 mj_header, OBJC_ASSOCIATION_ASSIGN);
        [self didChangeValueForKey:@"mj_header"]; // KVO
    }
}

- (MJRefreshHeader *)mj_header
{
    return objc_getAssociatedObject(self, &MJRefreshHeaderKey);
}

...

他是通过runtime中的关联对象实现的,关于Associated Objects可以学习NSHipster的魔鬼的交易这一篇。

最后我们的代码调整为:

/** FaiChouView.m */
#import "FaiChouView.h"
#import <objc/runtime.h>

@implementation FaiChouView
@end

@implementation FaiChouView (fcHeight)
@dynamic fcHeight;
- (CGFloat)fcHeight {
    // return self.fcHeight;
    
    // return self.frame.size.height;
    return [objc_getAssociatedObject(self, @selector(fcHeight)) floatValue];
}
- (void)setFcHeight:(CGFloat)fcHeightNew {
    
    // self.fcHeight = fcHeight;
    
//    CGRect newframe = self.frame;
//    newframe.size.height = fcHeight;
//    self.frame = newframe;
    NSNumber *fcHeightFloatNumber = @(fcHeightNew);
    objc_setAssociatedObject(self, @selector(fcHeight), fcHeightFloatNumber, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

objc_setAssociatedObjectobjc_getAssociatedObject方法绑定的实例变量与一个普通的实例变量完全是两码事。

参考链接


comments powered by Disqus