计算属性是指一个对象的一个属性的变化依赖这个对象的其他属性的变化。比如一个人的fullName 的变化依赖这个人的firstName,lastName这两个属性的变化
项目中存在UICollectionView,UITableView 列表需要对cell的size,height进行计算的场景,目前存在两种需要优化的地方
每次渲染cell的时候都会进行计算,造成了重复计算
渲染cell的时候才进行计算,没有将渲染和计算进行分离。如果某些计算是耗时操作,那么会造成页面滑动时流畅性降低。
为了实现配置计算属性与它依赖的属性之间的对应关系,我这边在NSObject+JKKVOHelper文件里创建了一个分类方法,具体如下:
- (NSDictionary *)jk_computedProperty_config
{
return @{};
}
开发者在具体使用时,在相对应的类中配置一下对应关系即可。以person为例,代码如下:
- (NSDictionary *)jk_computedProperty_config
{
return @{@"fullName":@[@"firstName",@"lastName"]};
}
备注:fullName的变化依赖firstName和lastName的变化
在NSObject+JKKVOHelper文件里创建了一个方法,具体如下:
- (void)jk_initComputed
{
if (!self.is_jk_computed) {
self.is_jk_computed = YES;
NSDictionary *computed_config = [self jk_computedProperty_config];
@weakify(self);
[self jk_addComputedObserverswithConfig:computed_config detailBlock:^(NSString *computedKey, NSString *dependentProperty) {
@strongify(self);
SEL getterSelector = NSSelectorFromString(computedKey);
JKKVOComputedItem *computed_item = [JKKVOItemManager isContainComputedItemWithObserver:self observered:self keyPath:computedKey];
computed_item.dirty = YES;
jk_computed_getter(self, getterSelector);
}];
[self jk_configComputedSubClassWithConfig:computed_config];
}
}
如果配置过了,那么配置的逻辑就不再执行了。然后根据计算属性与它依赖的属性的对应关系,建立对相关属性的监听。等到相关属性发生变化时,触发一下jk_computed_getter方法。
jk_initComputed
有两种用法,
jk_initComputed
。示例代码如下:JKPersonModel *person = [JKPersonModel new];
[person jk_initComputed];
person.firstName = @"A";
__block BOOL invoked = NO;
[person jk_addObserverForKeyPath:@"fullName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld withBlock:^(NSDictionary * _Nonnull change, void * _Nonnull context) {
invoked = YES;
NSString *oldStr = change[NSKeyValueChangeOldKey];
NSString *newStr = change[NSKeyValueChangeNewKey];
[[theValue([oldStr hasPrefix:@"A"]) should] beYes];
[[theValue(oldStr.length > person.firstName.length) should] beYes];
[[theValue([newStr isEqualToString:@"AB"]) should] beYes];
}];
person.lastName = @"B";
[[theValue(invoked) should] beYes];
[[theValue(person.invokedCount == 2) should] beYes];
NSLog(@"fullName: %@",person.fullName);
[[theValue(person.invokedCount == 2) should] beYes];
[[theValue([person.fullName isEqualToString:@"AB"]) should] beYes];
此时person对象赋值时,就会立即触发计算。应用场景:从网络接口请求的数据,初始化model后,然后赋值。这种用法可以将计算和渲染完全分离。但是如果请求回来的数据量过大,那么会触发大量的计算,这种用法就不太合适了。
2. 另一种用法就是在对象初始化,并且赋值后调用jk_initComputed
,这样适合一次性数据量较大,但每次只需展示很少一部分,将计算和渲染结合起来(备注:此时未将计算和渲染进行分离),通过缓存减少重复计算。
为了实现将计算属性的内容缓存起来,我这边在将计算属性的内容保存在JKKVOComputedItem(保存计算属性监听以及配置信息的一个类)中,具体代码如下:
@interface JKKVOComputedItem : JKBaseKVOItem
/// 用来标记数据是否有变化
@property (nonatomic, assign) BOOL dirty;
/// 计算属性缓存的值
@property (nonatomic) void *value;
///计算属性缓存非NSObject对象的值
@property (nonatomic) NSValue *non_obj_value;
///value的类型,NSObject的默认为NSObject,非NSObject 为真实的类型
@property (nonatomic) const char *valueType;
/// 依赖的属性建立的item组成的数组
@property (nonatomic, strong, readonly) NSArray *dependentProperties;
/// 回调
@property (nonatomic, copy, readonly) void(^block)(NSString *keyPath, NSDictionary *change, void *context);
+ (instancetype)initWith_kvoObserver:(nonnull JKKVOObserver *)kvoObserver
observered:(nonnull __kindof NSObject *)observered
keyPath:(nonnull NSString *)keyPath
dependentProperties:(NSArray *)dependentProperties
block:(nullable void(^)(NSString *keyPath, NSDictionary *change, void *context))block;
/// 添加对依赖属性的监听
- (void)addDependentPropertiesObserver;
/// 移除对依赖属性的监听
- (void)removeDependentPropertiesObserver;
/// 内部使用
+ (void *)computedObserverContext;
@end
其中对于NSObject 类型的计算属性,存储比较简单,直接用void *指针持有即可,而对于非NSObject类型的对象有一些需要转换成NSValue进行存储。用到的时候从对应的NSValue中取出来。
实现思路主要通过派生目标对象的子类,并重新实现计算属性的getter方法,在getter方法内部,根据该计算属性所依赖的属性是否发生变化(反映在JKKVOComputedItem的dirty属性上)来决定是读取缓存中的数据,还是执行计算逻辑。具体代码如下:
static void *jk_computed_getter(NSObject* self, SEL _cmd)
{
// 调用原类的setter方法
struct objc_super superClazz = {
.receiver = self,
.super_class = jk_originClass(self)
};
if (!self.is_jk_computed) {
return ((void *(*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
} else {
NSString *getterName = NSStringFromSelector(_cmd);
JKKVOComputedItem *computed_item = [JKKVOItemManager isContainComputedItemWithObserver:self observered:self keyPath:getterName];
if (strstr(computed_item.valueType, "@")) {
if (computed_item.dirty) {
void *oldValue = computed_item.value;
void *newValue = ((void* (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
if (oldValue != newValue) {
computed_item.dirty = NO;
NSString *setterName = [self jk_setterForGetter:getterName];
void(^block)(void) = ^(void) {
computed_item.value = newValue;
};
jk_computedProperty_setter(self, NSSelectorFromString(setterName), newValue,block);
}
}
return computed_item.value;
} else {
return jk_non_objc_getter(self, _cmd, computed_item, superClazz, getterName);
}
}
}
派生子类的方案参考了手动实现KVO,在实践的过程中发现在实现派生子类相应setter方法的时候,监听非NSObject类型时会崩溃, 我这边进行了改造,改造后的代码如下:
static void jk_dependentProperty_setter(id self, SEL _cmd, void *newValue)
{
struct objc_super superClazz = {
.receiver = self,
.super_class = jk_originClass(self)
};
NSString *setterSelectorName = NSStringFromSelector(_cmd);
NSString *getterName = [self jk_getterForSetter:setterSelectorName];
NSArray <__kindof JKBaseKVOItem *>*items = [JKKVOItemManager itemsOfObservered:self keyPath:getterName];
BOOL hasExternalObserver = NO;
for (__kindof JKBaseKVOItem *item in items) {
if (item.context != [JKKVOComputedItem computedObserverContext]) {
hasExternalObserver = YES;
}
}
if (hasExternalObserver) {
((void (*)(void *, SEL, typeof(newValue)))objc_msgSendSuper)(&superClazz, _cmd, newValue);
} else {
[self willChangeValueForKey:getterName];
((void (*)(void *, SEL, typeof(newValue)))objc_msgSendSuper)(&superClazz, _cmd, newValue);
[self didChangeValueForKey:getterName];
}
}
通过派生子类,并且实现计算属性相应的getter方法,对于非NSObject的类型,由于存储方式不一样,我这边采取了不同的方式将计算属性的值进行了缓存,具体代码如下:
static void * jk_non_objc_getter(NSObject *self, SEL _cmd, JKKVOComputedItem *computed_item, struct objc_super superClazz, NSString *getterName)
{
// if (strstr(computed_item.valueType, "{CGRect=")) {//CGRect
// if (computed_item.dirty) {
// NSValue *oldValue = computed_item.non_obj_value;
// CGRect rect = ((CGRect (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
// NSValue *newValue = [NSValue valueWithCGRect:rect];
// if (![oldValue isEqualToValue:newValue]) {
// computed_item.dirty = NO;
// NSString *setterName = [self jk_setterForGetter:getterName];
// void(^block)(void) = ^(void) {
// computed_item.non_obj_value = newValue;
// };
// jk_computedProperty_setter(self, NSSelectorFromString(setterName), &rect, block);
// }
// }
// CGRect rect = [computed_item.non_obj_value CGRectValue];
// void *value = ▭
// return value;
// }
if (strstr(computed_item.valueType, "{CGSize=")) {//CGSize
if (computed_item.dirty) {
NSValue *oldValue = computed_item.non_obj_value;
CGSize size = ((CGSize (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
NSValue *newValue = [NSValue valueWithCGSize:size];
if (![oldValue isEqualToValue:newValue]) {
computed_item.dirty = NO;
NSString *setterName = [self jk_setterForGetter:getterName];
void(^block)(void) = ^(void) {
computed_item.non_obj_value = newValue;
};
jk_computedProperty_setter(self, NSSelectorFromString(setterName), &size, block);
}
}
CGSize size = [computed_item.non_obj_value CGSizeValue];
void *value = &size;
return value;
}
if (strstr(computed_item.valueType, "{CGPoint=")) {//CGPoint
if (computed_item.dirty) {
NSValue *oldValue = computed_item.non_obj_value;
CGPoint point = ((CGPoint (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
NSValue *newValue = [NSValue valueWithCGPoint:point];
if (![oldValue isEqualToValue:newValue]) {
computed_item.dirty = NO;
NSString *setterName = [self jk_setterForGetter:getterName];
void(^block)(void) = ^(void) {
computed_item.non_obj_value = newValue;
};
jk_computedProperty_setter(self, NSSelectorFromString(setterName), &point, block);
}
}
CGPoint point = [computed_item.non_obj_value CGPointValue];
void *value = &point;
return value;
}
// if (strstr(computed_item.valueType, "{UIEdgeInsets=")) {//UIEdgeInsets
// if (computed_item.dirty) {
// NSValue *oldValue = computed_item.non_obj_value;
// UIEdgeInsets edgeInsets = ((UIEdgeInsets (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
// NSValue *newValue = [NSValue valueWithUIEdgeInsets:edgeInsets];
// if (![oldValue isEqualToValue:newValue]) {
// computed_item.dirty = NO;
// NSString *setterName = [self jk_setterForGetter:getterName];
// void(^block)(void) = ^(void) {
// computed_item.non_obj_value = newValue;
// };
// jk_computedProperty_setter(self, NSSelectorFromString(setterName), &edgeInsets, block);
// }
// }
// UIEdgeInsets edgeInsets = [computed_item.non_obj_value UIEdgeInsetsValue];
// void *value = &edgeInsets;
// return value;
// }
// if (strstr(computed_item.valueType, "{CGAffineTransform=")) {//CGAffineTransform
// if (computed_item.dirty) {
// NSValue *oldValue = computed_item.non_obj_value;
// CGAffineTransform transform = ((CGAffineTransform (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
// NSValue *newValue = [NSValue valueWithCGAffineTransform:transform];
// if (![oldValue isEqualToValue:newValue]) {
// computed_item.dirty = NO;
// NSString *setterName = [self jk_setterForGetter:getterName];
// void(^block)(void) = ^(void) {
// computed_item.non_obj_value = newValue;
// };
// jk_computedProperty_setter(self, NSSelectorFromString(setterName), &transform, block);
// }
// }
// CGAffineTransform transform = [computed_item.non_obj_value CGAffineTransformValue];
// void *value = &transform;
// return value;
// }
if (strstr(computed_item.valueType, "{UIOffset=")) {//CGSize
if (computed_item.dirty) {
NSValue *oldValue = computed_item.non_obj_value;
UIOffset offset = ((UIOffset (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
NSValue *newValue = [NSValue valueWithUIOffset:offset];
if (![oldValue isEqualToValue:newValue]) {
computed_item.dirty = NO;
NSString *setterName = [self jk_setterForGetter:getterName];
void(^block)(void) = ^(void) {
computed_item.non_obj_value = newValue;
};
jk_computedProperty_setter(self, NSSelectorFromString(setterName), &offset, block);
}
}
UIOffset offset = [computed_item.non_obj_value UIOffsetValue];
void *value = &offset;
return value;
}
if (strstr(computed_item.valueType, "c") //A char
|| strstr(computed_item.valueType, "i") //An int
|| strstr(computed_item.valueType, "s") //A short
|| strstr(computed_item.valueType, "l") //A long is treated as a 32-bit quantity on 64-bit programs.
|| strstr(computed_item.valueType, "q") //A long long
|| strstr(computed_item.valueType, "C") //An unsigned char
|| strstr(computed_item.valueType, "I") //An unsigned int
|| strstr(computed_item.valueType, "S") //An unsigned short
|| strstr(computed_item.valueType, "L") //An unsigned long
|| strstr(computed_item.valueType, "Q") //An unsigned long long
|| strstr(computed_item.valueType, "B") //A C++ bool or a C99 _Bool
|| strstr(computed_item.valueType, "*") //A character string (char *)
|| strstr(computed_item.valueType, "#") //A class object (Class)
) {
if (computed_item.dirty) {
void *oldValue = computed_item.value;
void *newValue = ((void * (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
if (oldValue != newValue) {
computed_item.dirty = NO;
NSString *setterName = [self jk_setterForGetter:getterName];
void(^block)(void) = ^(void) {
computed_item.value = newValue;
};
jk_computedProperty_setter(self, NSSelectorFromString(setterName), newValue, block);
}
}
return computed_item.value;
}
// if (strstr(computed_item.valueType, "f")) {//A float
// if (computed_item.dirty) {
// NSValue *oldValue = computed_item.non_obj_value;
// float floatValue = ((float (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
// NSNumber *newValue = [NSNumber numberWithFloat:floatValue];
// if (![oldValue isEqualToValue:newValue]) {
// computed_item.dirty = NO;
// NSString *setterName = [self jk_setterForGetter:getterName];
// void(^block)(void) = ^(void) {
// computed_item.non_obj_value = newValue;
// };
// jk_computedProperty_setter(self, NSSelectorFromString(setterName), &floatValue, block);
// }
// }
// float floatValue = [(NSNumber *)computed_item.non_obj_value floatValue];
// void *value = &floatValue;
// return value;
// }
// if (strstr(computed_item.valueType, "d")) {//A double
// if (computed_item.dirty) {
// NSValue *oldValue = computed_item.non_obj_value;
// double doubleValue = ((double (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
// NSNumber *newValue = [NSNumber numberWithDouble:doubleValue];
// if (![oldValue isEqualToValue:newValue]) {
// computed_item.dirty = NO;
// NSString *setterName = [self jk_setterForGetter:getterName];
// void(^block)(void) = ^(void) {
// computed_item.non_obj_value = newValue;
// };
// jk_computedProperty_setter(self, NSSelectorFromString(setterName), &doubleValue, block);
// }
// }
// double doubleValue = [(NSNumber *)computed_item.non_obj_value doubleValue];
// void *value = &doubleValue;
// return value;
// }
// if (strstr(computed_item.valueType, ":")) {//A method selector (SEL)
// if (computed_item.dirty) {
// NSValue *oldValue = computed_item.non_obj_value;
// SEL selValue = ((SEL (*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
// NSValue *newValue = [NSValue value:&selValue withObjCType:computed_item.valueType];
// if (![oldValue isEqualToValue:newValue]) {
// computed_item.dirty = NO;
// NSString *setterName = [self jk_setterForGetter:getterName];
// void(^block)(void) = ^(void) {
// computed_item.non_obj_value = newValue;
// };
// jk_computedProperty_setter(self, NSSelectorFromString(setterName), &selValue, block);
// }
// }
// SEL selValue;
// [computed_item.non_obj_value getValue:&selValue];
// void *value = &selValue;
// return value;
// }
return ((void *(*)(void *, SEL))objc_msgSendSuper)(&superClazz, _cmd);
}
由于不同类型的存储方式不一样,
不支持的类型:UCGRect,IEdgeInsets,CGAffineTransform,float,double,char*,SEL 可以通过NSValue封装的形式实现
源码地址:https://github.com/xindizhiyin2014/JKKVOHelper.git