美文网首页
iOS底层套索 --- Runtime(四)获取类详细属性、方法

iOS底层套索 --- Runtime(四)获取类详细属性、方法

作者: Jax_YD | 来源:发表于2021-06-01 13:56 被阅读0次
    image

    本文参考自
    iOS 开发:『Runtime』详解(四)获取类详细属性、方法
    IOS 对象的归档与解档
    本文不做任何商业用途。优秀的作品要大家一起欣赏,如有疑问请联系删除。

    本文主要知识点如下:

    1. 获取类详细属性、方法简述
    2. 获取类详细属性、方法(成员变量列表、属性列表、方法列表、所尊姓的协议列表)
    3. 应用场景
      3.1 修改私有属性
      3.2 万能控制器跳转
      3.3 实现字典转模型
      3.4 改进iOS归档与解档

    1、获取类详细属性、方法简述

    在Apple为我们提供的类中,只能获取一小部分公开的属性和方法。有些我们恰好需要的属性和方法,可能被隐藏起来了,并没有直接提供给我们。

    这个时候,如果我们要用到这些隐藏的属性和方法,就要使用Runtime为我们提供的一系列API来获取Class (类)成员变量 (Ivar)属性 (Peoperty)方法 (Method)协议 (Protocol)等。我们可以通过这些方法来遍历一个类中的成员变量列表、属性列表、方法列表、协议列表。从而查找我们需要的变量和方法。


    2、获取类详细属性、方法

    注意:头文件导入。 #import <objc/runtime.h>


    2.1 获取类的成员变量列表

    ///获取成员变量列表
    - (void)getIvarList {
        unsigned int count;
        
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar myIvar = ivarList[i];
            const char *ivarName = ivar_getName(myIvar);
            NSLog(@"ivar(%d) --- %@", i, [NSString stringWithUTF8String:ivarName]);
        }
        free(ivarList);
    }
    

    2.2 获取类的属性列表

    ///获取属性列表
    - (void)getProprtyList {
        unsigned int count;
        
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            const char *propertyName = property_getName(propertyList[i]);
            NSLog(@"propertyName(%d) --- %@", i, [NSString stringWithUTF8String:propertyName]);
        }
        free(propertyList);
    }
    

    2.3 获取类的方法列表

    ///获取类的方法列表
    - (void)getMethodList {
        unsigned int count;
        
        Method *methodList = class_copyMethodList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Method method = methodList[i];
            NSLog(@"method(%d) --- %@", i, NSStringFromSelector(method_getName(method)));
        }
        free(methodList);
    }
    

    2.4 获取类所遵循的协议列表

    ///获取遵循的协议列表
    - (void)getProtocolList {
        unsigned int count;
        
        __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Protocol *myProtocol = protocolList[i];
            const char *protocolName = protocol_getName(myProtocol);
            NSLog(@"protocol(%d) --- %@", i, [NSString stringWithUTF8String:protocolName]);
        }
        free(protocolList);
    }
    

    3 应用场景

    3.1 修改私有属性

    需求:更改UITextField占位文字的颜色和字号

    总共有一下几种办法:

    方法1:通过attributedPlaceholder属性修改

    我们知道UITextField中有placeholder属性和attributedPlaceholder属性。通过placeholder属性只能更改占位文字,无法修改占位文字的字体和颜色。而通过attributedPlaceholder属性,我们就可以修改UITextField占位文字的颜色和字号了。

    方法2:重写UITestFielddrawPlaceholderInRect:方法

    实现步骤:

    1. 自定义一个xxTextField继承自UITextField
    2. 重写自定义xxTextFielddrawPlaceholderInRect:方法
    3. drawPlaceholderInRect方法中设置placeholder的属性。
    - (void)drawPlaceholderInRect:(CGRect)rect {
        NSDictionary *attributes = @{
                                    NSForegroundColorAttributeName:[UIColor lightGrayColor],
                                    NSFontAttributeName:[UIFont systemFontOfSize:15]
                                    };
        CGSize placeholderSize = [self.placeholder sizeWithAttributes:attributes];
        
        [self.placeholder drawInRect:CGRectMake(0, (rect.size.height - placeholderSize.height)/2, rect.size.width, rect.size.height) withAttributes:attributes];
    }
    

    方法3:利用Runtime,找到并修改UITextfield的私有属性

    实现步骤:

    1. 通过获取类的属性列表成员变量列表的方法打印UITextfield所有属性和成员变量;
    2. 找到私有的成员变量_placeholderLabel;
    3. 利用KVC_placeholderLabel进行修改。
    ///打印 UITextfield 的所有属性和成员变量,找到我们需要修改的那个一个。也就是 `_placeholderLabel`
    - (void)getUITextfieldList {
        unsigned int count;
        
        Ivar *ivarList = class_copyIvarList([UITextField class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar myIvar = ivarList[i];
            const char *ivarName = ivar_getName(myIvar);
            NSLog(@"ivar(%d) --- %@", i, [NSString stringWithUTF8String:ivarName]);
        }
        free(ivarList);
        
        
        objc_property_t *propertyList = class_copyPropertyList([UITextField class], &count);
        for (unsigned int i = 0; i < count; i++) {
            const char *propertyName = property_getName(propertyList[i]);
            NSLog(@"propertyName(%d) --- %@", i, [NSString stringWithUTF8String:propertyName]);
        }
        free(propertyList);
    }
    ///通过修改 UITestField 的私有属性,更改占位颜色和字体
    - (void)createLoginField {
        UITextField *loginTextField = [[UITextField alloc] init];
        loginTextField.frame = CGRectMake(100, 100, 200, 50);
        loginTextField.textColor = [UIColor blackColor];
        
        loginTextField.placeholder = @"用户名/邮箱";
        [loginTextField setValue:[UIFont systemFontOfSize:15] forKeyPath:@"_placeholderLabel.font"];
        [loginTextField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
        
        [self.view addSubView:loginTextField];
    }
    

    3.2 万能控制器跳转

    需求:

    1. 某个页面的不同banner图,点击课可以跳转到不同页面。
    2. 推送通知,点击跳转到指定页面。
    3. 二维码扫描,根据不同内容,跳转不同页面。
    4. WebView 页面,根据URL点击不同,跳转不同的原生页面。

    我们先来列举一下几种解决办法:

    方法1:在每个需要跳转的地方,写一堆判断语句以及跳转语句。

    方法2:将判断语句和跳转语句抽取出来,写到基类,或者对应的Category中。

    方法3:利用Runtime,定制一个万能跳转控制器工具。

    下面我们来探索一下方法3,实现步骤如下:

    1. 事先和服务器商量好,定义跳转不同控制器的规则,让服务器传回对应规则的相关参数。比如:跳转到A控制器,需要服务器传回A控制器的类名,控制器A需要传入的属性参数(id/type等等)。
    2. 根据服务器传回的类名,创建对应的控制器对象。
    3. 遍历服务器传回的参数,利用Runtime遍历控制器对象的属性列表。
    4. 如果控制器对象存在该属性,则利用KVC进行赋值。
    5. 进行跳转。

    首先,定义跳转规则。xxViewController是将要跳转控制器的名称。property字典中保存的是控制器所需的属性参数。

    NSDictionary *params = @{
                            @"class":@"xxViewController",
                            @"property":@{
                                         @"ID":@"123",
                                         @"Data":@"1234567890"  
                                         }
                            };
    

    添加一个工具类xxJumpControllerTool,添加跳转相关的类方法。

    /*************** xxJumpControllerTool.h ***************/
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface xxJumpControllerTool : NSObject
    
    + (void)pushViewControllerWithParams:(NSDictionary *)params;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    /*************** xxJumpControllerTool.m ***************/
    #import "xxJumpControllerTool.h"
    #import <objc/runtime.h>
    #import <UIKit/UIKit.h>
    
    @implementation xxJumpControllerTool
    
    + (void)pushViewControllerWithParams:(NSDictionary *)params {
        ///取出控制器类名
        NSString *classNameStr = [NSString stringWithFormat:@"%@", params[@"class"]];
        const char *className = [classNameStr cStringUsingEncoding:NSASCIIStringEncoding];
        
        ///根据字符串返回一个类
        Class newClass = objc_getClass(className);
        if (!newClass) {
            // 创建一个类
            Class superClass = [NSObject class];
            newClass = objc_allocateClassPair(superClass, className, 0);
            // 注册你创建的这个类
            objc_registerClassPair(newClass);
        }
        
        // 创建对象(就是控制器对象)
        id instace = [[newClass alloc] init];
        
        NSDictionary *propertys = params[@"property"];
        [propertys enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            // 检测这个对象是否存在该属性
            if ([xxJumpControllerTool checkIsExistPropertyWithInstance:instace verifyPropertyName:key]) {
                // 利用 KVC 对控制器对象的属性赋值
                [instace setValue:obj forKey:key];
            }
        }];
        
        // 跳转到对应的控制器
        [[xxJumpControllerTool topViewController] presentViewController:instace animated:YES completion:nil];
    }
    
    // 检查对象是否存在该属性
    + (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName {
        unsigned int count, i;
        
        // 获取对象里的属性列表
        objc_property_t *propertys = class_copyPropertyList([instance class], &count);
        
        for (i = 0; i < count; i++) {
            objc_property_t property = propertys[i];
            // 属性名 转 字符串
            NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            // 判断该属性是否存在
            if ([propertyName isEqualToString:verifyPropertyName]) {
                free(propertys);
                return YES;
            }
        }
        free(propertys);
        
        return NO;
    }
    
    // 获取当前显示在屏幕最顶层的 ViewController
    + (UIViewController *)topViewController {
        UIViewController *resultVC = [xxJumpControllerTool _topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];
        while (resultVC.presentedViewController) {
            resultVC = [xxJumpControllerTool _topViewController:resultVC.presentedViewController];
        }
        return resultVC;
    }
    
    + (UIViewController *)_topViewController:(UIViewController *)vc {
        if ([vc isKindOfClass:[UINavigationController class]]) {
            return [xxJumpControllerTool _topViewController:[(UINavigationController *)vc topViewController]];
        } else if ([vc isKindOfClass:[UITabBarController class]]) {
            return [xxJumpControllerTool _topViewController:[(UITabBarController *)vc selectedViewController]];
        } else {
            return vc;
        }
        return nil;
    }
    
    @end
    
    

    测试代码:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSDictionary *params = @{
                                @"class":@"TwoViewController",
                                @"property":@{
                                             @"ID":@"123",
                                             @"Data":@"1234567890"
                                             }
                                };
        
        [xxJumpControllerTool pushViewControllerWithParams:params];
    }
    

    3.3 实现字典转模型

    在日常开发中,将网络请求中获取的JSON数据转化为数据模型,是我们开发中必不可少的操作。通常我们会选择诸如YYModelJSONModel或者MJExtension这样的第三方框架来实现。这些框架的核心原理就是RuntimeKVC,以及getter/setter

    实现的大致思路:
    利用Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再使用KVC或直接调用getter/setter将每一个对应的value赋值给模型,这就完成了字典转模型的目的。

    需求:将服务器返回的JSON字典转为数据模型

    先准备一份待解析的JSON数据,内容如下:

    {
        "id":"1234567890",
        "name":"Jax_Y",
        "age":"20",
        "address":{
            "country":"中华人民共和国",
        },
        "courses":[
            {
                "name":"Chinese",
                "desc":"语文"
            },
            {
                "name":"Math",
                "desc":"数学"
            },
            {
                "name":"English",
                "desc":"英语"
            }
        ],
    }
    

    3.3.1 创建模型

    我们总共要创建三个模型:xxPersonModelxxAddressModelxxCourseModel

    • xxPersonModel
    /*************** xxPersonModel.h ***************/
    #import <Foundation/Foundation.h>
    #import "NSObject+xxModel.h"
    @class xxCourseModel, xxAddressModel;
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface xxPersonModel : NSObject<xxModelProtocol>
    
    //姓名
    @property (nonatomic, copy) NSString *name;
    //学生ID
    @property (nonatomic, copy) NSString *uid;
    //年龄
    @property (nonatomic, assign) NSInteger age;
    //体重
    @property (nonatomic, assign) NSInteger weight;
    //地址(嵌套类型)
    @property (nonatomic, strong) xxAddressModel *address;
    //课程(嵌套模型数组)
    @property (nonatomic, strong) NSArray *courses;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    /*************** xxPersonModel.m ***************/
    #import "xxPersonModel.h"
    #import "xxCourseModel.h"
    
    @implementation xxPersonModel
    
    //需要特殊处理的属性
    + (NSDictionary<NSString *,id> *)modelContainerPropertyGenericClass {
        return @{
            @"courses":[xxCourseModel class],
            @"uid":@"id"
        };
    }
    
    
    • xxAddressModel
    /*************** xxAddressModel.h ***************/
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface xxAddressModel : NSObject
    
    @property (nonatomic, copy) NSString * _Nullable country;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    /*************** xxAddressModel.m ***************/
    #import "xxAddressModel.h"
    
    @implementation xxAddressModel
    
    @end
    
    • xxCourseModel
    /*************** xxCourseModel.h ***************/
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface xxCourseModel : NSObject<xxModelProtocol>
    
    @property (nonatomic, copy) NSString * _Nullable name;
    @property (nonatomic, copy) NSString * _Nullable desc;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    /*************** xxCourseModel.m ***************/
    #import "xxCourseModel.h"
    
    @implementation xxCourseModel
    
    @end
    

    3.3.2 在NSObject 分类中实现字典转模型

    NSObject+xxModel 使我们用来解决字典转模型所创建的分类,协议中的+ (NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;方法用来告诉分类,特殊字段的处理规则,比如id --> uid。(上面的模型中已经引入了该分类)。

    /*************** NSObject+xxModel.h ***************/
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @protocol xxModelProtocol <NSObject>
    
    @optional
    
    // 协议方法:返回一个字典,表明特殊字段的梳理规则
    + (NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
    
    @end
    
    @interface NSObject (xxModel)
    
    // 字典转模型方法
    + (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary;
    
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    /*************** NSObject+xxModel.m ***************/
    
    #import "NSObject+xxModel.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (xxModel)
    
    + (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary {
        // 创建当前模型对象
        id object = [[self alloc] init];
        
        unsigned int count;
        
        // 获取当前对象的属性列表
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        
        // 遍历 propertyList 中所有属性,以其属性名为 key,在字典中查找 value
        for (unsigned int i = 0; i < count; i++) {
            // 获取属性
            objc_property_t property = propertyList[i];
            const char *propertyName = property_getName(property);
            
            NSString *propertyNameStr = [NSString stringWithUTF8String:propertyName];
            
            // 获取 JSON 中属性值 value
            id value = [dictionary objectForKey:propertyNameStr];
            
            // 获取属性所属类名
            NSString *propertyType;
            unsigned int attrCount;
            objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
            for (unsigned int i = 0; i < attrCount; i++) {
                switch (attrs[i].name[0]) {
                    case 'T': // Type encoding
                    {
                        if (attrs[i].value) {
                            propertyType = [NSString stringWithUTF8String:attrs[i].value];
                            // 去除转移字符:@\“NSString” -> @“NSString”
                            propertyType = [propertyType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
                            // 去掉 @ 符号
                            propertyType = [propertyType stringByReplacingOccurrencesOfString:@"@" withString:@""];
                        }
                    }
                        break;
                        
                    default:
                        break;
                }
            }
            
            // 对特殊属性进行处理
            // 判断当前类是否实现了协议方法,获取协议方法中规定的特殊属性的处理方式
            NSDictionary *propertyTypeDic;
            if ([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
                propertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass)];
            }
            
            // 处理:字典的 key 与模型属性不匹配的问题,如 id -> uid
            id anotherName = propertyTypeDic[propertyNameStr];
            if (anotherName && [anotherName isKindOfClass:[NSString class]]) {
                value = dictionary[anotherName];
            }
            
            // 处理:模型嵌套模型的情况
            if ([value isKindOfClass:[NSDictionary class]] && ![propertyType hasPrefix:@"NS"]) {
                Class modelClass = NSClassFromString(propertyType);
                if (modelClass != nil) {
                    // 将被嵌套字典数据转化为Model
                    value = [modelClass xx_modelWithDictionary:value];
                }
            }
            
            // 处理:模型嵌套模型数组的情况
            // 判断当前 value 是一个数组,而且 存在 协议方法 返回了 propertyTypeDic
            if ([value isKindOfClass:[NSArray class]] && propertyTypeDic) {
                Class itemModelCLass = propertyTypeDic[propertyNameStr];
                // 封装数组:将每一个子数据转化为 Model
                NSMutableArray *itemArray = @[].mutableCopy;
                for (NSDictionary *itemDic in value) {
                    id model = [itemModelCLass xx_modelWithDictionary:itemDic];
                    [itemArray addObject:model];
                }
                value = itemArray;
            }
            
            // 使用 KVC 方法将 value 更新到 object 中
            if (value != nil) {
                [object setValue:value forKey:propertyNameStr];
            }
        }
        free(propertyList);
        
        return object;
    }
    
    @end
    

    测试代码:

    - (void)parseJson {
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"json"];
        NSData *jsonData = [NSData dataWithContentsOfFile:filePath];
    
        // 读取 JSON 数据
        NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
            NSLog(@"%@",json);
        
        // JSON  字典转模型
        xxPersonModel *person = [xxPersonModel xx_modelWithDictionary:json];
        NSLog(@"%@", person);
    }
    
    image

    这里我们只是简单的进行了一些模型转换,正在开发这种三方框架的时候,还要考虑缓存,性能等一系列问题。


    3.4 改进iOS归档和解档

    「归档」和「解档」是iOS中一种序列化和反序列化的方式。对象要实现序列化需要遵循NSCoding协议,而绝大多数FoundationCocoa Touch类都遵循了NSCoding协议。

    3.4.1 在需要归档的类中遵循归档协议:
    @interface Students : NSObject<NSCoding>
    
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, assign) int age;
    
    @end
    
    3.4.2 在归档对象的.m文件中,实现NSCoding的协议方法
    • 普通方法的实现
    - (void)encodeWithCoder:(NSCoder *)coder
    {
        //告诉系统归档的属性有哪些
        [coder encodeObject:self.name forKey:@"name"];
        [coder encodeInt:self.age forKey:@"age"];
    }
    
    - (instancetype)initWithCoder:(NSCoder *)coder
    {
        self = [super init];
        if (self) {
            //解档
            self.name = [coder decodeObjectForKey:@"name"];
            self.age = [coder decodeIntForKey:@"age"];
        }
        return self;
    }
    
    • 利用Runtime方法实现
    - (instancetype)initWithCoder:(NSCoder *)coder
    {
        self = [super init];
        if (self) {
            //解档
            unsigned int count = 0;
            objc_property_t *propertyList = class_copyPropertyList([self class], &count);
            for (int i = 0; i < count; i++) {
                //拿到Ivar
                objc_property_t property = propertyList[i];
                const char *name = property_getName(property);
                NSString *key = [NSString stringWithUTF8String:name];
                //解档
                id value = [coder decodeObjectForKey:key];
                //利用KVC赋值
                [self setValue:value forKey:key];
            }
            free(propertyList);
        }
        return self;
    }
    
    - (void)encodeWithCoder:(nonnull NSCoder *)coder {
        //告诉系统归档的属性有哪些
        unsigned int count = 0; ///表示对象的属性个数
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        for (int i = 0; i < count; i++) {
            //拿到Ivar
            objc_property_t property = propertyList[i];
            const char *name = property_getName(property); //获取到属性的C字符串名称
            NSString *key = [NSString stringWithUTF8String:name]; //转成对应的OC名称
            //归档 -- 利用KVC
            [coder encodeObject:[self valueForKey:key] forKey:key];
        }
        free(propertyList); //在OC中,使用了Copy、Create、New类型的函数,需要释放指针!!!(注:ARC 管不了C函数)
    
    }
    
    3.4.3 测试代码
    - (void)save {
        Students *stu = [[Students alloc] init];
        stu.name = @"Jax_Y";
        stu.age = 20;
        
        //这里以Home路径为例,存到Home路径下
        NSString *str = NSHomeDirectory();
        NSString *filePath = [str stringByAppendingPathComponent:@"student.plist"]; //注:保存文件的扩展名可以任意取,不影响。
        NSLog(@"%@", filePath);
        
        //归档
        [NSKeyedArchiver archiveRootObject:stu toFile:filePath];
        
    }
    
    - (void)read {
        //取出归档的文件再解档
        NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"student.plist"];
        
        NSLog(@"%@", filePath);
        //解档
        Students *stu = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
        NSLog(@"name = %@, age = %d", stu.name, stu.age);
    }
    

    结果:


    image

    相对于普通的「归档」和「解档」方法,利用Runtime技术,不管类有多少属性,都可以通过for循环来搞定,而且不用去判断属性的类型。

    注意:测试的时候使用模拟器测试,真机调试阶段没有获取用户权限的情况下,无法读取用户文件夹下的内容。

    相关文章

      网友评论

          本文标题:iOS底层套索 --- Runtime(四)获取类详细属性、方法

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