iOS底层学习——KVC

1.KVC协议定义

键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该机制来提供对其属性的间接访问。当对象符合键值编码时,其属性可通过字符串参数通过简洁、统一的消息传递接口进行寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。

本文收录:掘金【gufs镜像】《iOS底层学习——KVC》

KVC在Objective-C中的定义

KVC的定义都是对NSObject的扩展来实现的(Objective-C中有个显式的NSKeyValueCoding类别名-分类)。查看setValueForKey方法,发现其在Foundation里面,而Foundation框架是不开源的,只能在苹果官方文档查找。见下图:

image.png

如果你正在面试,或者正准备跳槽,不妨看看我精心总结的面试资料: BAT 大厂最新面试题+答案合集(持续更新中) 来获取一份详细的大厂面试资料 为你的跳槽加薪多一份保障

推荐👇:

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868 ,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

2.KVC提供的API方法

  • 我们可以学习解读苹果的官方文档,对KVC有更深的理解。

    Key-Value Coding Programming Guide

    苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。

  • 常用方法

    对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC,下面是KVC最为重要的四个方法:

       - (nullable id)valueForKey:(NSString *)key;                          // 直接通过Key来取值
       - (void)setValue:(nullable id)value forKey:(NSString *)key;          // 通过Key来设值
       - (nullable id)valueForKeyPath:(NSString *)keyPath;                  // 通过KeyPath来取值
       - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  // 通过KeyPath来设值
    
    
  • 特殊方法

    当然NSKeyValueCoding类别中还有其他的一些方法,这些方法在碰到特殊情况或者有特殊需求还是会用到的。

    // 默认返回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;
    
    // 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    
    
    
  • 结构体处理

    KVC在进行结构体处理时,需要用到NSValue,设值时,将结构体封装成NSValue,进行键值设值;取值同样返回NSValue,然后按照结构体格式进行解析,见下面代码:

        // 结构体
        ThreeFloats floats = {1.,2.,3.};
        // 封装成NSValue
        NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
        // 设值
        [person setValue:value forKey:@"threeFloats"];
    
        // 取值
        NSValue *value1    = [person valueForKey:@"threeFloats"];
        // 结构体解析
        ThreeFloats th;
        [value1 getValue:&th];
        NSLog(@"%f-%f-%f",th.x,th.y,th.z);
    
    
  • 字典处理(模型转换)

    字典可以实现与模型进行装换,也可以通过键值数组从模型中获取字典数据。见下面代码:

    - (void)dictionaryTest{
        // 字典
        NSDictionary* dict = @{
                               @"name":@"Cooci",
                               @"nick":@"KC",
                               @"subject":@"iOS",
                               @"age":@18,
                               @"length":@180
                               };
        // 模型
        LGStudent *p = [[LGStudent alloc] init];
        // 字典转模型
        [p setValuesForKeysWithDictionary:dict];
    
        // 键值数组
        NSArray *array = @[@"name",@"age"];
        // 从模型中获取响应的字典数据
        NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
        NSLog(@"%@",dic);
    }
    
    

3.KVC设值取值顺序

KVC是怎么使用的,我们都很清楚,那么KVC在内部是按什么样的顺序来寻找key的呢?这是我们要探索的重点。

1.设值

当调用setValue:forKey:代码时,底层的执行机制是怎样的呢?在官方文档中有相关的说明,见下图:

image.png
  • 翻译过来的意思是:

    setValue:forKey: 的默认实现,给定keyvalue参数作为输入,尝试将名为key的属性设置为value,在接收调用的对象内部,使用以下过程:按顺序查找名为 set<Key>:_set<Key> 的第一个访问器。 如果找到,则使用输入值(或根据需要展开的值)调用它并完成。如果未找到简单访问器,并且类方法 accessInstanceVariablesDirectly返回 YES,则按顺序查找名称类似于 _<key>_is<Key><key>is<Key> 的实例变量。 如果找到,直接使用输入值(或解包值)设置变量并完成。 在未找到访问器或实例变量时,调用 setValue:forUndefinedKey:。 默认情况下,这会引发异常,但 NSObject 的子类可能会提供特定于键的行为。

  • 根据上的官方内容,可以得出如下实现机制:

    • 按顺序查找名为set<Key>_set<Key> 或者setIs<Key>setter访问器顺序查找,如果找到就调用它
    • 只要实现任意一个,那么就会将调用这个方法,将属性的值设为传进来的值
    • 如果没有找到这些setter方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法;
    • 如果返回YESKVC机制会优先搜索该类里面有没有名为_<Key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以_<Key>命名的变量,KVC都可以对该成员变量赋值
    • KVC机制再会继续搜索_is<Key><key>is<key>的成员变量,再给它们赋值
    • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
  • [person setValue:@"newName" forKey:@"name"];为例,可以得出以下结论:

    • 优先通过setter方法,进行属性设置,调用顺序是:
      1. setName
      2. _setName
      3. setIsName
    • 如果以上方法均未找到,并且accessInstanceVariablesDirectly返回YES,则通过成员变量进行设置,顺序是:
      1. _name
      2. _isName
      3. name
      4. isName

    可通过案例进行验证,这里不再展示。

  • accessInstanceVariablesDirectly说明

    重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO,这样的话,如果KVC没有找到set<Key>_set<Key>setIs<Key>相关方法时,会直接用setValue:forUndefinedKey:方法。我们用代码来测试一下上面的KVC机制:

    @interface LGPerson : NSObject
    {
        @public
            NSString *_isName;
            NSString *name;
            NSString *isName;
            NSString *_name;
    }
    @end
    @implementation LGPerson
    
    +(BOOL)accessInstanceVariablesDirectly{
        return NO;
    }
    
    -(id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"出现异常,该key不存在%@",key);
        return nil;
    }
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
         NSLog(@"出现异常,该key不存在%@",key);
    }
    
    // 设置方法全部注释掉
    // -(void)setName:(NSString*)name{
    //     toSetName = name;
    // }
    // - (void)_setName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }
    // - (void)setIsName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson* person = [LGPerson new];
            [person setValue:@"NewName" forKey:@"name"];
            NSString* name = [person valueForKey:@"name"];
            NSLog(@"value for key : %@",name);
    
            NSLog(@"取值_name:%@",person->_name);
            NSLog(@"取值_isName:%@",person->_isName);
            NSLog(@"取值name:%@",person->name);
            NSLog(@"取值isName:%@",person->isName);
        }
        return 0;
    }
    
    

    运行结构见下图:

    image.png

    这说明了重写+(BOOL)accessInstanceVariablesDirectly方法让其返回NO后,KVC找不到set<Key>等方法后,不再去找<Key>系列成员变量,而是直接调用setValue:forUndefinedKey:方法,所以开发者如果不想让自己的类实现KVC,就可以这么做。

    KVC设值流程图

    image.png

    2.取值

    当调用valueForKey:的代码时,底层的执行机制又是怎样的呢?在官方文档中有相关的说明,见下图:

    iShot2021-07-25 21.53.39.png
  • 根据上的官方内容,翻译之后可以得出如下实现机制:

    • 首先按get<Key><Key>is<Key>_<Key>的顺序方法查找getter方法,找到的话会直接调用,如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
    • 如果上面的getter没有找到,KVC则会查找countOf<Key>objectIn<Key>AtIndex<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>objectIn<Key>AtIndexAt<Key>Indexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
    • 如果上面的方法没有找到,那么会同时查找countOf<Key>enumeratorOf<Key>memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>memberOf<Key>组合的形式调用。
    • 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<Key>_is<Key><Key>is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
    • 还没有找到的话,调用valueForUndefinedKey:
  • [person valueForKey:@"name"];为例

    • getter方法的调用顺序是:
      1. getName
      2. name
      3. isName
      4. _name
    • 如果以上方法没有找到,accessInstanceVariablesDirectly返回YES,则直接返回成员变量,获取顺序依然是:
      1. _name
      2. _isName
      3. name
      4. isName

    可通过案例进行验证,这里不再展示。

  • KVC取值流程图

    image.png

    可以通过下面的代码对以上结论进行验证!

        @interface LGPerson : NSObject
        {
            @public
                NSString *_isName;
                NSString *name;
                NSString *isName;
                NSString *_name;
        }
        @end
        @implementation LGPerson
    
        +(BOOL)accessInstanceVariablesDirectly{
            return NO;
        }
    
        -(id)valueForUndefinedKey:(NSString *)key{
            NSLog(@"出现异常,该key不存在%@",key);
            return nil;
        }
        -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
             NSLog(@"出现异常,该key不存在%@",key);
        }
    
        // 设置方法全部注释掉
        // -(void)setName:(NSString*)name{
        //     toSetName = name;
        // }
        // - (void)_setName:(NSString *)name{
        //     NSLog(@"%s - %@",__func__,name);
        // }
        // - (void)setIsName:(NSString *)name{
        //     NSLog(@"%s - %@",__func__,name);
        // }
    
        // 取值方法
        //- (NSString *)getName{
        //    return NSStringFromSelector(_cmd);
        //}
        //- (NSString *)name{
        //    return NSStringFromSelector(_cmd);
        //}
        //- (NSString *)isName{
        //    return NSStringFromSelector(_cmd);
        //}
        //- (NSString *)_name{
        //    return NSStringFromSelector(_cmd);
        //}
        @end
    
        int main(int argc, const char * argv[]) {
            @autoreleasepool {
                // insert code here...
                LGPerson* person = [LGPerson new];
                [person setValue:@"NewName" forKey:@"name"];
                NSString* name = [person valueForKey:@"name"];
                NSLog(@"value for key : %@",name);
    
                NSLog(@"取值_name:%@",person->_name);
                NSLog(@"取值_isName:%@",person->_isName);
                NSLog(@"取值name:%@",person->name);
                NSLog(@"取值isName:%@",person->isName);
            }
            return 0;
        }
    
    

    4.在KVC中使用keyPath

    除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如,对当前对象的location属性的country属性进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。

        [person setValue:@"" forKeyPath:@"location.country"];
    
    

    通过keyPath对数组进行取值时,并且数组中存储的对象类型都相同,可以通过valueForKeyPath:方法指定取出数组中所有对象的某个字段。例如下面例子中,通过valueForKeyPath:将数组中所有对象的name属性值取出,并放入一个数组中返回。

        NSArray *names = [array valueForKeyPath:@"name"];
    
    
    image.png

    5.异常处理

    当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个异常,并且应用程序Crash。见下图:

    image.png

    我们可以重写下面两个方法:

        -(id)valueForUndefinedKey:(NSString *)key{
            NSLog(@"出现异常,该key不存在%@",key);
            return nil;
        }
    
        -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
             NSLog(@"出现异常,该key不存在%@",key);
        }
    
    

    重写这两个方法之后,运行程序不再崩溃,见下图:

    image.png

    但是我们可以根据业务需要,合理的处理KVC导致的异常。比如下面的处理方式:

    - (void)setNilValueForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            [self setValue:@"" forKey:@”age”];
        } else {
            [super setNilValueForKey:key];
        }
    }
    
    

    6.自定义KVC

    根据苹果官方文档提供的设值、取值规则,我们可以自己进行KVC的自定义实现。见下面实现代码:

    // KVC 自定义
    @implementation NSObject (LGKVC)
    
    // 设置
    - (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
        // 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:判断是否能够直接赋值实例变量——NO
        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];
    }
    
    // 取值
    - (nullable id)lg_valueForKey:(NSString *)key{
    
        // 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:判断是否能够直接赋值实例变量-YES、NO
        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;
    }
    
    @end
    
    posted @   iOS开发专栏  阅读(112)  评论(0编辑  收藏  举报
    编辑推荐:
    · AI与.NET技术实操系列:基于图像分类模型对图像进行分类
    · go语言实现终端里的倒计时
    · 如何编写易于单元测试的代码
    · 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
    · .NET Core 中如何实现缓存的预热?
    阅读排行:
    · 25岁的心里话
    · 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
    · 零经验选手,Compose 一天开发一款小游戏!
    · 因为Apifox不支持离线,我果断选择了Apipost!
    · 通过 API 将Deepseek响应流式内容输出到前端
    历史上的今天:
    2020-08-24 iOS优化篇之App启动时间优化
    点击右上角即可分享
    微信分享提示