Tips之NSCache和Remote module

好久没有做记录了,前段时间版本忒忙,最近家人状况抱恙,只能忙里偷闲记录两
个挺有意思的小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中,需要用到注册这货(不用就算了).这或许是一个方案:

  1. 有一组或者一个.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!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,635评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,543评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,083评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,640评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,640评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,262评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,833评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,736评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,280评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,369评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,503评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,870评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,340评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,460评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,909评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,512评论 2 359

推荐阅读更多精彩内容

  • iOS应用架构谈 组件化方案 讨论论坛 源 简述 前几天的一个晚上在infoQ的微信群里,来自蘑菇街的Limboy...
    其实也没有阅读 1,433评论 1 9
  • 前言: 本文转自前同事casa的博文,这篇文章是基于runtime实现的iOS组件化方案,其实iOS组件化方案基本...
    monkey01阅读 1,668评论 1 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,701评论 18 139
  • 原文链接: http://casatwy.com/iOS-Modulization.html?hmsr=touti...
    anddygon阅读 1,993评论 1 13
  • 技术无极限,从菜鸟开始,从源码开始。 由于公司目前项目还是用OC写的项目,没有升级swift 所以暂时SDWebI...
    充满活力的早晨阅读 12,656评论 0 2