美文网首页基础知识iOS开发常用知识点
iOS-字典防崩溃机制(runtime)

iOS-字典防崩溃机制(runtime)

作者: woniu | 来源:发表于2018-01-31 18:08 被阅读221次
    在写项目的过程中难免会遇到后台开发者返回的字典中有nil的扯淡问题,前面的文章中有提到过一个第三方Dictionary+NilSafe的处理机制可以自动去除掉字典中有nil的key/value,以防止崩溃。正好Dictionary+NilSafe有runtime的应用场景以及相关知识点,所以Dictionary+NilSafe就成为了学习runtime实际应用的一个很好的剖析对象,剖析demo会在文末呈给各位看官。还有,如果本人有说错的地方还希望各位能指出来。好了,下面我们开始吧:

    一、进入到Dictionary+NilSafe类别的.m文件中,我们设置方法交换类别NSObject (Swizzling),再此处处理相关的方法交换。

    #pragma mark  设置通用的交换方法
    @implementation NSObject (Swizzling)
    
    #pragma mark 进行方法的交换
    + (BOOL)gl_swizzleMethod:(SEL)origSel withMethod:(SEL)altSel {
        //1、获取原生的方法和我们要交换的方法
        Method origMethod = class_getInstanceMethod(self, origSel);
        Method altMethod = class_getInstanceMethod(self, altSel);
        //2、如果两个方法有一个不存在返回NO
        if (!origMethod || !altMethod) {
            return NO;
        }
        //3、添加方法  若已经存在会添加失败
       BOOL ori = class_addMethod(self,
                        origSel,
                        class_getMethodImplementation(self, origSel),
                        method_getTypeEncoding(origMethod));
        NSLog(@"原方法添加:%@",ori?@"yes":@"no");
    
       BOOL alt = class_addMethod(self,
                        altSel,
                        class_getMethodImplementation(self, altSel),
                        method_getTypeEncoding(altMethod));
        
        NSLog(@"新方法添加:%@",alt?@"yes":@"no");
        //4、交换方法的实现
        method_exchangeImplementations(class_getInstanceMethod(self, origSel),
                                       class_getInstanceMethod(self, altSel));
        return YES;
    }
    //交换的类方法
    + (BOOL)gl_swizzleClassMethod:(SEL)origSel withMethod:(SEL)altSel {
        return [object_getClass((id)self) gl_swizzleMethod:origSel withMethod:altSel];
    }
    
    @end
    

    二、针对不可变字典我们还是在load里面交换法方法,然后对字典的两种初始化方法(类、对象方法)进行处理,遍历出所有的key/value并判断是否为nil,并过滤掉为nil的key/value,然后返回正确的数据。详细剖析如下:

    #pragma mark 设置不可变字典的崩溃处理
    @implementation NSDictionary (NilSafe)
    
    //我们还是在load方法中实现保证一开始就被加载,dispatch_once保证在多线程下的安全执行。
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //获取原生方法以及设置需要交换的方法  两种初始化方式都涵盖到,防止因为涵盖不周到导致的崩溃。
            [self gl_swizzleMethod:@selector(initWithObjects:forKeys:count:) withMethod:@selector(gl_initWithObjects:forKeys:count:)];
    
            [self gl_swizzleClassMethod:@selector(dictionaryWithObjects:forKeys:count:) withMethod:@selector(gl_dictionaryWithObjects:forKeys:count:)];
        });
    }
    //字典 类方法的调用,取出所有的key和value来进行比对,安顿是否有为空
    + (instancetype)gl_dictionaryWithObjects:(const id [])objects forKeys:(const id<NSCopying> [])keys count:(NSUInteger)cnt {
        id safeObjects[cnt];
        id safeKeys[cnt];
        NSUInteger j = 0;
        for (NSUInteger i = 0; i < cnt; i++) {
            id key = keys[i];
            id obj = objects[i];
            //如果key或value有为空的情况,就跳过去
            if (!key || !obj) {
                /*
                 break是结束整个循环,而continue是结束本次循环(跳过下一步),
                 为了循环的继续,我们就必须选择continue.
                 */
                continue;
            }
            //每一个value对应一个key,这个是相互对应的,详见demo。
            safeKeys[j] = key;
            safeObjects[j] = obj;
            j++;
        }
        //处理完毕之后,我们返回新的kay、value以及count,此时我们已经将nil的key&value清除掉了。
        return [self gl_dictionaryWithObjects:safeObjects forKeys:safeKeys count:j];
    }
    
    //在这里对数据进行重组,针对数据为空的情况,处理方式同上
    - (instancetype)gl_initWithObjects:(const id [])objects forKeys:(const id<NSCopying> [])keys count:(NSUInteger)cnt {
        id safeObjects[cnt];
        id safeKeys[cnt];
        NSUInteger j = 0;
        for (NSUInteger i = 0; i < cnt; i++) {
            id key = keys[i];
            id obj = objects[i];
            if (!key || !obj) {
                continue;
            }
            if (!obj) {
                obj = [NSNull null];
            }
            safeKeys[j] = key;
            safeObjects[j] = obj;
            j++;
        }
        return [self gl_initWithObjects:safeObjects forKeys:safeKeys count:j];
    }
    @end
    

    三、可变字典这里我必须要好好说道说道了,可变字典的添加数据的方式有两种,一是通过类方法直接添加一个不可变字典,二是初始化后添加key/value。

    • a:我们如果才用第一种方式添加数据,那么不可变字典为nil的时候,在不可变情况里面就已经处理了。
    • b:如果我们采用方法二,字典不可以为nil,我们需要进行处理。
    划重点啦:可变字典的处理机制和不可变数组的机制不同,在可变数组中判断key/value为nil之后return的时候,开始的时候推测一次性遍历所有的key/value,遇到数据nil的就跳出来。でも(呆毛),这样处理的话会导致nil后面的正常数据无法返回从而丢失数据。所以,由此可以推断,可变数组中是每次只一对key/value进行监测的,然后遇到nil的数据就return,然后再判断下一对数据,这样nil的数据就会被被过滤掉了。
    类方法初始化,直接添加一个不可变字典。
    NSMutableDictionary *mudic = [NSMutableDictionary dictionaryWithDictionary:dic];
    
    对象方法初始化
    @interface NSMutableDictionary<KeyType, ObjectType> : NSDictionary<KeyType, ObjectType>
    
    - (void)removeObjectForKey:(KeyType)aKey;
    - (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;
    - (instancetype)init NS_DESIGNATED_INITIALIZER;
    - (instancetype)initWithCapacity:(NSUInteger)numItems NS_DESIGNATED_INITIALIZER;
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
    
    @end
    
    @interface NSMutableDictionary<KeyType, ObjectType> (NSExtendedMutableDictionary)
    
    - (void)addEntriesFromDictionary:(NSDictionary<KeyType, ObjectType> *)otherDictionary;
    - (void)removeAllObjects;
    - (void)removeObjectsForKeys:(NSArray<KeyType> *)keyArray;
    - (void)setDictionary:(NSDictionary<KeyType, ObjectType> *)otherDictionary;
    - (void)setObject:(nullable ObjectType)obj forKeyedSubscript:(KeyType <NSCopying>)key API_AVAILABLE(macos(10.8), ios(6.0), watchos(2.0), tvos(9.0));
    
    @end
    
    #pragma mark 设置可变字典的崩溃处理
    @implementation NSMutableDictionary (NilSafe)
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //获取可变字典的类名。调用方法进行交换
            Class class = NSClassFromString(@"__NSDictionaryM");
            [class gl_swizzleMethod:@selector(setObject:forKey:) withMethod:@selector(gl_setObject:forKey:)];
            //为什么要使用下面的这个方法呢?
            [class gl_swizzleMethod:@selector(setObject:forKeyedSubscript:) withMethod:@selector(gl_setObject:forKeyedSubscript:)];
        });
    }
    //为什么在这里不讲顺序呢,这里直接使用teturn不会导致前期遍历到而导致返回,后面的数据
    //int i;
    - (void)gl_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
        //疑问:字典里面就几个数据,但是在这里执行了几百次,我是百思不得姐,希望有朝一日能解开这个千古谜团。
    //    NSLog(@"可变数组到底调用执行了几次呢:%d",i++);
        if (!aKey || !anObject) {
            //结束整个函数,这里调用的数据是每次都已调用一次,这里如果改成coutinue�呢? やめで ,必须要用到遍历循环中才可以使用。
            //如果这里我不做return处理呢,测试结果是崩溃,由此可以推断,可变数组中的数据是每次一对key/value进行监测的,然后遇到nil的数据就return,这样就不会返回nil的数据,相当于被过滤掉了。
            NSLog(@"遇到为nil的情况,执行一次");
            return;
        }
        [self gl_setObject:anObject forKey:aKey];
    }
    
    - (void)gl_setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key {
    //    NSLog(@"~~~~~~~可变数组到底调用执行了几次呢:%d",i++);
        if (!key || !obj) {
            return;
        }
        [self gl_setObject:obj forKeyedSubscript:key];
    }
    @end
    

    奉上demo:https://github.com/caiqingchong/NSDictionaryNilTest

    相关文章

      网友评论

        本文标题:iOS-字典防崩溃机制(runtime)

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