iOS 底层探索—–KVC

前言

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

资源准备

KCV的简介

定义

  • KVC的全称是Key-Value Coding(键值编码),是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问,这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问,可以通过字符串来访问一个对象的成员变量或其关联的存取方法。当一个对象符合键值编码时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数来寻址。

Objective-C中,KVC相当于NSObject的分类。查看setValueForKey方法,是在Foundation里面,而Foundation框架是不开源的,只能在苹果官方文档查找。再看看 API,是Foundation框架的NSKeyValueCoding 文件:

D6937278-ECB8-4F69-A6E5-691F0786A946.png

本质上是对NSObjectNSArrayNSDictionaryNSMutableDictionaryNSOrderedSetNSSet等,增加了NSKeyValueCoding分类,让它们具备Key-Value Coding的能力。

API 介绍

  • 通过Key读取和存储:
- (nullable id)valueForKey:(NSString *)key; 
- (void)setValue:(nullable id)value forKey:(NSString *)key;
复制代码
  • 通过keyPath读取和存储:
- (nullable id)valueForKeyPath:(NSString *)keyPath; 
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
复制代码
  • 其他API
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员 
+ (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; 

//输入一组Key,返回该组Key对应的Value,再转成字典返回,用于将Model转到字典 
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
复制代码

API 使用

对象属性

  • 属性:这些是简单的值,例如标量、字符串或布尔值。值对象NSNumber和其他不可变类型NSColor也被视为属性;
  • 一对一的关系:这些是具有自己属性的可变对象。一个对象的属性可以在不改变对象本身的情况下改变;
  • 对多关系:这些是集合对象。通常使用NSArray或的实例NSSet来保存此类集合,也可能是自定义集合类型;

通过Key查询

LGPerson *person = [[LGPerson alloc] init]; 

// 通过`setter`方法为`name`属性赋值:
person.name = @"LG_Cooci";
person.age = 20; 
person->myName = @"cooci";

// 1:Key-Value Coding (KVC) : 基本类型 - 看底层原理
// 非正式协议 - 间接访问 
[person setValue:@"KC" forKey:@"name"];
复制代码

通过keyPath查询

LGStudent *student = [LGStudent alloc]; 
student.subject = @"五谷丰登"; 
person.student = student; 

[person setValue:@"Swift" forKeyPath:@"student.subject"]; 
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]); 
复制代码

获得的打印结果:Swift;通过kvc再结合路径student.subject进行了替换。

集合属性

对外的 api 接口:

  • NSMutableArray
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; // 通过 key
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath; //  通过 keyPath
复制代码
  • NSMutableSet
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key; // 通过 key
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath; //  通过 keyPath
复制代码
  • NSMutableOrderedSet
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); // 通过 key
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); //  通过 keyPath
复制代码

可以通过具体代码实现来看下:

  • 数组取值
LGStudent *p = [LGStudent new]; 
p.penArr = [NSMutableArray arrayWithObjects:@"pen0", @"pen1", @"pen2", @"pen3", nil]; 
NSArray *arr = [p valueForKey:@"penArr"];
NSLog(@"pens = %@", arr); //打印arr数组

NSEnumerator *enumerator = [arr objectEnumerator]; 
NSString* str = nil; 
while (str = [enumerator nextObject]) {
    NSLog(@"%@", str); //遍历打印
}
复制代码

打印arr数组结果:

795E6E3B-3E3D-46CD-B5DF-0631B58E3107.png

遍历打印结果:

A42AFA61-6528-4826-AE85-240D44A71022.png

  • 修改数组元素
person.array = @[@"1",@"2",@"3"]; 

// 第一种:创建新的数组,KVC赋值就 
NSArray *array = [person valueForKey:@"array"];
array = @[@"10",@"8",@"9"]; 
[person setValue:array forKey:@"array"]; 
NSLog(@"%@",[person valueForKey:@"array"]); // 修改前打印

// 第二种:使用mutableArrayValueForKey: 
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"100";
NSLog(@"%@",[person valueForKey:@"array"]); // 修改后打印
复制代码

修改前打印:

5FEF7B7A-1F8E-4230-9F3B-F4AA39CD5324.png

修改后打印:

38E447B6-3F46-46F6-B47C-E677D737F01E.png

  • 模型字典转换
NSDictionary* dict = @{ 
                      @"name":@"Cooci", 
                      @"nick":@"KC", 
                      @"subject":@"iOS", 
                      @"age":@18, 
                      @"length":@190 
                      }; 
LGStudent *s = [[LGStudent alloc] init]; 

// 字典转模型
[s setValuesForKeysWithDictionary:dict]; 
NSLog(@"模型:%@",s); 

// 键数组转模型到字典
NSArray *array = @[@"name",@"age"]; 
NSDictionary *dic = [s dictionaryWithValuesForKeys:array]; 
NSLog(@"字典:%@",dic);
复制代码

字典转模型转化:

2873993F-52E2-46B1-B102-9C8A664A9BBD.png

键数组转模型到字典打印:

03B7BD13-7E99-4E6C-AAB8-C047470B9A6C.png

  • KVC 消息传递
NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
NSArray *lenStr= [array valueForKeyPath:@"length"];

// 消息从array传递给了string
NSLog(@"%@",lenStr);

NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"]; // 关键字 lowercaseString

NSLog(@"%@",lowStr);
复制代码

打印字符串长度:

4CBE8463-7963-4188-845E-82D51784444D.png

打印字符串:

B50BA3A0-19DC-46C6-8C81-3DE3A20E2CE0.png

  • 集合运算符

集合操作符有三种基本类型:

  • 1、聚合操作符:以某种方式合并集合的对象,并返回一个通常与在正确键路径中命名的属性的数据类型匹配的对象。@count操作符是个例外,它不接受正确的键路径,并且总是返回一个NSNumber实例;

  • 2、数组操作符:返回一个NSArray实例,该实例包含命名集合中持有的对象的一些子集;

  • 3、嵌套操作符:处理包含其他集合的集合,并根据操作符返回一个NSArrayNSSet实例,以某种方式组合嵌套集合的对象;

接下来,通过案例使用来展示下。

  • 聚合操作符

3882331D-529D-4DB2-98E9-5BCF6BB2A656.png

打印结果,分别是:数组的 length、平均值、数组个数、总值、最大值、最小值:

C397DFAB-CB16-4BAB-A088-70091BE1A55A.png

  • 数组操作符

DDB753AE-D7FD-4DC0-8AE2-27529AEF3226.png

打印结果:

33ED4A09-3952-4AD3-B24B-B6EC23275F40.png

  • 嵌套操作符

08E1DEDB-8D23-4BEE-8442-2006F93D3518.png

打印结果:

排重:(排重 ---- arr = 185, 183, 179, 177 ) 
不排重:(不排重 ---- arr1 = 177, 177, 177, 183, 185, 177, 183, 179, 179, 179, 179, 179 )
复制代码

访问非对象属性

  • 默认键值编码实现使用NSNumber实例包装的标量类型:

D6E58F97-BAD7-4458-8872-1B039842D0BA.png

  • 默认存取用于包装和展开常见NSPointNSRangeNSRect、和NSSize结构:

08D96895-69EB-4474-A687-5DAA9A7FE7B9.png

  • 结构类型,可以包装在一个NSValue对象中:

结构体:

2FE8E57C-18E9-4025-A4D6-014464853B96.png

调用:

09DF6404-2B31-4145-945B-AE51EA538002.png

打印结果:

A8401991-C03A-4D4C-85DD-6C046D6D1F69.png

属性的验证

通过特定于属性的验证,方法如下:

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
复制代码

可以得到三种有可能的结果:

  • 1、如果是属性是有效的,并不改变值或没报错误,返回YES

  • 2、如果值是无效,但选择不更改它。在这种情况下,该方法返回NO。并且返回错误 outError

  • 3、如果值无效,但会创建一个新的有效对象作为替换。在这种情况下,该方法返回YES。同时保持错误对象不变。在返回之前,该方法修改值引用以指向新的值对象。当它进行修改时,该方法总是创建一个新对象,而不是修改旧对象,即使值对象是可变的;

KVC 的存储过程

通过调用setValue:forKey:实现:

  • 查找是否存在以下三种setter方法,按顺序查找名为:set<Key>:_set<Key>setIs<Key>访问器顺序查找,如果找到就调用它;

    • key是指成员变量名,首字母大小写需要符合KVC的命名规范;
    • 如果存在任意一种setter方法,都能直接设置属性的value,传入进来;
  • 如果没有找到这些setter方法,进入步骤:KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES

    • 返回YES,查找间接访问的实例变量进行赋值,查找顺序:_<key>_is<Key><key>is<Key>,如果找到其中任意一个实例变量,可对其赋值
    • 如果都未找到,就返回NO,那么将进入步骤
  • 如果setter方法或实例变量都没有找到,系统会调用该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常;

通过案例展示:

#import <Foundation/Foundation.h> 

@interface LGPerson : NSObject{ 
    @public 
        NSString *_name; 
        NSString *_isName; 
        NSString *name; 
        NSString *isName; 
} 
@end

@implementation LGPerson 
#pragma mark - 关闭/开启实例变量 
+ (BOOL)accessInstanceVariablesDirectly { 
    return YES; 
} 
@end
复制代码

setter方法

- (void)setName:(NSString *)name { 
    NSLog(@"%s - %@",__func__,name); 
} 

- (void)_setName:(NSString *)name { 
    NSLog(@"%s - %@",__func__,name); 
}

- (void)setIsName:(NSString *)name { 
    NSLog(@"%s - %@",__func__,name); 
} 

// 没有调用 
- (void)_setIsName:(NSString *)name { 
    NSLog(@"%s - %@",__func__,name); 
}
复制代码

示意图:

未命名文件.png

KVC 的读取过程

取值,是调用valueForKey:进行获取的,其流程是:

  • 、首先是查找getter方法,查找顺序为:get<Key> –> <key> –> is<Key> –> _<key>。如果找到,根据搜索到的属性值的类型,返回不同的结果:

    • 如果是对象,则直接返回结果;
    • 如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象,将其存储在NSNumber实例中并返回它;
  • 、如果未找到,KVC则会查找countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:,如果找到countOf<Key>或其他两个中的至少一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即:NSKeyValueArray,属于NSArray的子类。代理对象随后将接收到任何NSArray消息都会转换为countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息的某种组合,用来创建键值编码对象。

当然,还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。

  • 、如果还是未找到,那么会同时查找以下几种方法,countOf<Key>enumeratorOf<Key>memberOf<Key>:

如果上面三个方法都找到,就会返回一个可以响应所有NSSet方法的集合代理,此代理对象随后将其收到的所有NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的某种组合,进行调用。

  • 、如果未找到,会再接着查找accessInstanceVariablesDirectly方法的返回值,如果返回YES,依次搜索_<key>_is<Key><key>is<Key>的实例变量(不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱),找到实例变量,直接获取实例变量的值;

  • 、如果返回NO,系统调用该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。

下面通过案例来看看:

先查找 getter 方法

//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>

- (NSString *)getName{ 
    return NSStringFromSelector(_cmd); 
}

- (NSString *)name { 
    return NSStringFromSelector(_cmd); 
} 

- (NSString *)isName { 
    return NSStringFromSelector(_cmd); 
} 

- (NSString *)_name { 
    return NSStringFromSelector(_cmd); 
}
复制代码

再查找集合类型

//MARK: - 集合类型的走
// 个数 
- (NSUInteger)countOfPens { 
    NSLog(@"%s",__func__); 
    return [self.arr count]; 
} 

// 获取值 
- (id)objectInPensAtIndex:(NSUInteger)index {
    NSLog(@"%s",__func__);
    return [NSString stringWithFormat:@"pens %lu", index];
}
复制代码

接着再查找NSSet类型

//MARK: - set
// 个数
- (NSUInteger)countOfBooks {
    NSLog(@"%s",__func__);
    return [self.set count];
}

// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
    NSLog(@"%s",__func__);
    return [self.set containsObject:object] ? object : nil;
}

// 迭代器 
- (id)enumeratorOfBooks { 
    // objectEnumerator
    NSLog(@"来了 迭代编译"); 
    return [self.arr reverseObjectEnumerator]; 
}
复制代码

查找实例变量

#import <Foundation/Foundation.h> 

@interface LGPerson : NSObject { 
@public 
    NSString *_name; 
    NSString *_isName; 
    NSString *name; 
    NSString *isName; 
}

@end 

@implementation LGPerson 

#pragma mark - 关闭/开启实例变量 
+ (BOOL)accessInstanceVariablesDirectly { 
    return YES; 
} 
@end
复制代码

图解:

未命名文件-7.png

自定义 KVC

KVC存储

  • 1、判断什么 key
  • 2、setter set<Key>: or _set<Key>
  • 3、判断是否响应 accessInstanceVariablesDirectly 响应返回YES,不响应返回NO,就直接奔溃;判断是否能够直接赋值实例变量;
  • 4、间接变量,获取 ivar -> 遍历 containsObjct
    • 4.1 定义一个收集实例变量的可变数组;
    • 4.2 获取相应的 ivar
    • 4.3 对相应的 ivar 设置值;
  • 5、如果找不到相关实例;
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{

    // KVC 自定义
    // 1: 判断什么 key
    if (key == nil || key.length == 0) {
        return;
    }

    // 2: setter set<Key>: or _set<Key>,
    // key 要大写
    NSString *Key = key.capitalizedString;

    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

    if ([self lg_performSelectorWithMethodName:setKey value:value]) {

        NSLog(@"*********%@**********",setKey);
        return;

    }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {

        NSLog(@"*********%@**********",_setKey);
        return;

    }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {

        NSLog(@"*********%@**********",setIsKey);
        return;
    }

    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {

        @throw [NSException exceptionWithName:@"LGUnknownKeyException" 
           reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] 
         userInfo:nil];
    }

    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct -

    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];

    // _<key> _is<Key> <key> is<Key>
    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:@"LGUnknownKeyException" 
                                   reason:[NSString 
                         stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] 
                                 userInfo:nil];
}
复制代码

KVC取值

  • 1、筛选 key 判断非空;
  • 2、找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
  • 3、判断是否能够直接赋值实例变量;
  • 4、找相关实例变量进行赋值;
    • 4.1、定义一个收集实例变量的可变数组
// 1:刷选key 判断非空
    if (key == nil  || key.length == 0) {

        return nil;
    }

    // 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // 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 = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];

            for (int i = 0; i<num-1; i++) {
                num = (int)[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:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {

        @throw [NSException exceptionWithName:@"LGUnknownKeyException" 
                                       reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] 
                                     userInfo:nil];

    }

    // 4.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];

    // _<key> _is<Key> <key> is<Key>
    // _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 @"";
复制代码

相关方法

#pragma mark - 相关方法

- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{

    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{

    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (NSMutableArray *)getIvarListName{

    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;

    Ivar *ivars = class_copyIvarList([self class], &count);

    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享