• [OC学习笔记]KVC原理


    KVC的全称是Key-Value Coding,翻译成中文是键值编码,键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来间接访问其属性。可以通过一个字符串key来访问某个属性。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。关于这部分内容,我之前有过简单介绍:[iOS开发]KVC

    KVC介绍

    键值编码是由 NSKeyValueCoding 非正式协议启用的一种机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码时,其属性可通过简洁、统一的消息传递接口通过字符串参数进行寻址。此间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
    通常使用访问器方法来访问对象的属性。get 访问器(或 getter)返回属性的值。set 访问器(或 setter)设置属性的值。在 Objective-C 中,您还可以直接访问属性的基础实例变量。以这些方式中的任何一种访问对象属性都很简单,但需要调用特定于属性的方法或变量名称。随着属性列表的增长或更改,访问这些属性的代码也必须如此。相反,符合键值编码的对象提供了一个简单的消息传递接口,该接口在其所有属性中都是一致的。
    键值编码是许多其他 Cocoa 技术的基础,例如键值观察、Cocoa 绑定、Core Data 和 AppleScript-ability。在某些情况下,键值编码还有助于简化代码。

    KVC相关API

    常用方法

    通过key设值/取值

    //直接通过Key来取值
    - (nullable id)valueForKey:(NSString *)key;
    
    //通过Key来设值
    - (void)setValue:(nullable id)value forKey:(NSString *)key;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通过keyPath设值/取值

    //通过KeyPath来取值
    - (nullable id)valueForKeyPath:(NSString *)keyPath; 
    
    //通过KeyPath来设值                 
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其他方法

    // 默认返回YES,表示如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
    + (BOOL)accessInstanceVariablesDirectly;
    
    // KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
    - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
    
    // 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
    
    // 如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
    - (nullable id)valueForUndefinedKey:(NSString *)key;
    
    // 和上一个方法一样,但这个方法是设值。
    - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
    
    // 如果你在SetValue方法时面给Value传nil,则会调用这个方法
    - (void)setNilValueForKey:(NSString *)key;
    
    // 使用字典为Model赋值
    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    
    // 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    KVC底层原理探索

    KVC设值底层原理

    在日常开发中,针对对象属性的赋值,一般有以下两种方式:

    • 直接通过setter方法赋值
    • 通过KVC键值编码的相关API赋值
    Person *me = [[Person alloc] init];
    // setter
    me.name = @"Billy";
    NSLog(@"%@", me.name);
    // KVC
    [me setValue:@"BILLY" forKey:@"name"];
    NSLog(@"%@", [me valueForKey:@"name"]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    下面针对较常使用的KVC设值方法:setValue:forKey,来进行其底层原理的探索。点进其声明,发现它在Foundation框架里,而Foundation框架是不开源的。像前面文章里提到的,可以通过Hopper反汇编查看,但我没看懂(hhh),接下来我们就通过阅读苹果官方文档来学习:Key-Value Coding Programming Guide
    针对设值流程,当调用setValue:forKey:设置属性value时,其底层的执行流程为:

    1. 【第一步】首先查找是否有这三种setter方法,按照查找顺序为set:->_set->setIs

      如果有其中任意一个setter方法,则直接设置属性的value(主注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范)
      如果都没有,则进入【第二步】

    2. 【第二步】:如果没有第一步中的三个简单的setter方法,则查找accessInstanceVariablesDirectly是否返回YES

      如果返回YES,则查找间接访问的成员变量进行赋值,查找顺序为:_ -> _is -> -> is
      如果找到其中任意一个实例变量,则赋值
      如果都没有,则进入【第三步】

      如果返回NO,则进入【第三步】

    3. 【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

    我们可以试着验证一下,先来验证一下第一步:

    [me setValue:@"BILLY" forKey:@"name"];
    
    • 1

    可以发现,优先进入set:
    请添加图片描述
    再者是_set
    请添加图片描述
    最后是setIs
    请添加图片描述
    第一步验证完毕。注意第二步,官方文档写的是instance variable,实际上应该是成员变量。我们试验一下。
    请添加图片描述

    @interface Person : NSObject {
        NSString *_name;
        NSString *_isName;
        NSString *name;
        NSString *isName;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出结果:
    请添加图片描述
    删除_name成员变量,输出:
    请添加图片描述
    继续删除_isName成员变量,输出:
    请添加图片描述
    继续删除name成员变量,输出:
    请添加图片描述
    接下来验证第三步,重写setValue:forUndefinedKey:方法:

    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
        NSLog(@"setValue:forUndefinedKey:");
    }
    
    • 1
    • 2
    • 3

    打印输出:
    请添加图片描述
    在网路上看到一张图片非常好:
    请添加图片描述

    KVC取值底层原理

    同样的,我们可以通过官方文档分析KVC取值的底层原理
    当调用valueForKey:时,其底层的执行流程如下

    1. 【第一步】首先查找getter方法,按照get-> -> is -> _的方法顺序查找

      如果找到,则进入【第五步】
      如果没有找到,则进入【第二步】

    2. 【第二步】如果第一步中的getter方法没有找到,KVC会查找countOfobjectInAtIndex:AtIndexes:

      如果找到countOf和其他两个中的一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即NSKeyValueArray,是NSArray的子类。代理对象随后将接收到的所有NSArray消息转换为countOfobjectInAtIndex:AtIndexes:消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
      如果没有找到这三个访问数组的,请继续进入【第三步】

    3. 【第三步】如果没有找到上面的几种方法,则会同时查找countOfenumeratorOfmemberOf这三个方法

      如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOfenumeratorOfmemberOf:消息的某种组合,用于创建它的对象
      如果还是没有找到,则进入【第四步】

    4. 【第四步】如果还没有找到,检查类方法InstanceVariablesDirectly是否YES,依次搜索__isis的实例变量

      如果搜到,直接获取实例变量的值,进入【第五步】

    5. 【第五步】根据搜索到的属性值的类型,返回不同的结果

      如果是对象指针,则直接返回结果
      如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它
      如果是是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象

    6. 【第六步】如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

    接下来试验一下:

    - (NSString *)getName {
        NSLog(@"getName");
        return nil;
    }
    
    - (NSString *)name {
        NSLog(@"name");
        return nil;
    }
    
    - (NSString *)isName {
        NSLog(@"isName");
        return nil;
    }
    
    - (NSString *)_name {
        NSLog(@"_name");
        return nil;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出:
    请添加图片描述
    注释掉,继续看:
    请添加图片描述
    请添加图片描述
    请添加图片描述
    第一步验证完了,接下来看第二步,随便实现一下我们也同时试着查看对应的类型:

    - (int)countOfName {
        return 3;
    }
    
    - (id)objectInNameAtIndex:(NSUInteger)index {
        return [NSString stringWithFormat:@"name%ld", index];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    请添加图片描述

    - (int)countOfName {
        return 3;
    }
    
    - (NSArray *)nameAtIndexes:(NSIndexSet *)indexes {
        NSArray *array = @[@"name1", @"name2", @"name3"];
        return [array objectsAtIndexes:indexes];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    请添加图片描述

    接着试第三步:

    - (int)countOfName {
        return 3;
    }
    
    - (NSEnumerator *)enumeratorOfName {
        NSSet *set = [[NSSet alloc] initWithArray:@[@"name1", @"name2", @"name3"]];
        return set.objectEnumerator;
    }
    
    - (id)memberOfName:(NSString *)name {
        NSSet *set = [[NSSet alloc] initWithArray:@[@"name1", @"name2", @"name3"]];
        return [set member:name];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    输出结果:
    请添加图片描述
    可以发现,实现的方法不同,KVC帮我们“取到”东西类型也不同,因为取到的这个实际上是它为我们创建的。接下来看四五步可以轻易验证:

    {
        NSString *_name;
        NSNumber *_isName;
        NSInteger name;
        NSString *isName;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    请添加图片描述
    在这里插入图片描述
    请添加图片描述
    请添加图片描述
    再用一下在网路上看到的图片来总结过程:
    在这里插入图片描述

    自定义KVC

    原理:通过给NSObject添加分类MYKVC,实现自定义的billy_setValue:forKey:billy_valueForKey:方法,仿照苹果官方文档提供的查找规则进行实现。

    @interface NSObject (MYKVC)
    
    - (void)billy_setValue:(nullable id)value forKey:(NSString *)key;
    - (nullable id)billy_valueForKey:(NSString *)key;
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    自定义KVC设值

    自定义KVC设置流程,主要分为以下几个步骤:

    1. 判断key非空
    2. 查找setter方法,顺序是:setXXX_setXXXsetIsXXX
    3. 判断是否响应accessInstanceVariablesDirectly方法,即间接访问实例变量
      (1) 返回YES,继续下一步设值
      (2) 如果是NO,则崩溃
    4. 间接访问变量赋值(只会走一次),顺序是:_key_isKeykeyisKey
      (1) 定义一个收集实例变量的可变数组
      (2) 通过class_getInstanceVariable方法,获取相应的 ivar
      (3) 通过object_setIvar方法,对相应的 ivar 设置值
    5. 如果找不到相关实例变量,则抛出异常

    下面给出代码:

    - (void)billy_setValue:(id)value forKey:(NSString *)key {
        //1、判断key 是否存在
        if (key == nil || key.length == 0) return;
            
        //2、找setter方法,顺序是:setXXX、_setXXX、 setIsXXX
        // key 首字母要大写 Key
        NSString *Key = key.capitalizedString;
        // key 要大写
        NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];
        NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];
        NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];
            
        if ([self billy_performSelectorWithMethodName:setKey value:value]) {
            NSLog(@"My KVC goes: %@", setKey);
            return;
        } else if ([self billy_performSelectorWithMethodName:_setKey value:value]){
            NSLog(@"My KVC goes: %@", _setKey);
            return;
        } else if ([self billy_performSelectorWithMethodName:setIsKey value:value]){
            NSLog(@"My KVC goes: %@", setIsKey);
            return;
        }
            
            
        //3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
        if (![self.class accessInstanceVariablesDirectly]) {
            @throw [NSException exceptionWithName:@"BillyUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
        }
            
        //4、间接访问变量赋值,顺序为:_key、_isKey、key、isKey
        // 4.1 定义一个收集实例变量的可变数组
        NSMutableArray *mArray = [self getIvarListName];
        // _ _is  is
        NSString *_key = [NSString stringWithFormat:@"_%@", key];
        NSString *_isKey = [NSString stringWithFormat:@"_is%@", key];
        NSString *isKey = [NSString stringWithFormat:@"is%@", key];
        if ([mArray containsObject:_key]) {
            // 4.2 获取相应的 ivar
            Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
            // 4.3 对相应的 ivar 设置值
            object_setIvar(self, ivar, value);
            return;
        } else if ([mArray containsObject:_isKey]) {
            
            Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
            object_setIvar(self, ivar, value);
            return;
        } else if ([mArray containsObject:key]) {
            
            Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
            object_setIvar(self, ivar, value);
            return;
        } else if ([mArray containsObject:isKey]) {
            
            Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
            object_setIvar(self, ivar, value);
            return;
        }
            
        //5、如果找不到则抛出异常
        @throw [NSException exceptionWithName:@"BillyUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    }
    
    - (BOOL)billy_performSelectorWithMethodName:(NSString *)name value:(nullable id)value {
        SEL selecter = NSSelectorFromString(name);
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        if ([self respondsToSelector:selecter]) {
            [self performSelector:selecter withObject:value];
            return YES;
        } else {
            return NO;
        }
    #pragma clang diagnostic pop
    }
    
    - (NSMutableArray *)getIvarListName {
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        NSMutableArray *array = [NSMutableArray array];
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            //获取实例变量名
            const char*cName = ivar_getName(ivar);
            NSString *ivarName = [NSString stringWithUTF8String:cName];
            [array addObject:ivarName];
        }
        return array;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    自定义KVC取值

    自定义KVC取值分为以下几步

    1. 判断key非空
    2. 查找相应方法,顺序是:getcountOfobjectInAtIndex
    3. 判断是否能够直接赋值实例变量,即判断是否响应accessInstanceVariablesDirectly方法,间接访问实例变量,
      (1) 返回YES,继续下一步取值
      (2) 如果是NO,则崩溃
    4. 间接访问实例变量,顺序是:__isis
      (1) 定义一个收集实例变量的可变数组
      (2) 通过class_getInstanceVariable方法,获取相应的 ivar
      (3) 通过object_getIvar方法,返回相应的 ivar 的值
    - (nullable id)billy_valueForKey:(NSString *)key {
        
    //    1、判断非空
        if (key == nil || key.length == 0) {
            return nil;
        }
        
    //    2、找到相关方法:get  countOf  objectInAtIndex
        // key 首字母要大写 Key
        NSString *Key = key.capitalizedString;
        // 拼接方法
        NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
        NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
        NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
            return [self performSelector:NSSelectorFromString(getKey)];
        } else if ([self respondsToSelector:NSSelectorFromString(key)]) {
            return [self performSelector:NSSelectorFromString(key)];
        }
        //集合类型
        else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]) {
            if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
                int num = (NSInteger)[self performSelector:NSSelectorFromString(countOfKey)];
                NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
                for (int i = 0; i < num - 1; i++) {
                    num = (NSInteger)[self performSelector:NSSelectorFromString(countOfKey)];
                }
                for (int j = 0; j<num; j++) {
                    id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                    [mArray addObject:objc];
                }
                return mArray;
            }
        }
    
    #pragma clang diagnostic pop
        
    //    3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
        if (![self.class accessInstanceVariablesDirectly]) {
            @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
        }
        
    //    4.找相关实例变量进行赋值,顺序为:_、 _is、 is
        // 4.1 定义一个收集实例变量的可变数组
        NSMutableArray *mArray = [self getIvarListName];
        // 例如:_name -> _isName -> name -> isName
        NSString *_key = [NSString stringWithFormat:@"_%@",key];
        NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
        NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
        if ([mArray containsObject:_key]) {
            Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
            return object_getIvar(self, ivar);;
        } else if ([mArray containsObject:_isKey]) {
            Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
            return object_getIvar(self, ivar);;
        } else if ([mArray containsObject:key]) {
            Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
            return object_getIvar(self, ivar);;
        } else if ([mArray containsObject:isKey]) {
            Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
            return object_getIvar(self, ivar);;
        }
    
        return NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    KVC使用场景

    动态设值和取值

    常用的可以通过setValue:forKey:valueForKey:
    也可以通过路由的方式setValue:forKeyPath:valueForKeyPath:

    通过KVC访问和修改私有变量

    在日常开发中,对于类的私有属性,在外部定义的对象,是无法直接访问私有属性的,但是对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性。
    如果不想让外界使用KVC的方法访问类的成员变量,可以将accessInstanceVariablesDirectly属性设置为NO。

    多值操作(model和字典互转)

    model和字典的转换可以通过下面两个KVC的API实现

    //字典转模型
    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    
    //模型转字典
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    修改一些系统空间的内部属性

    很多UI控件都是在其内部由多个UI空间组合而成,对于这些内部控件,苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是自定义tabbar、个性化UITextField中的placeHolderText

    用KVC实现高阶消息传递

    在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合来返回另一个集合

  • 相关阅读:
    HTTP协议
    十天学前端之JS篇(七)
    Python函数绘图与高等代数互融实例(一):正弦函数与余弦函数
    第三十章 管理许可(三)
    智能化安全防护:AI防火墙的原理与应用
    YOLO之Mini_Rect Training(矩形训练)
    mybatis判断list长度与数组长度
    【愚公系列】2022年09月 微信小程序-FFmpeg的安装与使用
    Python 如何实现 Command(命令)模式?什么是 Command(命令)设计模式?
    电脑重装系统打印机脱机状态怎么恢复正常
  • 原文地址:https://blog.csdn.net/weixin_52192405/article/details/125577014