美文网首页
Tips之NSCache和Remote module

Tips之NSCache和Remote module

作者: oopp | 来源:发表于2016-11-08 09:32 被阅读125次

好久没有做记录了,前段时间版本忒忙,最近家人状况抱恙,只能忙里偷闲记录两
个挺有意思的小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!

相关文章

  • Tips之NSCache和Remote module

    好久没有做记录了,前段时间版本忒忙,最近家人状况抱恙,只能忙里偷闲记录两个挺有意思的小Tip.包含了解决思路和最终...

  • NSCache详解

    Tips:NSCache是Foundation框架提供的缓存类的实现,使用方式类似于可变字典。由于NSMutabl...

  • iOS之NSCache的简单介绍

    NSCache简单说明 NSCache属性和方法介绍 代码示例

  • 了解NSCache的基本使用

    NSCache是专门用来进行缓存处理的, NSCache简单介绍:NSCache是苹果官方提供的缓存类,具体使用和...

  • NSCache内存缓存

    NSCache 基本使用 NSCache缓存类介绍 NSCache源码

  • NSCache

    NSCache NSCache是苹果官方提供的缓存类,它的用法和NSMutableDictionary非常类似. ...

  • NSCache & NSDictionary &

    NSCache和NSURLCache一点关系也没有 NSCache和NSURLCache一点关系也没有 NSURL...

  • NSCache

    NSCache是什么? NSCache是苹果官方提供的缓存类,在AFNetWorking和SDWebImage等主...

  • Gradle 调试Transform代码

    Edit Configurations 左上角➕ 选择remote 复制箭头处内容,并选择调试的module为pl...

  • 正确使用NSCache

    NSCache NSCache是专门用来进行缓存处理的 NSCache简单介绍 1-1.NSCache是苹果官方提...

网友评论

      本文标题:Tips之NSCache和Remote module

      本文链接:https://www.haomeiwen.com/subject/tddauttx.html