美文网首页
NSNull与工厂模式和runtime之间的问题

NSNull与工厂模式和runtime之间的问题

作者: Trigger_o | 来源:发表于2021-07-26 19:39 被阅读0次

    今天遇到群友们在讨论一个处理NSNull的问题;
    在JSON格式化时,value是null的会被转换为NSNull对象:

    /*
         {
             "type" : 1,
             "time" : null,
             "avatar" : ""
         }
         */
    NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"code" ofType:@"json"]];
        NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
        id value1 = dic[@"time"];
        id value2 = dic[@"name"];
        NSLog(@"\n%@---%@--\n%@",value1,value2,dic);
    

    从输出可以看到time的值转换为NSNull对象,它是<null>
    取一个不存在的key,输出,它是(null)


    输出结果

    (null)就是nil,在OC里对nil发送消息是安全的,[(NSString *)value2 length]什么都不会发生;
    如果调用[(NSString *)value1 length],就会出现NSNull length]: unrecognized selector sent to instance;
    到这里都是老生常谈了;

    这时候有老哥说,能不能全局来处理这个问题,把NSNull转换为安全的nil;
    首先想到的方法就是交换方法,简单实用且彻底.

    NSDictionary的取值有两个方法,valueForKey:和objectForKeyedSubscript:,后者是NSExtendedDictionary增加的下标方法.也就是dic[@"time"]的写法;
    增加一个分类来交换方法,如果遇到NSNull就返回nil.

    #import "NSDictionary+Safe.h"
    #import <objc/runtime.h>
    
    @implementation NSDictionary (Safe)
    
    + (void)load{
        Method originM1 = class_getInstanceMethod(self, @selector(valueForKey:));
        Method swizzledM1 = class_getInstanceMethod(self, @selector(glvalueForKey:));
        method_exchangeImplementations(originM1, swizzledM1);
        
        Method originM2 = class_getInstanceMethod(self, @selector(objectForKeyedSubscript:));
        Method swizzledM2 = class_getInstanceMethod(self, @selector(globjectForKeyedSubscript:));
        method_exchangeImplementations(originM2, swizzledM2);
    }
    
    - (id)glvalueForKey:(NSString *)key{
        id value = [self glvalueForKey:key];
        if([value isKindOfClass:NSNull.class]){
            return nil;
        }
        return value;
    }
    
    - (id)globjectForKeyedSubscript:(id)key{
        id value = [self globjectForKeyedSubscript:key];
        if([value isKindOfClass:NSNull.class]){
            return nil;
        }
        return value;
    }
    
    @end
    

    VC里还是刚才的代码,试一下,竟然不好使,value1还是<null>,[(NSString *)value1 length]还是会crash;
    哦,差点忘了重要的事,对于类簇只操作一个公共父类是不行的,这里不能用分类了,得多操作几个.


    字典的类是__NSDictionaryM

    于是改造一下

    + (void)load{
        [self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKey:) classNmae:@"__NSDictionaryM"];
        [self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscript:) classNmae:@"__NSDictionaryM"];
        [self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKey:) classNmae:@"__NSSingleEntryDictionaryI"];
        [self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(objectForKeyedSubscript:) classNmae:@"__NSSingleEntryDictionaryI"];
        [self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKey:) classNmae:@"NSDictionary"];
        [self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscript:) classNmae:@"NSDictionary"];
    }
    
    + (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae:(NSString *)className{
        Method originM = class_getInstanceMethod(NSClassFromString(className), sel1);
        Method swizzledM = class_getInstanceMethod(NSClassFromString(className), sel2);
        method_exchangeImplementations(originM, swizzledM);
    }
    

    尝试输出length

    NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"code" ofType:@"json"]];
        NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
        id value1 = dic[@"time"];
        id value2 = dic[@"name"];
        NSLog(@"\n%@---%@--\n%@\nlength=%ld",value1,value2,dic,[(NSString *)value1 length]);
    

    果然可以了


    value1也是(null)了

    看似清晰了,但是问题还远没有搞定,刚才替换了objectForKeyedSubscript:和valueForKey:还没有试一试valueForKey:行不行,把value换成下面这样,果然还是可以的.

    id value1 = [dic valueForKey:@"time"];
    

    那再试试不可变的呢

     NSDictionary *dic = @{@"photo" : [NSNull new]};
        id s1 = dic[@"photo"];
        id s2 = [dic valueForKey:@"photo"];
        NSLog(@"\n%ld -- %ld",[(NSString *)s1 length],[(NSString *)s2 length]);
    

    运行,也没问题


    不可变的字典

    是不是真的没问题了,这时候有老哥说我不需要可变的,只保留了"__NSSingleEntryDictionaryI"和"NSDictionary",结果不改不要紧,一改出大问题;
    注释了"__NSDictionaryM"的交换方法,上面那个不可变字典的例子缺crash了,具体来说是valueForKey:获取的s2闪退了,因为s2仍然是NSNull对象,不是nil,转换失败.
    接着又试着把"__NSSingleEntryDictionaryI"和"NSDictionary"的交换方法来回注释,总是只保留两个,发现可变和不可变字典可能转换成功,也可能转换失败,及其混乱.

    + (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae:(NSString *)className{
        Method originM = class_getInstanceMethod(NSClassFromString(className), sel1);
        Method swizzledM = class_getInstanceMethod(NSClassFromString(className), sel2);
        method_exchangeImplementations(originM, swizzledM);
    }
    

    回头看看这个方法,两个选择器都是className的方法,如果swizzledM都固定成NSDictionary呢;
    IMP作为函数指针,假设自定义的方法是P,A和P交换,调用A会走P,然后B又跟P交换,调用A直接变成B了,为了验证我们可以写一个demo试一试.

    #import "Father.h"
    #import <objc/runtime.h>
    
    @implementation Father
    
    + (void)load{
        [self swizzlingWithSel1:@selector(methodA) sel2:@selector(methodB)];
        [self swizzlingWithSel1:@selector(methodA) sel2:@selector(methodC)];
    }
    
    - (instancetype)init{
        if(self = [super init]){
            [self methodA];
            [self methodB];
            [self methodC];
        }
        return self;
    }
    
    + (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2{
        Method originM = class_getInstanceMethod(self, sel1);
        Method swizzledM = class_getInstanceMethod(self, sel2);
        method_exchangeImplementations(originM, swizzledM);
    }
    
    - (void)methodA{
        NSLog(@"A");
    }
    
    - (void)methodB{
        NSLog(@"B");
    }
    
    - (void)methodC{
        NSLog(@"C");
    }
    
    @end
    

    首先互换对象自己的方法,用这样一个简单的例子就能看到,这里调用方法ABC,输出的是"CAB";
    假设A是0x001,B是0x002,C是0x003,第一轮变成0x002,0x001,0x003,第二轮变成0x003,0x001,0x002


    输出结果

    那如果是交换父类的方法呢,创建出Father的子类SonA,结果也是一样的,代码就不贴了;
    如果SonA重写了方法呢,

    + (void)load{
        [self swizzlingWithSel1:@selector(methodA) sel2:@selector(methodB) classNmae:@"SonA"];
        [self swizzlingWithSel1:@selector(methodB) sel2:@selector(methodC) classNmae:@"SonA"];
    }
    
    @implementation SonA
    
    - (void)methodA{
        NSLog(@"new A");
    }
    
    @end
    

    这段代码sonA重写了methodA,然后自己和自己交换,结果和之前是一样的


    结果还是一样的
    + (void)load{
        [self swizzlingWithSel1:@selector(methodA) sel2:@selector(methodB) classNmae1:@"SonA" classNmae2:@"SonB"];
        [self swizzlingWithSel1:@selector(methodB) sel2:@selector(methodC) classNmae1:@"SonA" classNmae2:@"SonB"];
    }
    
    + (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae1:(NSString *)className1 classNmae2:(NSString *)className2{
        Method originM = class_getInstanceMethod(NSClassFromString(className1), sel1);
        Method swizzledM = class_getInstanceMethod(NSClassFromString(className2), sel2);
        method_exchangeImplementations(originM, swizzledM);
    }
    

    A和B交换也是一样的,同样如果B也重写了,那就会和B的方法交换,没重写,那就是和父类方法交换,其实都是一回事,和我们理解的IMP没有出入.

    现在回到一开始的问题,到这里就已经清晰了,首先字典替换后的globjectForKeyedSubscript:方法是写在NSDictionary的分类里的,NSDictionary的子类__NSDictionaryM等,都没有实现gl方法,当调用替换的时候,他们都会走父类的这个新增的gl方法,load的最后一行父类的gl方法和原本的方法互换,也就是说,_NSDictionaryM调用valueForKey:其实是走了分类里的glvalueForKey:,最后glvalueForKey调用父类原本的valueForKey:
    即使__NSDictionaryM重写了valueForKey:方法,也没能生效,并且__NSDictionaryM的superclass是NSMutableDictionary,NSMutableDictionary的superclass才是NSDictionary

    如果考虑到NSArray格式化也会存在NSNull,把NSArray的几个子类实现交换方法,会出现一堆的问题,比如method only defined for abstract class.或者系统内部的越界等等.

    所以最后,这算是一个乌龙,因为都忽略了一个重要问题,
    多个子类的方法都跟同一个方法交换显然是不行的,应该给每一个子类都准备一个方法

    NSDictionary

    @implementation NSDictionary (Safe)
    
    + (void)load{
        [self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKeyM:) classNmae:@"__NSDictionaryM"];
        [self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscriptM:) classNmae:@"__NSDictionaryM"];
        [self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKeyI:) classNmae:@"__NSSingleEntryDictionaryI"];
        [self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscriptI:) classNmae:@"__NSSingleEntryDictionaryI"];
        [self swizzlingWithSel1:@selector(valueForKey:) sel2:@selector(glvalueForKey:) classNmae:@"NSDictionary"];
        [self swizzlingWithSel1:@selector(objectForKeyedSubscript:) sel2:@selector(globjectForKeyedSubscript:) classNmae:@"NSDictionary"];
    }
    
    + (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae:(NSString *)className{
        Method originM = class_getInstanceMethod(NSClassFromString(className), sel1);
        Method swizzledM = class_getInstanceMethod(NSClassFromString(className), sel2);
        method_exchangeImplementations(originM, swizzledM);
    }
    
    - (id)glvalueForKeyI:(NSString *)key{
        id value = [self glvalueForKeyI:key];
        if([value isKindOfClass:NSNull.class]){
            return nil;
        }
        return value;
    }
    
    - (id)globjectForKeyedSubscriptI:(id)key{
        id value = [self globjectForKeyedSubscriptI:key];
        if([value isKindOfClass:NSNull.class]){
            return nil;
        }
        return value;
    }
    
    - (id)glvalueForKeyM:(NSString *)key{
        id value = [self glvalueForKeyM:key];
        if([value isKindOfClass:NSNull.class]){
            return nil;
        }
        return value;
    }
    
    - (id)globjectForKeyedSubscriptM:(id)key{
        id value = [self globjectForKeyedSubscriptM:key];
        if([value isKindOfClass:NSNull.class]){
            return nil;
        }
        return value;
    }
    
    - (id)glvalueForKey:(NSString *)key{
        id value = [self glvalueForKey:key];
        if([value isKindOfClass:NSNull.class]){
            return nil;
        }
        return value;
    }
    
    - (id)globjectForKeyedSubscript:(id)key{
        id value = [self globjectForKeyedSubscript:key];
        if([value isKindOfClass:NSNull.class]){
            return nil;
        }
        return value;
    }
    
    @end
    

    NSArray

    @implementation NSArray (GLSafe)
    
    + (void)load{
        [self swizzlingWithSel1:@selector(objectAtIndex:) sel2:@selector(globjectAtIndexSingleI:) classNmae:@"__NSSingleObjectArrayI"];
        [self swizzlingWithSel1:@selector(objectAtIndexedSubscript:) sel2:@selector(globjectAtIndexedSubscriptSingleI:) classNmae:@"__NSSingleObjectArrayI"];
    
        [self swizzlingWithSel1:@selector(objectAtIndex:) sel2:@selector(globjectAtIndex0:) classNmae:@"__NSArray0"];
        [self swizzlingWithSel1:@selector(objectAtIndexedSubscript:) sel2:@selector(globjectAtIndexedSubscript0:) classNmae:@"__NSArray0"];
        
        [self swizzlingWithSel1:@selector(objectAtIndex:) sel2:@selector(globjectAtIndexI:) classNmae:@"__NSArrayI"];
        [self swizzlingWithSel1:@selector(objectAtIndexedSubscript:) sel2:@selector(globjectAtIndexedSubscriptI:) classNmae:@"__NSArrayI"];
      
        [self swizzlingWithSel1:@selector(objectAtIndex:) sel2:@selector(globjectAtIndexM:) classNmae:@"__NSArrayM"];
        [self swizzlingWithSel1:@selector(objectAtIndexedSubscript:) sel2:@selector(globjectAtIndexedSubscriptM:) classNmae:@"__NSArrayM"];
    }
    
    + (void)swizzlingWithSel1:(SEL)sel1 sel2:(SEL)sel2 classNmae:(NSString *)className{
        Method originM = class_getInstanceMethod(NSClassFromString(className), sel1);
        Method swizzledM = class_getInstanceMethod(NSClassFromString(className), sel2);
        method_exchangeImplementations(originM, swizzledM);
    }
    
    - (id)globjectAtIndexSingleI:(NSUInteger)index{
        id obj = [self globjectAtIndexSingleI: index];
        if([obj isKindOfClass:NSNull.class]){
            return nil;
        }
        return obj;
    }
    
    - (id)globjectAtIndexedSubscriptSingleI:(NSUInteger)idx{
        id obj = [self globjectAtIndexedSubscriptSingleI:idx];
        if([obj isKindOfClass:NSNull.class]){
            return nil;
        }
        return obj;
    }
    
    - (id)globjectAtIndex0:(NSUInteger)index{
        id obj = [self globjectAtIndex0: index];
        if([obj isKindOfClass:NSNull.class]){
            return nil;
        }
        return obj;
    }
    
    - (id)globjectAtIndexedSubscript0:(NSUInteger)idx{
        id obj = [self globjectAtIndexedSubscript0:idx];
        if([obj isKindOfClass:NSNull.class]){
            return nil;
        }
        return obj;
    }
    
    - (id)globjectAtIndexI:(NSUInteger)index{
        id obj = [self globjectAtIndexI: index];
        if([obj isKindOfClass:NSNull.class]){
            return nil;
        }
        return obj;
    }
    
    - (id)globjectAtIndexedSubscriptI:(NSUInteger)idx{
        id obj = [self globjectAtIndexedSubscriptI:idx];
        if([obj isKindOfClass:NSNull.class]){
            return nil;
        }
        return obj;
    }
    
    - (id)globjectAtIndexM:(NSUInteger)index{
        id obj = [self globjectAtIndexM: index];
        if([obj isKindOfClass:NSNull.class]){
            return nil;
        }
        return obj;
    }
    
    - (id)globjectAtIndexedSubscriptM:(NSUInteger)idx{
        id obj = [self globjectAtIndexedSubscriptM:idx];
        if([obj isKindOfClass:NSNull.class]){
            return nil;
        }
        return obj;
    }
    
    @end
    

    实际上这种通过runtime来处理Foundation的异常问题并不可靠,如果cocoa内部或者别的库就是会往集合里存NSNull,那这就会出现大问题,类似的情况还有通过runtime来防止数组越界等,越界是阻止了,但同时错误也会被隐藏,抛出异常比隐藏异常更有用,应该去收集crash,然后从触发的原因着手在根本上避免crash.

    相关文章

      网友评论

          本文标题:NSNull与工厂模式和runtime之间的问题

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