为OC类添加属性的若干方法
前言
为了解决为某些类添加属性而在不破坏封装性、影响原有代码并进行解耦优化的前提下,一般可以使用分类、扩展进行属性的添加,还有一种就是使用协议进行添加,这也是这篇文章想要讲的。
分类添加属性
一般来说分类是为主类添加方法,而不会选择添加属性。在分类头文件中添加的属性,只会生成setter、getter方法的声明而不会在.m文件当中添加它们的实现,所以需要我们手动的添加setter、getter,一般采用runtime关联对象的方法进行解决:
- (BasePropertyManager *)propertyManager {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setPropertyManager:(BasePropertyManager *)propertyManager {
objc_setAssociatedObject(self, @selector(propertyManager), propertyManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
这能够解决我们大多的需求,但是也会存在某些问题。
问题
- 如果想要添加的属性很多,那么就会出现大量上面类似的代码。
- 对于weak对象的引用的不支持,虽然runtime提供了OBJC_ASSOCIATION_ASSIGN这样的内存策略,其解释也是表示是对对象的弱引用,但是网上各种说法是这种内存策略和unsafe_unretained的作用一样,即指针指向的内存释放了,指针不会被自动置为nil, 再使用该指针的时候相当于调用了野指针,程序会崩溃。 而weak指针则会在指向的内存释放时自动清为nil。但是也有解决方案,解决方案如下。(ps:虽然网上各种这样的说法,但是自己试过是没有什么问题,也可能是恰好那块内存还没有被真正的释放仅仅被标记为可用,有人说过OC所说的内存被释放了,只是说这块内存被标记为可以被其他地方使用而已。)
/// 解决关联对象过程当中weak对象引用的问题
/// 1.创建一个容器类来保存想要weak的对象,然后关联这个容器,避免循环引用:
@interface WeakObjectContainer : NSObject
@property (nonatomic, readonly, weak) id weakObject;
- (instancetype)initWithWeakObject:(id)object;
@end
// 使用如下,注意这里使用的是OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, kEmptyDataSetSource, [[WeakObjectContainer alloc] initWithWeakObject:datasource], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
/// 2.使用block弱引用这个对象,然后关联block
- (void)setContext:(CDDContext*)object {
id __weak weakObject = object;
id (^block)() = ^{ return weakObject; };
objc_setAssociatedObject(self, @selector(context), block, OBJC_ASSOCIATION_COPY);
}
- (CDDContext*)context {
id (^block)() = objc_getAssociatedObject(self, @selector(context));
id curContext = (block ? block() : nil);
return curContext;
}
扩展添加属性
我们现在在Xcode里面创建一个类,会自动的在.m文件当中为我们创建一个扩展。当然也可以单独创建一个扩展文件独立出来,使用的时候导入即可。
可以在扩展中声明属性和方法,声明的属性的setter、getter、实例变量会自动在类的.m文件中生成,但是扩展中声明的方法必须在.m文件当中实现。
@interface ViewController ()
/// <#description#>
@property (nonatomic, strong) UITableView *tableView;
@end
@implementation ViewController
协议添加属性
协议中添加的方法需要我们去实现,那么协议中添加的属性的setter、getter方法也是需要我们实现的,只是对于属性使用@synthesize关键字即可,该关键字会在对应的实现文件中动态的添加实例变量、setter、getter方法。
// 协议 注意下面用了@optional修饰
@protocol BaseScrollViewPropertyProtocol <NSObject>
@optional
/// 翻页
@property (nonatomic, assign) NSInteger pageIndex;
/// pageSize
@property (nonatomic, assign) NSInteger pageSize;
@end
// BasePropertyManager采纳协议并在.m文件中使用@synthesize进行同步
@implementation BasePropertyManager
@synthesize pageSize, pageIndex = _pageIndex;
@end
// 创建完成后,生成的实例变量分别为pageSize、_pageIndex,上面的 pageIndex = _pageIndex只是为了我们手动去规定实例变量名字,默认生成的实例变量名和属性名一样,不会为我们自动添加下划线。
可以通过runtime进行检测:
- (void)getIvarName {
unsigned int count = 0;
//拷贝出所有的成员变量的列表
Ivar *ivars = class_copyIvarList(object_getClass(self), &count);
for (int i =0; i<count; i++) {
//取出成员变量
Ivar var = *(ivars + i);
//打印成员变量名字
NSLog(@"BasePropertyManager的实例变量:%s",ivar_getName(var));
}
//释放
free(ivars);
}
/**
* 获取对象的所有方法
*/
-(NSArray *)getAllMethods
{
unsigned int count_f =0;
//获取方法链表
Method* methodList_f = class_copyMethodList([self class],&count_f);
NSMutableArray *methodsArray = [NSMutableArray arrayWithCapacity:count_f];
for(int i=0;i<count_f;i++)
{
Method temp_f = methodList_f[i];
//方法
SEL name_f = method_getName(temp_f);
//方法名字符串
NSLog(@"BasePropertyManager的方法:%@",NSStringFromSelector(name_f));
}
free(methodList_f);
return methodsArray;
}
有一个好处是,这样的一个写满属性的协议,就可以被所有类采纳了,那就相当于所有的类都有了这样的属性!
但是到这里并没有完!
如果说你想要为苹果自己的类、或者第三方类添加属性,但是又想自定义setter、getter的实现呢? 是的,最前面说到的分类直接添加属性可以解决问题, 但是所有的地方都说并不建议分类中添加属性。
于是我为了解决这一问题,想到了结合消息转发机制.
上面说到定义一个属性的协议,所有的类可以采纳,那么你可以写一个基类(控制器为例),让基类采纳这个协议,并自定义setter、getter,于是每一个不同的基类你都写了一遍同样的setter、getter。 这个时候利用消息转发机制可以将相同的setter、getter方法提取出来。
1、新建一个BasePropertyManager采纳这个协议,并同步所有的属性。
2、让这些基类本身持有一个BasePropertyManager。
/// 属性管理器,会接收所有的setter、getter调用信息
@property (nonatomic, strong) BasePropertyManager *propertyManager;
3、在每个基类的内部实现消息转发的方法:
// 因为基类并没有setter、getter的实现,所以你调用基类的属性时,理论上是会崩溃的,但是下面的方法会返回一个可以处理消息的对象,去处理setter、getter方法。
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (!self.propertyManager) {
self.propertyManager = [[BasePropertyManager alloc] init];
}
return self.propertyManager;
}
于是每一个不同的基类你都写了一个propertyManager属性并实现了消息转发的方法。
这个时候就可以结合分类了,写一个专门进行消息转发的UIViewController的分类,分类里面有一个propertyManager属性,当然也可以让这个分类采纳协议,并不用所有的基类控制器都去采纳一遍协议了。.m实现如下:
#pragma mark - 属性管理
- (BasePropertyManager *)propertyManager {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setPropertyManager:(BasePropertyManager *)propertyManager {
objc_setAssociatedObject(self, @selector(propertyManager), propertyManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (!self.propertyManager) {
self.propertyManager = [[BasePropertyManager alloc] init];
}
return self.propertyManager;
}
扩展
这样的一个属性管理器BasePropertyManager,可以专门用于整个项目的属性扩充处理。 比如说为UISCrollView添加属性,那么可以建立一个BaseScrollViewPropertyProtocol协议,然后在UISCrollView分类中实现消息的转发,实现如下:
#pragma mark - 转发消息
- (id)forwardingTargetForSelector:(SEL)aSelector {
UIViewController *vc = self.viewController;
NSAssert(vc, @"拿不到Scrollview所在的控制器,请先将Scrollview加入到控制器中再执行协议中属性的赋值");
vc.propertyManager.managerOwner = self;
return self.viewController;
}
注意这里UISCrollView并没有添加一个propertyManager属性,而是这里选择将消息先转发到UISCrollView所在的控制器,再由控制器将消息转发到propertyManager。 managerOwner里面有一个managerOwner指向当前属性真正的拥有者。
问题
其实前面铺垫了那么多只是为了引出后面使用协议、属性管理器进行属性的添加。 但是这种做法会存在一个问题,因为我在分类里面覆写了UIViewController的方法,如果说UIViewController的其他基类或者子类也覆写了这个方法,那么就会出现意向不到的问题。
关于性能上面的事情,个人觉得这归根结底也是方法的调用,当系统找到这个方法的时候并会将其缓存起来,下次再调用就会省下很多步骤了。