好久没有做记录了,前段时间版本忒忙,最近家人状况抱恙,只能忙里偷闲记录两
个挺有意思的小Tip.包含了解决思路和最终的方案.也方便有遇上同样问题的同学解决问题.
NSCache的鬼
某一天,在bug系统中看到了一个Ticket,是这样记录的:
页面打开后,会闪烁.
??这是几个意思?于是按照描述,进行的bug复现.明白了闪烁是什么意思.
QA所谓的闪烁,是指的页面每次进入有图片的地方,会进行一次闪烁.也就是图片的刷新.
修复这个bug,可以有2个方法.一是解决"每次"这个问题,使得页面只有手动刷新的时候才进行图片的处理.二是找清楚问题的本质,为什么使用了缓存(sdwebimage
),仍然要"闪烁".
对于方法一,当然是不能接受的.一个是业务需要,另一个是没有本质上解决问题.于是开始查询缓存的问题.
首先进行一个判定,是否是缓存引起的.于是稍微修改下代码,把网络图片替换成了本地图片,看看现象是否仍然存在:果然消失了,于是确定是缓存问题.
其次跟踪代码,查询是否是sd缓存被清除:通过断点调试,很容易知道,的确是缓存被清空了.从sd的内存缓存中拿不到相应的key对应的图片.
然后查询代码,查询是否有以下2种简单的可能:
- 是否调用了
[[SDImageCache sharedImageCache] clearMemory]
之类的相关代码 - 是否对sd进行了相关设置,比如max size/limit/age等.
然而通过查询,没有类似的相关代码.经过一番瞎搞,似乎没什么辙了.
再想想,复现步骤有一步:按下home键再进入
.难道和生命周期相关?不过不好排查,原因是:
- 操作步骤包含多个生命周期,例如
enter background
,enter forgeground
. - 除了
app delete
中的代理以外,还包含了分散在整个项目内的通知,并且还有较多的他人模块.
不过麻烦也得做,从简到难.首先排除app delegate
中是否有影响:果断注释掉,不过现象仍然存在.
然后再来排除通知:这个难度就比较大了,注册的地方太多,再加上几种通知...
没办法了么?想了想,我们如果交换了通知,拦截我们需要的通知.这样就不会发送生命周期相关的通知,注册的地方就全部失效了.是不是能知道些什么呢?
几行代码搞定,拦截了涉及到的几个生命周期.最后发现是enter background
这个生命周期搞的鬼.
到现在为止,也就是发现了在sd中,一旦enter background
就会清除缓存.于是我们猜测,是sd故意这么写么?这不科学啊.
追踪到sd的源码,内存缓存(memory cache),就是一个NSCache
.于是给sd的清除内存缓存的方法打上断点,看看在进入后台时是否会执行:然而答案是令人失望的,并没有.也就是说,sd本身并没有做这个操作.
这就诡异了,项目中没有类似的操作,sd没有,那...难道是系统的?
如果是系统的,那就是系统调用了这个NSCache的清除方法.因为是全页面的闪烁,也就是全部缓存都被清除,而不是针对某一个缓存.那么看看NSCache
的方法,自然就怀疑到了removeAllObjects
这个家伙身上.
于是再次使用run time
赋予我们的神器:交换,来验证我们的想法:NSCache
在进入后台的时候,会自动的删除相关的value,调用removeAllObjects
这个方法.
Hook了removeAllObjects
方法,答案水落石出.
Well done,果然是这样的.原因找出来了.剩下的事情就简单了,调查下具体是怎么一回事.
原来有这么个协议:NSDiscardableContent
.这个协议一共有4个required的方法:
@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess;
- (void)endContentAccess;
- (void)discardContentIfPossible;
- (BOOL)isContentDiscarded;
@end
这个协议决定了存储在NSCache
中的value的一些特性.NSCache
会在进入后台的时候对其中存储的所有value按照协议的方式进行保留或者删除(discard),如果没有实现该协议,则默认删除.
至于这个bug,大不了就是对image实现个category,category实现了该协议,然后操作代理方法,根据业务逻辑来进行判定即可.
Remote Module
组件化是吵了好久的话题了.虽然我爱凑个热闹,看看各家的吵吵闹闹:道理是越辩越明,从方便项目上升到架构的艺术.不过实在没有太大兴趣,也不敢擅自重复造轮子.但是最近实在是被部分历史代码折腾烦了,不得不也开始组件化的道路.
套路还是那个套路,没有什么新意.基本按照casa的Mediator
走.只是有一点思考:
是否要注册
在casa的Mediator
中,认为不需要注册.因为注册实际上是一种映射
,而有了target-action的话,其实只需要一种转换
即可.
然后通过url转换
成target-action进行执行:
id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
不过在实际中,因为各种原因,url本身是不会附带这些信息的.比如url是服务端定的,还要考虑到android等,所以实际情况没有那么理想.
所以才有注册一说,才有了是否需要注册的争论.
放开这些争论,这里有个Tip是:即使选择了注册方案,也无须手动注册,自动注册即可
.
因为有objc_getClassList
这个东东,小虾的公众号专门聊过.
假设在你的Router中,需要用到注册这货(不用就算了).这或许是一个方案:
- 有一组或者一个.h文件专门负责记录有哪些URL.这一步从程序上来讲,是没用的,但是从维护的角度来讲,是有意义的:
static NSString *const SchemeShooseVideo = @"jumeimall://page/choosevideo";
2.有一个protocol,来约定服务提供方提供的服务(UPDATE:现在取消了这个protocol,否则服务方会依赖这个protocol.目前是直接约定supportedSchemes方法,为了防止命名冲突,方法名可以增加前缀):
@protocol JMMallProtocol <NSObject>
@required
+ (NSArray <NSString *> *)supportedSchemes;
@end
因为服务方可能提供多个URL远程服务,所以是个NSArray
;因为我们使用target-action的方案,所以NSArray
中的是字符串,其中包含了target-action信息,也包含了其他的信息(比如url).
3.因为该服务最终由Mediator
解析成target-action并且执行,所以字符串必须按照规定的方式进行组装.所以最好提供一个helper方法.
+ (NSString *)urlFromScheme:(NSString *)url target:(NSString *)target isClass:(BOOL)isClass action:(NSString *)action {
return [NSString stringWithFormat:@"%@^%@^%@^%@",url,target,(isClass? @"C" : @"O"),action];
}
4.最后使用objc_getClassList
这货进行一个处理,当然注意下细节即可.
+ (void)load {
int classCount;
Class *classes;
classCount = objc_getClassList(NULL, 0);
if (classCount <= 0) {
return;
}
NSMutableDictionary *moduls = @{}.mutableCopy;
classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * classCount);
classCount = objc_getClassList(classes, classCount);
for (int i = 0; i < classCount; i++) {
Class c = classes[i];
const char *name = class_getName(c);
if (strncmp(name, "JM", 2) != 0 && strncmp(name, "SC", 2) != 0) {
continue;
}
SEL selector = NSSelectorFromString(@"supportedSchemes");
if (![c respondsToSelector:selector]) {
continue;
}
Method method = class_getClassMethod(c, selector);
if (strncmp(method_getDescription(method)->types, "@", 1) != 0) {
continue;
}
IMP imp = method_getImplementation(method);
NSArray <NSString *> *result = (NSArray <NSString *> *)imp(c,selector);
for (NSString *url in result) {
NSRange range = [url rangeOfString:@"^"];
if (range.location == NSNotFound) {
continue;
}
moduls[[url substringToIndex:range.location]] = url;
}
}
[[JMMediator sharedInstance] setValue:moduls forKey:@"modules"];
free(classes);
}
主要是处理字符串,所以char *
和NSString
之间的转换是挺耗时的操作,所以能用c方法的尽量用.我这里大概2w多个文件处理完毕,耗时0.1s.如果有需求,当然可以做进一步优化:)
通过这样的处理,就可以有以下效果:
- 有一个/多个列表(.h文件),可以知道需要处理那些url
- 在内存中维护了一个字典,key为url,value为我们组合的信息(target-action)
- 通过
Mediator
,在处理remote model的时候,可以通过url查询字典,拿到对应的字符串,解析后进行target-action的方式进行执行.
当然缺点就是..调用反转了:
应该由调用方决定哪个url对应执行哪个服务,而非服务方将服务进行绑定.
不过一方面是这是个架构艺术的问题,另一方面这也是个tip,如果你要这么做,可以帮着省点事.不这么做,完全可以有其他做法.Up to you!
当然不敢献丑,具体代码就不好意思拿出来了.Just a tip!