Objective-C 的 RunTime(四):获取类的详细信息

目录

相关函数介绍

  • ivar 相关函数

    /*
    获取成员变量的名称
    
    @param v 要检视的成员变量
    @return 一个包含成员变量名称的 C 字符串
    */
    const char * _Nullable ivar_getName(Ivar _Nonnull v);
    
    /*
    获取成员变量的偏移量(相对于实例对象结构体的首地址)
    
    @param v 要检视的成员变量
    @return 成员变量的偏移量(相对于实例对象结构体的首地址)
    @note 如果要访问类型为(id 或其他对象类型的)成员变量,请调用 object_getIvar() 和 object_setIvar(),而不是使用 offset 来直接访问成员变量的数据
    */
    ptrdiff_t ivar_getOffset(Ivar _Nonnull v);
    
    /*
    获取成员变量的类型编码
    
    @param v 要检视的成员变量
    @return 一个包含成员变量类型编码的 C 字符串
    @note 关于类型编码更详细的介绍,请参考苹果开发者文档(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html)
    */
    const char * _Nullable ivar_getTypeEncoding(Ivar _Nonnull v);
    
    /*
    获取实例对象中成员变量的值
    
    @param obj 要检视的实例对象
    @param ivar 描述要读取其值的成员变量的 Ivar 结构体
    @return 由参数 ivar 指定的成员变量的值。如果参数 obj 为 nil,则返回 nil
    @note 如果描述成员变量的 Ivar 结构体已知的话,object_getIvar() 会比 object_getInstanceVariable() 快
    */
    id _Nullable object_getIvar(id _Nullable obj, Ivar _Nonnull ivar);
    
    /*
    设置实例对象中成员变量的值
    
    @param obj 要修改的实例对象
    @param ivar 描述要设置其值的成员变量的 Ivar 结构体
    @param value 成员变量的新值
    @note 具有已知内存管理策略的成员变量(如:ARC 的 strong 和 weak),使用该内存管理策略
      	  具有未知内存管理策略的成员变量,默认使用 unsafe_unretained
    @note 如果描述成员变量的 Ivar 结构体已知的话,object_setIvar() 会比 object_setInstanceVariable() 快
    */
    void object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value);
    
    /*
    设置实例对象中成员变量的值
    
    @param obj 要修改的实例对象
    @param ivar 描述要设置其值的成员变量的 Ivar 结构体
    @param value 成员变量的新值
    @note 具有已知内存管理策略的成员变量(如:ARC 的 strong 和 weak),使用该内存管理策略
     	  具有未知内存管理策略的成员变量,默认使用 strong
    @note 如果描述成员变量的 Ivar 结构体已知的话,object_setIvar() 会比 object_setInstanceVariable() 快
     */
    void object_setIvarWithStrongDefault(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value);
    
    /*
    获取一个指向(为给定实例对象分配的任何额外字节)的指针
    
    @param obj 要检视的实例对象(Objective-C)
    @return 一个指向(为 obj 分配的额外字节)的指针。如果 obj 没有分配任何额外的字节,则返回的指针将未被定义
    @note 此函数返回一个指向(随实例对象分配的任何额外字节)的指针,如由 class_createInstance() 指定的大于 0 的外部字节。此内存跟随在实例对象的普通 Ivar 之后,但可能不紧邻实例对象的最后一个普通 Ivar 
    @note 返回的指针保证是 pointer-size 的对齐,即使实例对象的最后一个 Ivar 后面的区域小于这个对齐。但是比 pointer-size 大的对齐不能被保证,即使实例对象的最后一个 Ivar 后面的区域大于这个对齐
    @note 在垃圾回收机制的环境中,内存会被保守地输入
    */
    void * _Nullable object_getIndexedIvars(id _Nullable obj);
    
    /*
    向类中添加一个新的成员变量
    
    @param cls 要修改的类(必须是一个动态创建的类,不能是一个已存在的现有类)
    @param name 成员变量的名称
    @param size 成员变量的大小
    @param alignment 成员变量的内存对齐
    @param types 成员变量的类型编码
    @return 如果成功添加成员变量则返回 YES。否则返回 NO(例如:该类已经包含了一个具有该名称的成员变量)
    @note 此函数只能在 objc_allocateClassPair() 之后和 objc_registerClassPair() 之前调用
    	  不支持向现有的类中添加成员变量(此函数只能向动态生成的类中添加成员变量)
    @note 该类一定不能是一个元类,不支持将成员变量添加到元类中
    @note 成员变量的最小字节对齐值为 1<<align。成员变量的最小字节对齐取决于成员变量的类型和 CPU 的架构。对于任何指针类型的成员变量,请传递 log2(sizeof(pointer_type))
    */
    BOOL class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size, uint8_t alignment, const char * _Nullable types);
    
    /*
    获取给定的类的成员变量的内存布局
    
    @param cls 要检视的类
    @return 成员变量的内存布局
    */
    const uint8_t * _Nullable class_getIvarLayout(Class _Nullable cls);
    
    /*
    设置给定的类的成员变量的内存布局
    
    @param cls 要修改的类
    @param layout 成员变量的内存布局
    */
    void class_setIvarLayout(Class _Nullable cls, const uint8_t * _Nullable layout);
    
    /*
    获取给定的类的弱成员变量的内存布局
    
    @param cls 要检视的类
    @return 弱成员变量的内存布局
    */
    const uint8_t * _Nullable class_getWeakIvarLayout(Class _Nullable cls);
    
    /*
    设置给定的类的弱成员变量的内存布局
    
    @param cls 要修改的类
    @param layout 弱成员变量的内存布局
    */
    void class_setWeakIvarLayout(Class _Nullable cls, const uint8_t * _Nullable layout);
    
    /*
    获取给定类的指定名称的成员变量
    
    @param cls 要检视的类
    @param name 要获取的成员变量的名称
    @return 一个指向 Ivar 数据结构的指针,其中包含了 cls 类中名字为 name 的成员变量的信息
    */
    Ivar _Nullable class_getInstanceVariable(Class _Nullable cls, const char * _Nonnull name);
     
    /*
    获取类的成员变量列表(获取类的 Ivar 列表)
    
    @param 要检视的类
    @param outCount 在函数返回时,用于标识返回的 Ivar 数组的长度。如果传 NULL,则不返回 Ivar 数组的长度
    @return 返回一个 Ivar 类型的指针数组(数组中的每一个 Ivar 指针元素,都用于描述一个由类声明的成员变量)
    		不包含任何由父类声明的成员变量
    		返回的数组包含 *outCount 个指针,后面跟着一个 NULL 终止符
    		必须手动调用 free() 函数释放返回的数组
    		如果该类未声明任何成员变量,或者参数 cls 为 Nil,则该函数返回 NULL 并且 *outCount == 0
     */
    Ivar _Nonnull * _Nullable class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount);
    
  • property 相关函数

    /*
    获取属性的名称
    
    @param property 要检视的属性
    @return 一个包含属性名称的 C 字符串
    */
    const char * _Nonnull property_getName(objc_property_t _Nonnull property);
    
    /*
    获取属性的特性字符串
    
    @param property 要检视的属性
    @return 一个包含属性特性的 C 字符串
    @note 特性字符串的格式在 Objective-C Runtime 编程指南的属性声明中有介绍
    */
    const char * _Nullable property_getAttributes(objc_property_t _Nonnull property);
    
    /*
    获取属性的特性数组(数组中的元素为代表属性某一方面特性的键值对 name-value)
    
    @param property 要检视的属性
    @param outCount 在函数返回时,用于标识返回的 objc_property_attribute_t 数组的长度。如果传 NULL,则不返回 objc_property_attribute_t 数组的长度
    @return 属性的特性数组,必须手动调用 free() 函数释放返回的数组
    */
    objc_property_attribute_t * _Nullable property_copyAttributeList(objc_property_t _Nonnull property, unsigned int * _Nullable outCount);
    
    /*
    获取给定属性给定特性名称的特性值
    
    @param property 要检视的属性
    @param attributeName 表示特性名称的 C 字符串
    @return 如果属性中存在该特性名称所对应的特性值(C 字符串),则返回该特性值。否则返回 nil
    */
    char * _Nullable property_copyAttributeValue(objc_property_t _Nonnull property, const char * _Nonnull attributeName);
    
    /*
    为指定的类添加一个属性
    
    @param cls 要修改的类
    @param name 属性的名称
    @param attributes 属性的特性数组
    @param attributeCount 属性的特性数组的元素个数
    @return 如果属性添加成功,则返回 YES。否则返回 NO(例如:类中已经存在该属性)
    */
    BOOL class_addProperty(Class _Nullable cls, const char * _Nonnull name,
                      const objc_property_attribute_t * _Nullable attributes,
                      unsigned int attributeCount);
    
    /*
    获取指定类中给定名称的属性
    
    @param cls 要检视的类
    @param name 要检视的属性的名称
    @return 用于描述该属性的 objc_property_t 类型的指针。如果类中没有声明该名称的属性,或者参数 cls 传 Nil,则返回 NULL
    */
    objc_property_t _Nullable class_getProperty(Class _Nullable cls, const char * _Nonnull name);
    
    /*
    替换指定类中给定名称的属性(注意:修改的是属性的特性,而不是属性的名称)
    
    @param cls 要修改的类
    @param name 属性的名称
    @param attributes 属性的特性数组
    @param attributeCount 属性的特性数组的元素个数 
    */
    void class_replaceProperty(Class _Nullable cls, const char * _Nonnull name,
                          const objc_property_attribute_t * _Nullable attributes,
                          unsigned int attributeCount);
    
    /*
    获取类的属性列表(获取类的 objc_property_t 列表)
    
    @param cls 要检视的类
    @param outCount 在函数返回时,用于标识返回的 objc_property_t 数组的长度。如果传 NULL,则不返回 objc_property_t 数组的长度
    @return 返回一个 objc_property_t 类型的指针数组(数组中的每一个 objc_property_t 指针元素,都用于描述一个由类声明的属性)
    		不包含任何由父类声明的属性
    		返回的数组包含 *outCount 个指针,后面跟着一个 NULL 终止符
    		必须手动调用 free() 函数释放返回的数组
    		如果该类未声明任何属性,或者参数 cls 为 Nil,则该函数返回 NULL 并且 *outCount == 0
    */
    objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount);
    
  • method 相关函数

    /*
    设置一个方法的实现
    
    @param m 要修改的方法
    @param imp 新的方法实现
    @return 该方法先前的实现
    */
    IMP _Nonnull method_setImplementation(Method _Nonnull m, IMP _Nonnull imp);
    
    /* 
    获取一个方法的实现
    
    @param m 要检视的方法
    @return 方法的实现(一个 IMP 类型的函数指针)
    */
    IMP _Nonnull method_getImplementation(Method _Nonnull m);
    
    /* 
    交换两个方法的实现
    
    @param m1 要与 m2 交换实现的方法
    @param m2 要与 m1 交换实现的方法
    
    本函数是以下内容的原子版本:
    	IMP imp1 = method_getImplementation(m1);
    	IMP imp2 = method_getImplementation(m2);
    	method_setImplementation(m1, imp2);
    	method_setImplementation(m2, imp1);
    */
    void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
    
    /*
    获取一个方法的名称
    
    @param m 要检视的方法
    @return 方法的名称(一个 SEL 类型的指针)
    @note 如果要获取方法名称的 C 字符串,调用 sel_getName(method_getName(method))
    */
    SEL _Nonnull method_getName(Method _Nonnull m);
    
    /*
    获取一个描述方法的参数和返回值类型的字符串编码
    
    @param m 要检视的方法
    @return 方法的类型编码(一个 C 字符串,该字符串可能为空)
    */
    const char * _Nullable method_getTypeEncoding(Method _Nonnull m);
    
    struct objc_method_description * _Nonnull method_getDescription(Method _Nonnull m);
    
    /*
    获取一个描述方法的返回值类型的字符串编码
    
    @param m 要检视的方法
    @return 一个用于描述方法返回值类型的 C 字符串。必须手动调用 free() 函数释放该字符串
    */
    char * _Nonnull method_copyReturnType(Method _Nonnull m);
    
    /*
    通过引用的方式来返回描述方法的返回值类型的字符串编码
    
    @param m 要检视的方法
    @param dst 一个指向方法返回值类型的字符指针
    @param dst_len 参数 dst 中可以存储的最大的字符个数
    @note 方法返回值类型的字符串编码,将被复制到 dst 中。dst 通过 strncpy(dst, parameter_type, dst_len) 函数填充
    */
    void method_getReturnType(Method _Nonnull m, char * _Nonnull dst, size_t dst_len);
        
    /*
    通过引用的方式来返回方法中单个参数类型的字符串编码
    
    @param m 要检视的方法
    @param index 要获取类型编码的参数的索引
    @param dst 一个指向参数类型编码的字符指针
    @param dst_len 参数 dst 中可以存储的最大的字符个数
    @note 参数类型的字符串编码将被复制到 dst 中。dst 通过 strncpy(dst, parameter_type, dst_len) 函数填充
    	  如果方法 m 中不包含具有 index 索引的参数,则 dst 通过 strncpy(dst, "", dst_len) 函数填充
    */
    void method_getArgumentType(Method _Nonnull m, unsigned int index, char * _Nullable dst, size_t dst_len);
    
    /*
    返回一个方法所接受的参数的个数
    
    @param m 要检视的方法
    @return 参数的个数
    */
    unsigned int method_getNumberOfArguments(Method _Nonnull m);
    
    /*
    获取一个用于描述方法中单个参数类型的字符串编码
    
    @param m 要检视的方法
    @param index 要获取类型的参数的索引
    @return 一个用于描述 index 处参数类型的 C 字符串,如果方法在 index 处没有参数则为 NULL
    		必须手动调用 free() 函数释放该字符串
    */
    char * _Nullable method_copyArgumentType(Method _Nonnull m, unsigned int index);
    
    /*
    为给定类添加一个具有给定名称和给定实现的新方法
    
    @param cls 要修改的类
    @param name 方法选择器,用于标识要添加的方法的名称
    @param imp 方法实现,用于实现方法的一个函数。该函数至少有两个参数:self 和 _cmd
    @param types 方法类型,用于描述该方法的参数和返回值的类型的字符串编码
    @return 如果方法添加成功,则返回 YES。否则返回 NO(例如:该类已经包含了一个具有该方法名称的方法实现)
    @note 通过 class_addMethod() 添加的方法将会覆盖父类的方法实现(因为父类的方法实现没有存储在本类的方法列表中)
    	  通过 class_addMethod() 添加的方法不会覆盖本类的方法实现。如果要修改本类已经存在的方法的实现,请调用 method_setImplementation()
    */
    BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);
    
    /*
    替换给定类给定方法名称的方法实现
    
    @param cls 要修改的类
    @param name 方法选择器,用于标识要替换的方法的名称
    @param imp 新的方法实现
    @param types 方法类型,用于描述该方法的参数和返回值的类型的字符串编码
    			 因为用于实现方法的函数至少有两个参数:self 和 _cmd,所以第二个和第三个类型编码肯定是 "@:"(第一个类型编码是函数的返回值类型)
    @return 该方法先前的实现
    @note 此函数有 2 种不同的操作方式:
    	  ① 如果方法选择器 name 标识的方法不存在,则添加方法(就像是调用了 class_addMethod() 函数一样)。此时按照给定的方法类型 types 作为方法的类型编码
    	  ② 如果方法选择器 name 标识的方法已经存在,则将会使用新的方法实现 imp 替换原有的方法实现(就像是调用了 method_setImplementation() 函数一样)。此时由参数 types 指定的方法类型将被忽略
    */
    IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);
    
    /*
    获取给定类给定方法名称的类方法
    
    @param cls 要检视的类
    @param name 方法选择器,用于标识要获取的方法的名称
    @return 一个指向 Method 结构体的指针
     		如果给定的类或其父类不包含方法选择器 name 所指定的类方法,则返回 NULL
    @note 请注意:此函数会搜索父类的方法列表,而 class_copyMethodList() 不会
    */
    Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);
    
    /*
    获取给定类给定方法名称的对象方法
    
    @param cls 要检视的类
    @param name 方法选择器,用于标识要获取的方法的名称
    @return 一个指向 Method 结构体的指针
    		如果给定的类或其父类不包含方法选择器 name 所指定的对象方法,则返回 NULL
    @note 请注意:此函数会搜索父类的方法列表,而 class_copyMethodList() 不会
    */
    Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
    
    /*
    获取给定类给定方法名称所对应的方法实现
    
    @param cls 要检视的类
    @param name 方法选择器,用于标识要获取方法实现的方法的名称
    @return 给定类 cls 给定方法名称 name 所对应的方法实现。如果参数 cls 传 Nil,则返回 NULL
    @note class_getMethodImplementation() 的执行速度会比 method_getImplementation(class_getInstanceMethod(cls, name)) 快
    @note 返回的函数指针可能是运行时内部的函数,而不是实际的方法实现
    	  例如:如果类的实例对象没有响应方法选择器,则返回的函数指针将是运行时的消息转发机制的一部分
    */
    IMP _Nullable class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name);
    
    /*
    获取类的方法列表(获取类的 Method 列表)
    
    @param cls 要检视的类
    @param outCount 在函数返回时,用于标识返回的 Method 数组的长度。如果传 NULL,则不返回 Method 数组的长度
    @return 返回一个 Method 类型的指针数组(数组中的每一个 Method 指针元素,都用于描述一个由类实现的方法)
    		不包含任何由父类实现的方法
    		返回的数组包含 *outCount 个指针,后面跟着一个 NULL 终止符
    		必须手动调用 free() 函数释放返回的数组
    		如果该类未实现任何实例方法,或者参数 cls 为 Nil,则该函数返回 NULL 并且 *outCount == 0
    @note 如果要获取一个类的类方法列表,请使用 class_copyMethodList(object_getClass(cls), &count)
    @note 如果在当前类中要获取的方法可能由父类实现,请使用 class_getInstanceMethod() 或者 class_getClassMethod()
    */
    Method _Nonnull * _Nullable class_copyMethodList(Class _Nullable cls, unsigned int * _Nullable outCount);
    
  • protocol 相关函数

    /*
    获取协议的名称
    
    @param proto 要检视的协议
    @return 一个包含协议名称的 C 字符串
    */
    const char * _Nonnull protocol_getName(Protocol * _Nonnull proto);
    
    /*
    返回一个布尔值,用于标识两个协议是否相等
    
    @param proto 第一个协议
    @param other 第二个协议
    @return 如果第一个协议 proto 和第二个协议 other 相等,则返回 YES。否则返回 NO
    */
    BOOL protocol_isEqual(Protocol * _Nullable proto, Protocol * _Nullable other);
    
    /*
    将一个构建完成的协议 addition 添加到另一个正在构建的协议 proto 中
    
    @param proto 要接收 addition 的协议,该协议必须处于正在构建中
    @param addition 要添加到 proto 中的协议,该协议必须已经构建完成
    */
    void protocol_addProtocol(Protocol * _Nonnull proto, Protocol * _Nonnull addition);
    
    /*
    获取一个协议所遵守的协议的数组
    
    @param proto 要检视的协议
    @param outCount 在函数返回时,用于标识返回的 Protocol 数组的长度。如果传 NULL,则不返回 Protocol 数组的长度
    @return 协议 proto 所遵守的协议的数组
    		返回的数组包含 *outCount 个指针,后面跟着一个 NULL 终止符
    		必须手动调用 free() 函数释放返回的数组
    		如果 proto 没有遵守其他协议,则该函数返回 NULL 并且 *outCount == 0
    */
    Protocol * __unsafe_unretained _Nonnull * _Nullable protocol_copyProtocolList(Protocol * _Nonnull proto, unsigned int * _Nullable outCount);
    
    /*
    返回一个布尔值,用于标识一个协议 proto 是否遵守另一个协议 other
    
    @param proto 一个协议
    @param other 另一个协议
    @return 如果 proto 协议遵守 other 协议,则返回 YES。否则返回 NO
    @note 一个协议可以采用如下语法来遵守其他的协议:
    			@protocol ProtocolName <protocol-list>
    	  在尖括号之间的列出的所有协议,都将被认为是 ProtocolName 协议的一部分
    */
    BOOL protocol_conformsToProtocol(Protocol * _Nullable proto, Protocol * _Nullable other);
    
    /*
    向协议中添加一个属性,该协议必须处于正在构建中
    
    @param proto 要修改的协议
    @param name 属性的名称
    @param attributes 属性的特性数组
    @param attributeCount 属性的特性数组的元素个数
    @param isRequiredProperty 如果设置为 YES,则属性(和它的访问器)是必选的。如果设置为 NO,则属性(和它的访问器)是可选的
    @param isInstanceProperty 如果设置为 YES,则属性(和它的访问器)为对象方法。如果设置为 NO,则该属性不会添加到协议中
    */
    void protocol_addProperty(Protocol * _Nonnull proto, const char * _Nonnull name,
                         const objc_property_attribute_t * _Nullable attributes,
                         unsigned int attributeCount,
                         BOOL isRequiredProperty, BOOL isInstanceProperty);
    
    /*
    获取给定协议的给定属性
    
    @param proto 要检视的协议
    @param name 属性的名称
    @param isRequiredProperty 如果设置为 YES,则搜索协议的必选属性。如果设置为 NO,则搜索协议的可选属性
    @param isInstanceProperty 如果设置为 YES,则搜索协议的对象属性。如果设置为 NO,则搜索协议的类属性
    @return 协议 proto 中,由属性名称 name,是否必选属性 isRequiredProperty,是否对象属性 isInstanceProperty 指定的属性。如果没有符合要求的属性,则返回 NULL
    */
    objc_property_t _Nullable protocol_getProperty(Protocol * _Nonnull proto, const char * _Nonnull name, 
    											   BOOL isRequiredProperty, BOOL isInstanceProperty);
    
    /*
    获取协议中声明的必选属性的列表
    
    @note 该方法等效于 protocol_copyPropertyList2(proto, outCount, YES, YES);
    */
    objc_property_t _Nonnull * _Nullable protocol_copyPropertyList(Protocol * _Nonnull proto, unsigned int * _Nullable outCount);
    
    /*
    获取协议中声明的属性的列表
    
    @param proto 要检视的协议
    @param outCount 在函数返回时,用于标识返回的 Property 数组的长度。如果传 NULL,则不返回 Property 数组的长度
    @param isRequiredProperty 如果设置为 YES,则获取协议的必选属性。如果设置为 NO,则获取协议的可选属性
    @param isInstanceProperty 如果设置为 YES,则获取协议的对象属性。如果设置为 NO,则获取协议的类属性
    @return 返回一个 objc_property_t 类型的指针数组(数组中的每一个 objc_property_t 指针元素,都用于描述一个由协议声明的属性)
    		不包含任何由该协议所遵守的协议所声明的属性
    		返回的数组包含 *outCount 个指针,后面跟着一个 NULL 终止符
    		必须手动调用 free() 函数释放返回的数组
    		如果该协议未声明任何匹配的属性,则该函数返回 NULL 并且 *outCount == 0			
    */
    objc_property_t _Nonnull * _Nullable protocol_copyPropertyList2(Protocol * _Nonnull proto,
                               unsigned int * _Nullable outCount,
                               BOOL isRequiredProperty, BOOL isInstanceProperty);
    
    /*
    向协议中添加一个方法描述,该协议必须处于正在构建中
    
    @param proto 要修改的协议
    @param name 方法选择器,用于标识要添加的方法的名称
    @param types 一个用于标识方法类型的 C 字符串
    @param isRequiredMethod 如果设置为 YES,则该方法是必选的。如果设置为 NO,则该方法是可选的
    @param isInstanceMethod 如果设置为 YES,则该方法是对象方法。如果设置为 NO,则该方法是类方法
    */
    void protocol_addMethodDescription(Protocol * _Nonnull proto, SEL _Nonnull name,
                                  const char * _Nullable types,
                                  BOOL isRequiredMethod, BOOL isInstanceMethod);
    
    /*
    获取给定协议中给定方法的方法描述(struct objc_method_description)
    
    @param proto 要检视的协议
    @param aSel 方法选择器,用于标识要获取描述的方法的名称
    @param isRequiredMethod 用于标识由 aSel 指定的方法是必选的还是可选的。如果设置为 YES,则该方法是必选的。如果设置为 NO,则该方法是可选的
    @param isInstanceMethod 用于标识由 aSel 指定的方法是对象方法还是类方法。如果设置为 YES,则该方法是对象方法。如果设置为 NO,则该方法是类方法
    @return 协议 proto 中,由方法名称 aSel,是否必选方法 isRequiredMethod,是否对象方法 isInstanceMethod 指定的方法的方法描述
    		如果没有符合要求的方法,则返回的方法描述结构体为 {NULL, NULL}
    @note 此函数会递归地搜索此协议所遵守的任何协议
    */
    struct objc_method_description protocol_getMethodDescription(Protocol * _Nonnull proto, SEL _Nonnull aSel,
                                  BOOL isRequiredMethod, BOOL isInstanceMethod);
    
    /* 
    获取协议中声明的方法的方法描述列表(一个方法描述包含一个方法的:名称和类型)
    
    @param proto 要检视的协议
    @param isRequiredMethod 用于标识要获取描述的方法是必选的还是可选的。如果设置为 YES,则获取必选方法的描述。如果设置为 NO,则获取可选方法的描述
    @param isInstanceMethod 用于标识要获取描述的方法是对象方法还是类方法。如果设置为 YES,则获取对象方法的描述。如果设置为 NO,则获取类方法的描述
    @param outCount 在函数返回时,用于标识返回的 struct objc_method_description 数组的长度。如果传 NULL,则不返回 struct objc_method_description 数组的长度
    @return 返回一个 struct objc_method_description 类型的指针数组(数组中的每一个 struct objc_method_description 指针元素,都用于描述一个由协议声明的方法)
    		返回的数组包含 *outCount 个指针,后面跟着一个 NULL 终止符
    		必须手动调用 free() 函数释放返回的数组
    		如果该协议未声明任何匹配的方法,则该函数返回 NULL 并且 *outCount == 0
    @note 不包含任何由该协议所遵守的协议所声明的方法的方法描述
    */
    struct objc_method_description * _Nullable protocol_copyMethodDescriptionList(Protocol * _Nonnull proto,
                                       BOOL isRequiredMethod, BOOL isInstanceMethod,
                                       unsigned int * _Nullable outCount);
    
    /*
    向给定的类中添加一个给定的协议
    
    @param cls 要修改的类
    @param protocol 要添加到类中的协议
    @return 如果添加成功,则返回 YES。否则返回 NO(例如:例如该类已经遵守了此协议)
    */
    BOOL class_addProtocol(Class _Nullable cls, Protocol * _Nonnull protocol);
    
    /*
    返回一个布尔值,用于标识给定的类是否遵守给定的协议
    
    @param cls 要检视的类
    @param protocol 要验证的协议
    @return 如果类 cls 遵守 protocol 协议,则返回 YES。否则返回 NO
    @note 通常应该使用 NSObject 的 conformsToProtocol: 方法代替此函数
    */
    BOOL class_conformsToProtocol(Class _Nullable cls, Protocol * _Nullable protocol);
    
    /*
    获取类的协议列表(获取类的 Protocol 列表)
    
    @param cls 要检视的类
    @param outCount 在函数返回时,用于标识返回的 Protocol 数组的长度。如果传 NULL,则不返回 Protocol 数组的长度
    @return 返回一个 Protocol* 类型的指针数组(数组中的每一个 Protocol* 指针元素,都用于描述一个由类遵守的协议)
    		不包含任何由父类遵守的协议,也不包含本类协议所遵守的协议
    		返回的数组包含 *outCount 个指针,后面跟着一个 NULL 终止符
    		必须手动调用 free() 函数释放返回的数组
    		如果该类未遵守任何协议,或者参数 cls 为 Nil,则该函数返回 NULL 并且 *outCount == 0
    */
    Protocol * __unsafe_unretained _Nonnull * _Nullable class_copyProtocolList(Class _Nullable cls, unsigned int * _Nullable outCount);
    

获取类的:成员变量列表 && 属性列表 && 方法列表 && 所遵守的协议列表

  • 获取类的成员变量列表

    #import <objc/runtime.h>
    
    // 打印成员变量列表
    -(void)printIvarList {
        unsigned int count = 0;
        Ivar* ivarList = class_copyIvarList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar anIvar = ivarList[i];
            // const char * -> NString *
            NSString* ivarName = @(ivar_getName(anIvar));
            NSLog(@"ivar(%d) = %@", i, ivarName);
        }
        free(ivarList);
    }
    
  • 获取类的属性列表

    #import <objc/runtime.h>
    
    // 打印属性列表
    -(void)printPropertyList {
        unsigned int count = 0;
        objc_property_t* propertyList = class_copyPropertyList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            objc_property_t aProperty = propertyList[i];
            // const char * -> NString *
            NSString* propertyName = @(property_getName(aProperty));
            NSLog(@"property(%d) = %@", i, propertyName);
        }
        free(propertyList);
    }
    
  • 获取类的方法列表

    #import <objc/runtime.h>
    
    // 打印方法列表
    -(void)printMethodList {
        unsigned int count = 0;
        Method* methodList = class_copyMethodList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Method aMethod = methodList[i];
            // SEL -> NString *
            NSString* methodName = NSStringFromSelector(method_getName(aMethod));
            NSLog(@"method(%d) = %@", i, methodName);
        }
        free(methodList);
    }
    
  • 获取类所遵循的协议列表

    #import <objc/runtime.h>
    
    // 打印协议列表
    -(void)printProtocolList {
        unsigned int count = 0;
        __unsafe_unretained Protocol** protocolList = class_copyProtocolList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Protocol* aProtocol = protocolList[i];
            // const char * -> NString *
            NSString* protocolName = @(protocol_getName(aProtocol));
            NSLog(@"protocol(%d) = %@", i, protocolName);
        }
        free(protocolList);
    }
    

应用场景:修改私有属性

  • 需求

    更改 UITextField 占位文字(placeholder)的颜色和字号

  • 一般的实现方式

    ① 通过 UITextFieldattributedPlaceholder 属性

    -(void)setupPlaceholder {
        NSMutableDictionary* attrsDict = [NSMutableDictionary dictionary];
        attrsDict[NSForegroundColorAttributeName] = [UIColor orangeColor];
        attrsDict[NSFontAttributeName] = [UIFont systemFontOfSize:15];
        NSAttributedString* attrString = [[NSAttributedString alloc] initWithString:@"用户名/邮箱" attributes:attrsDict];
        self.loginTextField.attributedPlaceholder = attrString;
    }
    

    ② 通过重写 UITextField-drawPlaceholderInRect: 方法,步骤如下:

    1. 自定义一个 HcgTextField 继承自 UITextField
    2. 重写 HcgTextField-drawPlaceholderInRect: 方法
    3. -drawPlaceholderInRect: 方法中设置 placeholder 的属性
    // HcgTextField .h
    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface HcgTextField : UITextField
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    // HcgTextField.m
    #import "HcgTextField.h"
    
    @implementation HcgTextField
    
    -(void)drawPlaceholderInRect:(CGRect)rect {
        NSMutableDictionary* attrsDict = [NSMutableDictionary dictionary];
        attrsDict[NSForegroundColorAttributeName] = [UIColor orangeColor];
        attrsDict[NSFontAttributeName] = [UIFont systemFontOfSize:15];
        CGSize placeholderSize = [self.placeholder sizeWithAttributes:attrsDict];
        CGFloat placeholderX = 0;
        CGFloat placeholderY = (rect.size.height - placeholderSize.height) / 2;
        CGFloat placeholderW = rect.size.width;
        CGFloat placeholderH = rect.size.height;
        CGRect placeholderFrame = CGRectMake(placeholderX, placeholderY, placeholderW, placeholderH);
        [self.placeholder drawInRect:placeholderFrame withAttributes:attrsDict];
    }
    
    @end
    
  • 利用 RunTime 的 API:找到并修改 UITextField 的私有属性

    步骤如下:

    1. 通过 RunTime 获取类的属性列表和成员变量列表的函数,打印 UITextField 的所有属性和成员变量
    2. 找到 UITextField 私有的成员变量 _placeholderLabel
    3. 利用 KVC 对 _placeholderLabel 进行修改
    // 打印 UITextField 的成员变量列表和属性列表
    -(void)printUITextFieldIvarListAndPropertyList {
        // 打印 UITextField 的成员变量列表
        unsigned int ivarCount = 0;
        Ivar* ivarList = class_copyIvarList([UITextField class], &ivarCount);
        for (unsigned int i = 0; i < ivarCount; i++) {
            Ivar anIvar = ivarList[i];
            NSString* ivarName = @(ivar_getName(anIvar));
            NSLog(@"ivar(%d) = %@", i, ivarName);
        }
        free(ivarList);
        // 打印 UITextField 的属性列表
        unsigned int propertyCount = 0;
        objc_property_t* propertyList = class_copyPropertyList([UITextField class], &propertyCount);
        for (unsigned int i = 0; i < propertyCount; i++) {
            objc_property_t aProperty = propertyList[i];
            NSString* propertyName = @(property_getName(aProperty));
            NSLog(@"property(%d) = %@", i, propertyName);
        }
        free(propertyList);
    }
    
    // 通过 KVC 设置 UITextField 的私有成员变量
    -(void)setupPrivateIvarForUITextField {
    /*
        // 在 iOS13 及以上的版本,通过此种方式访问 UITextField 的 _placeholderLabel 会导致程序奔溃
        [self.loginTextField setValue:[UIColor orangeColor] forKeyPath:@"_placeholderLabel.textColor"];
        [self.loginTextField setValue:[UIFont systemFontOfSize:15] forKeyPath:@"_placeholderLabel.font"];
        */
        // 解决方式:去掉 _placeholderLabel 的下划线,原因如下所示:
        [self.loginTextField setValue:[UIColor orangeColor] forKeyPath:@"placeholderLabel.textColor"];
        [self.loginTextField setValue:[UIFont systemFontOfSize:15] forKeyPath:@"placeholderLabel.font"];
    }
    
    // iOS13 及以上的版本,系统的 UITextField 重写了 KVC 的 valueForKey: 拦截了外部对 "_placeholderLabel" 的取值,实现如下:
    -(id)valueForKey:(NSString *)key {
        if ([key isEqualToString:@"_placeholderLabel"]) {
            [NSException raise:NSGenericException format:@"Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug"];
        }
        return [super valueForKey:key];
    }
    

应用场景:万能控制器跳转

  • 需求

    ① 点击某个页面的不同 banner 图,可以跳转到不同的页面
    ② 点击推送通知,跳转到指定的页面
    ③ 扫描二维码,根据二维码不同的内容,跳转到不同的页面
    ④ 点击 WebView 页面的不同 URL,跳转到不同的原生页面

  • 一般的实现方式

    ① 在每个需要跳转的地方写一堆的判断语句以及跳转语句
    ② 将判断语句和跳转语句抽取出来,写成工具类

  • 利用 RunTime 的 API:定制一个万能跳转控制器工具

    步骤如下:

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

    首先,定义跳转规则,如下所示:

    // 万能控制器跳转
    -(void)JumpViewControllerDemo {
        // 键(targetVCName)中保存着:将要跳转的控制器的类名
        // 键(params)中保存着:控制器所需要的属性参数
        NSDictionary* jumpInfoDict = @{
            @"targetVCName":@"XXViewController",
            @"params":@{
                    @"age":@"20",
                    @"height":@"170.0",
                    @"sex":@"m",
                    @"name":@"hcg"
            }
        };
        // 调用 万能跳转控制器工具 进行跳转
        [HcgJumpViewControllerTool jumpViewControllerWihtJumpInfoDict:jumpInfoDict];
    }
    

    然后,添加一个工具类 HcgJumpViewControllerTool ,在其中添加跳转相关的方法

    // HcgJumpViewControllerTool.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface HcgJumpViewControllerTool : NSObject
    
    // 根据跳转信息字典,跳转到指定的控制器
    +(void)jumpViewControllerWihtJumpInfoDict:(NSDictionary *)jumpInfoDict;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    // HcgJumpViewControllerTool.m
    #import "HcgJumpViewControllerTool.h"
    #import <UIKit/UIKit.h>
    #import <objc/runtime.h>
    
    @implementation HcgJumpViewControllerTool
    
    // 根据跳转信息字典,跳转到指定的控制器
    +(void)jumpViewControllerWihtJumpInfoDict:(NSDictionary *)jumpInfoDict {
        // 获取控制器类名
        const char * targetVCName = [jumpInfoDict[@"targetVCName"] cStringUsingEncoding:NSASCIIStringEncoding];
        // 获取控制器所属的类
        Class targetVCClass = objc_getClass(targetVCName);
        if (!targetVCClass) {
            Class superClass = [NSObject class];
            targetVCClass = objc_allocateClassPair(superClass, targetVCName, 0);
            objc_registerClassPair(targetVCClass);
        }
        // 创建控制器对象
        id targetVC = [[targetVCClass alloc] init];
        // 遍历参数字典 Params,使用 KVC 对控制器的属性进行赋值
        NSDictionary* paramsDict = jumpInfoDict[@"params"];
        [paramsDict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            if ([self checkIsExistPropertyWithInstance:targetVC name:key]) {
                [targetVC setValue:obj forKey:key];
            }
        }];
        // 跳转到对应的控制器
        // [[self getFrontestViewController].navigationController pushViewController:targetVC animated:YES];
        [[self getFrontestViewController] presentViewController:targetVC animated:YES completion:nil];
    }
    
    // 检测给定的对象是否存在给定名称的属性
    +(BOOL)checkIsExistPropertyWithInstance:(id)instance name:(NSString *)name {
        unsigned int count = 0;
        objc_property_t* propertyList = class_copyPropertyList([instance class], &count);
        for (unsigned int i = 0; i < count; i++) {
            objc_property_t aProperty = propertyList[i];
            // const char * -> NSString *
            NSString* propertyName = @(property_getName(aProperty));
            if ([propertyName isEqualToString:name]) {
            	free(propertyList);
                return YES;
            }
        }
        free(propertyList);
        return NO;
    }
    
    // 获取当前显示在屏幕最前面的 ViewController
    +(UIViewController *)getFrontestViewController {
        UIViewController* rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
        UIViewController* frontestVC = [self getViewControllerNoNivigationNoTabBar:rootVC];
        while (frontestVC.presentedViewController) {
            frontestVC = [self getViewControllerNoNivigationNoTabBar:frontestVC.presentedViewController];
        }
        return frontestVC;
    }
    
    // 过滤容器类型的控制器:NavigationController 和 TabBarController
    +(UIViewController *)getViewControllerNoNivigationNoTabBar:(UIViewController *)vc {
        if ([vc isKindOfClass:[UINavigationController class]]) {
            return [self getViewControllerNoNivigationNoTabBar:[(UINavigationController *)vc topViewController]];
        } else if ([vc isKindOfClass:[UITabBarController class]]) {
            return [self getViewControllerNoNivigationNoTabBar:[(UITabBarController *)vc selectedViewController]];
        } else {
            return vc;
        }
    }
    
    @end
    

应用场景:实现字典转模型

  • 需求

    将服务器端返回的 JSON 字典转换为数据模型

  • 实现方式分析

    我们在日常开发中,经常需要将从网络请求中获取的 JSON 数据转换为数据模型,我们通常会选用诸如 YYModel、JSONModel、MJExtension 等第三方框架来实现这一过程。这些框架实现原理的核心就是 RunTime 和 KVC,以及 Getter / Setter

    实现的大体思路如下:
    借助 RunTime 可以动态获取属性列表的特性,遍历数据模型中的所有属性,然后以获取到的属性名为 key,在 JSON 字典中寻找对应的值 value。再使用 KVC 或直接调用 Getter / Setter 将每一个对应的 value 赋值给数据模型的属性。这样就完成了字典转模型的目的

  • ① 先准备一份待解析的 JSON 数据

    {
        "id": "994923259",
        "name": "行走少年郎",
        "age": "18",
        "height": 180.0,
        "address": {
            "country": "中国",
            "province": "北京"
        },
        "courses": [
            {
                "name": "Chinese",
                "desc": "语文课"
            },
            {
                "name": "Math",
                "desc": "数学课"
            },
            {
                "name": "English",
                "desc": "英语课"
            }
        ]
    }
    

    假设这就是服务器端返回的 JSON 数据,内容是一个学生的信息。现在我们需要将该 JSON 字典转换为方便开发的数据模型

    从这份 JSON 数据中可以看出:字典中的取值除了基本数据类型之外,还有数组和字典。那么在将字典转换成数据模型的时候,就要考虑 模型嵌套模型模型嵌套模型数组 的情况了

    ② 创建数据模型

    经过分析,总共需要三个数据模型:StudentModelAddressModelCourseModel

    // StudentModel.h
    #import <Foundation/Foundation.h>
    #import "NSObject+HcgModel.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @class AddressModel, CourseModel;
    @interface StudentModel : NSObject <HcgModelProtocol>
    
    @property (nonatomic, strong) NSString* uid;                        // 学号
    @property (nonatomic, strong) NSString* name;                       // 姓名
    @property (nonatomic, assign) int age;                              // 年龄
    @property (nonatomic, assign) float height;                         // 身高
    @property (nonatomic, strong) AddressModel* address;             	// 地址(嵌套模型)
    @property (nonatomic, strong) NSArray<CourseModel *>* courses;   	// 课程(嵌套模型数组)
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    
    // StudentModel.m
    #import "StudentModel.h"
    #import "CourseModel.h"
    
    @implementation StudentModel
    
    +(NSDictionary<NSString*, NSString*> *)modelContainGenericProperty {
        return @{ @"uid":@"id" };
    }
    
    +(NSDictionary<NSString*, Class> *)modelContainGenericClass {
        return @{ @"courses":[CourseModel class] };
    }
    
    @end
    
    // AddressModel.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface AddressModel : NSObject
    
    @property (nonatomic, strong) NSString* country;    // 国籍
    @property (nonatomic, strong) NSString* province;   // 省份
    @property (nonatomic, strong) NSString* city;       // 城市
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    
    // AddressModel.m
    #import "AddressModel.h"
    
    @implementation AddressModel
    
    @end
    
    // CourseModel.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface CourseModel : NSObject
    
    @property (nonatomic, strong) NSString* name;   // 课程名称
    @property (nonatomic, strong) NSString* desc;   // 课程介绍
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    
    // CourseModel.m
    #import "CourseModel.h"
    
    @implementation CourseModel
    
    @end
    
  • ③ 利用 RunTime 的 API:实现字典转模型

    这里需要注意以下 2 个细节:

    1. 上面的 StudentModel.h 中导入了 NSObject+HcgModel.h,并遵守了 HcgModelProtocol 协议
    2. 上面的 StudentModel.m 中实现了 HcgModelProtocol 协议

    NSObject+HcgModel.hNSObject+HcgModel.m 就是我们用来解决字典转模型问题所创建的分类
    HcgModelProtocol 协议中的 +modelContainGenericProperty 方法用于处理:模型属性名称与 JSON 字典的 key 不一致的情况
    HcgModelProtocol 协议中的 +modelContainGenericClass 方法用于处理:模型嵌套模型数组的情况

    #import <Foundation/Foundation.h>
    
    
    
    @protocol HcgModelProtocol <NSObject>
    
    @optional
    // 特殊字段处理规则:模型属性名称与 JSON 字典的 key 不一致
    +(nullable NSDictionary<NSString*, NSString*> *)modelContainGenericProperty;
    // 特殊字段处理规则:模型嵌套模型数组
    +(nullable NSDictionary<NSString*, Class> *)modelContainGenericClass;
    
    @end
    
    
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NSObject (HcgModel)
    
    // 字典转模型
    +(instancetype)hcg_modelWithDictionary:(NSDictionary *)dictionary;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "NSObject+HcgModel.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (HcgModel)
    
    +(instancetype)hcg_modelWithDictionary:(NSDictionary *)dictionary {
        // 1.创建当前模型对象
        id model = [[self alloc] init];
        // 2.获取当前模型对象的属性列表
        unsigned int propertyCount = 0;
        objc_property_t* propertyList = class_copyPropertyList([self class], &propertyCount);
        // 3.遍历属性列表中的所有属性,以其属性名称为 key,在 JSON 字典 dictionary 中查找对应的 value
        for (unsigned int i = 0; i < propertyCount; i++) {
            // 3.1 获取属性 + 属性名称
            objc_property_t aProperty = propertyList[i];
            NSString* propertyName = @(property_getName(aProperty));
            // 3.2 获取属性类型(属性所属的类的名称)
            NSString* propertyType = nil;
            unsigned int attrCount = 0;
            objc_property_attribute_t* attrList = property_copyAttributeList(aProperty, &attrCount);
            for (unsigned int j = 0; j < attrCount; j++) {
                switch (attrList[j].name[0]) {
                    // Type Encoding
                    case 'T':
                    {
                        if (attrList[j].value) {
                            propertyType = [NSString stringWithUTF8String:attrList[j].value];
                            // 去除转义字符:@\"NSString\" -> @NSString
                            propertyType = [propertyType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
                            // 去除 @ 符号:@NSString -> NSString
                            propertyType = [propertyType stringByReplacingOccurrencesOfString:@"@"  withString:@""];
                        }
                    }
                    break;
                        
                    default: break;
                }
            }
            // 3.3 获取属性值
            id value = dictionary[propertyName];
            // 3.4 特殊字段处理规则:模型属性名称与 JSON 字典的 key 不一致
            if ([self respondsToSelector:@selector(modelContainGenericProperty)]) {
                NSDictionary* genericPropertyDict = [self performSelector:@selector(modelContainGenericProperty)];
                NSString* anotherName = genericPropertyDict[propertyName];
                if (anotherName) {
                    value = dictionary[anotherName];
                }
            }
            // 3.5 处理模型嵌套模型的情况
            if ([value isKindOfClass:[NSDictionary class]] && ![propertyType hasPrefix:@"NS"]) {
                Class subModelClass = NSClassFromString(propertyType);
                // 将被嵌套的字典也转换为模型
                value = [subModelClass hcg_modelWithDictionary:value];
            }
            // 3.6 特殊字段处理规则:模型嵌套模型数组
            if ([value isKindOfClass:[NSArray class]] &&
                [self respondsToSelector:@selector(modelContainGenericClass)]) {
                NSDictionary* genericClassDict = [self performSelector:@selector(modelContainGenericClass)];
                Class elementModelClass = genericClassDict[propertyName];
                NSMutableArray* elementArray = [NSMutableArray array];
                for (NSDictionary* elementDict in value) {
                    id elementModel = [elementModelClass hcg_modelWithDictionary:elementDict];
                    [elementArray addObject:elementModel];
                }
                value = elementArray;
            }
            // 3.7 使用 KVC 将 value 设置到 model 上
            if (value) {
                [model setValue:value forKey:propertyName];
            }
        }
        // 4.释放属性列表
        free(propertyList);
        // 5.返回模型
        return model;
    }
    
    @end
    
  • ④ 测试代码

    // 将 JSON 字典转换为数据模型
    -(void)convertJsonDictToDataModel {
        // 获取 JSON 字典
        NSString* jsonFilePath = [[NSBundle mainBundle] pathForResource:@"Student" ofType:@"json"];
        NSData* jsonData = [NSData dataWithContentsOfFile:jsonFilePath];
        NSDictionary* jsonDict = [NSJSONSerialization JSONObjectWithData:jsonData
                                                                 options:(NSJSONReadingOptions)NSJSONReadingMutableContainers
                                                                   error:nil];
        NSLog(@"jsonDict = %@", jsonDict);
        // JSON 字典转数据模型
        StudentModel* student = [StudentModel hcg_modelWithDictionary:jsonDict];
        // 打印输出
        NSLog(@"student.uid = %@", student.uid);
        NSLog(@"student.name = %@", student.name);
        for (unsigned int i = 0; i < student.courses.count; i++) {
            CourseModel* courseModel = student.courses[i];
            NSLog(@"courseModel[%d] name = %@, desc = %@", i, courseModel.name, courseModel.desc);
        }
    }
    

    测试效果如下:
    Objective-C 的 RunTime(四):获取类的详细信息

    当然,如果需要考虑缓存机制、性能问题、对象类型检查等,建议还是使用例如 YYModel 之类的知名第三方框架

应用场景:改进归档和解档

  • 实现方式分析

    『归档』和『解档』是一种常用的轻量型文件存取方式。在项目中,如果需要将数据模型本地化存储,一般就会用到『归档』和『解档』。但是如果数据模型中有很多个属性的话,我们不得不对每个属性进行处理,这个过程既繁琐又单调

    改进归档和解档的大体思路如下:
    借助 RunTime 可以动态获取属性列表的特性,遍历数据模型中的所有属性,然后以获取到的属性名为 key,通过 KVC 获取到属性对应的值,再对属性的值进行归档
    借助 RunTime 可以动态获取属性列表的特性,遍历数据模型中的所有属性,然后以获取到的属性名为 key,对属性的值进行解档,再通过 KVC 设置属性对应的值

  • 利用 RunTime 的 API:改进归档和解档

    『归档』和『解档』的测试代码如下:

    -(void)encodeAndDecodeDemo {
        // 存储路径
        NSString* path = [NSString stringWithFormat:@"%@/person.plist", NSHomeDirectory()];
        // 对 Person 对象进行归档
        Person* person0 = [[Person alloc] init];
        person0.uid = @"994923259";
        person0.name = @"hcg";
        person0.sex = 'M';
        person0.age = 18;
        person0.height = 180.0f;
        [NSKeyedArchiver archiveRootObject:person0 toFile:path];
        // 对 Person 对象进行解档
        Person* person1 = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
        NSLog(@"person1.uid = %@", person1.uid);
        NSLog(@"person1.name = %@", person1.name);
    }
    

    需要本地化存储的数据模型 Person 类如下:

    // Person.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Person : NSObject <NSCoding>
    
    @property (nonatomic, strong) NSString* uid;
    @property (nonatomic, strong) NSString* name;
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) char sex;
    @property (nonatomic, assign) float height;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    
    // Person.m
    #import "Person.h"
    #import "NSObject+HcgModel.h"
    
    @implementation Person
    
    #pragma mark - NSCoding
    
    -(instancetype)initWithCoder:(NSCoder *)coder {
        if (self = [super init]) {
            [self hcg_modelInitWithCoder:coder];
        }
        return self;
    }
    
    -(void)encodeWithCoder:(NSCoder *)coder {
        [self hcg_modelEncodeWithCoder:coder];
    }
    
    @end
    

    实现自动归档和解档的代码如下:

    // NSObject+HcgModel.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface NSObject (HcgModel)
    
    // 归档
    -(void)hcg_modelEncodeWithCoder:(NSCoder *)aCoder;
    // 解档
    -(instancetype)hcg_modelInitWithCoder:(NSCoder *)aDecoder;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    
    // NSObject+HcgModel.m
    #import "NSObject+HcgModel.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (HcgModel)
    
    // 归档
    -(void)hcg_modelEncodeWithCoder:(NSCoder *)aCoder {
        // 判空
        if (!aCoder || !self) {
            return;
        }
        // 遍历模型的属性列表,使用 KVC 获取模型的属性对应的值,然后进行归档
        unsigned int propertyCount = 0;
        objc_property_t* propertyList = class_copyPropertyList([self class], &propertyCount);
        for (unsigned int i = 0; i < propertyCount; i++) {
            objc_property_t aProperty = propertyList[i];
            NSString* propertyName = @(property_getName(aProperty));
            id propertyValue = [self valueForKey:propertyName];
            [aCoder encodeObject:propertyValue forKey:propertyName];
        }
        free(propertyList);
    }
    
    // 解档
    -(instancetype)hcg_modelInitWithCoder:(NSCoder *)aDecoder {
        // 判空
        if (!aDecoder || !self) {
            return self;
        }
        // 遍历模型的属性列表进行解档,并使用 KVC 对模型的属性进行赋值
        unsigned int propertyCount = 0;
        objc_property_t* propertyList = class_copyPropertyList([self class], &propertyCount);
        for (unsigned int i = 0; i < propertyCount; i++) {
            objc_property_t aProperty = propertyList[i];
            NSString* propertyName = @(property_getName(aProperty));
            id propertyValue = [aDecoder decodeObjectForKey:propertyName];
            [self setValue:propertyValue forKey:propertyName];
        }
        free(propertyList);
        // 返回模型
        return self;
    }
    
    @end
    
上一篇:ViewModel 源码分析


下一篇:JWAS: 基于Julia开发的一款基于贝叶斯的GWAS和GS软件