JKKVOHelper实现对计算属性的监听与缓存
Posted JackLee18
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JKKVOHelper实现对计算属性的监听与缓存相关的知识,希望对你有一定的参考价值。
什么是计算属性
计算属性是指一个对象的一个属性的变化依赖这个对象的其他属性的变化。比如一个人的fullName 的变化依赖这个人的firstName,lastName这两个属性的变化
计算属性优化的背景
项目中存在UICollectionView,UITableView 列表需要对cell的size,height进行计算的场景,目前存在两种需要优化的地方
- 重复计算
每次渲染cell的时候都会进行计算,造成了重复计算
- 计算没有和渲染分离
渲染cell的时候才进行计算,没有将渲染和计算进行分离。如果某些计算是耗时操作,那么会造成页面滑动时流畅性降低。
对计算属性监听的目标
- 能够实现对计算属性实现计算和渲染的分离,渲染的时候不计算
- 能够实现对计算属性的缓存,计算过一次后,如果依赖的属性未发生变化,后续不再进行重复计算
- 影响范围可控,一个对象只有计算属性生效的配置后才会生效,不会影响其他对象
实现对计算的监听
配置计算属性与它依赖的属性之间的对应关系
为了实现配置计算属性与它依赖的属性之间的对应关系,我这边在NSObject+JKKVOHelper文件里创建了一个分类方法,具体如下:
- (NSDictionary <NSString *,NSArray *>*)jk_computedProperty_config
return @;
开发者在具体使用时,在相对应的类中配置一下对应关系即可。以person为例,代码如下:
- (NSDictionary<NSString *,NSArray *> *)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 <NSString *>*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 <NSString *>*)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);
遇到了哪些坑点
派生子类实现setter方法,传参不能为id类型
派生子类的方案参考了手动实现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方法,不同类型处理逻辑不一样
通过派生子类,并且实现计算属性相应的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);
待优化项
由于不同类型的存储方式不一样,
- 对于非NSObject的自定义结构体类型不支持直接的监听与缓存,需要先提前将这种类型转换为NSValue的对象作为替代的计算属性。
- 对于系统提供的一些类型,也不支持,需要参考第一条的方法对计算属性进行监听和缓存
不支持的类型:UCGRect,IEdgeInsets,CGAffineTransform,float,double,char*,SEL 可以通过NSValue封装的形式实现
源码地址:https://github.com/xindizhiyin2014/JKKVOHelper.git
QQ交流群
更多干货文章,扫描下方二维码关注公众号
以上是关于JKKVOHelper实现对计算属性的监听与缓存的主要内容,如果未能解决你的问题,请参考以下文章