static NSMutableDictionary* gPhoneCache = nil;
- (NSString*)getFormmatedPhoneNumber:(NSNumber*)phone
{
if(phone == nil)
{
return nil;
}
NSString* phoneNumberStr = nil;
[_phoneLock lock];
if(gPhoneCache == nil)
{
gPhoneCache = @{}.mutableCopy;
}
phoneNumberStr = [gPhoneCache objectForKey:phone];
if (phoneNumberStr == nil) {
phoneNumberStr = [PhoneFormatLib formatPhoneNumber:phone];
[gPhoneCache setObject:phoneNumberStr forKey:phone];
}
[_phoneLock unlock];
return phoneNumberStr;
}
通过引入NSMutableDictionary,就避免了每次都需要重复调用 formatPhoneNumber 的问题,so easy就完成了一个快速的cache设计,马上就可以提交给测试,把优化成果甩产品经理脸上,这归功于hash表O(1)的时间复杂度。内存空间会多消耗一些,不过对于小量的数据影响比较小,现代的hash表不会一开始就分配大量的空间,而是随着数据的增加而逐渐扩容。
这种简单可用型的Cache设计,最大的问题在于,代码过于零散且不可控。小量且分散的cache设计几乎等同于挖坑,在你设计cache的时候可能数据量还小,但后面维护的时候,业务改变的时候,谁也不能保证这块内存的开销依然可以忽略不计。而且这种内存方面的损耗很难察觉,巧妙的隐蔽在某个.m文件中,到后期想控制整个App的内存开销时,会感觉到处都有坑,无从下手。你可能也发现了,上面这段Cache代码没有释放Cache的地方。
所有对我们整个App有副作用的代码都需要被集中管理,要能从架构的层面去理解和定位。怎么去定义副作用呢?可以抽象成一种「写操作」,往Cache中添加新的记录就是写操作,这种写操作的副作用是额外的内存开销,Cache的本质是以空间换时间,这空间损耗就是我们的副作用,一个副作用会引发其他更多的副作用,理清这些副作用往往需要反复查阅大量的代码。更好的办法是,一开始就把有副作用的代码集中管理。
优雅可控型Cache
避免Cache代码散乱放置的做法是,设计一个优雅可控的Cache模块。一个App中,可能会有各种各样的数据需要Cache,phoneNumberCache,avatarCache,spaceshipCache等等,我们需要有个源头来追踪这些cache,直观的做法是通过工厂类来生成和持有这些各式各样的cache:
//CacheFactory.h
@interface CacheFactory : NSObject
+ (instancetype)sharedInstance;
- (id<MyCacheProtocol>)getPhoneNumberCache;
- (void)clearPhoneNumberCache;
- (id<MyCacheProtocol>)getAvatarCache;
- (void)clearAvatarCache;
@end
这样当我们需要评估各种Cache对整个App内存开销的影响之时,只需要从CacheFactory代码着手即可,调试起来也有迹可循,其他工程师接手你的代码也会感激涕零的。
通过protocol的方式,将cache的声明和实现想分离,这也是个好习惯。cache的另一个重要知识点是cache的淘汰策略,不同的策略表现也不一样,FIFO,LRU,2Queues等等,现在有不少成熟的第三方cache框架可以使用,系统也提供了淘汰策略不明确的NSCache,如果没有动手写过任何cache淘汰策略,我还是建议大家自己动手试着做一个,至少要读一下相关的实现源码,了解这些淘汰策略很有必要,在做一些深度优化的时候需要因地制宜来做决定。










