一、Runtime简介

因为Objective-C是一门动态语言,所以它总是想办法把一些决定性工作从编译链接推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统(runtime system)来执行编译后的代码。这就是Objective-C Runtime系统存在的意义,它是整个Objective-C运行框架的一块基石。

Runtime其实有两个版本:modern和legacy。我们现在用的Objective-C 2.0采用的是modern版的Runtime系统,只能运行在iOS 2.0和OS X 10.5之后的64位程序中。而OS X较老的32位程序仍采用Objective-C 1中的legacy版的Runtime系统。

关于这两个版本之间的区别,对于接触Objective-C时间不长的我来说,自然是没有研究过。目前如果能把现行的Runtime研究得比较透彻,就算是一个不小的进步。

二、Runtime相关头文件

iOS的SDK中usr/include/objc文件夹下有这样几个文件:

 List.h
NSObjCRuntime.h
NSObject.h
Object.h
Protocol.h
a.txt
hashtable.h
hashtable2.h
message.h
module.map
objc-api.h
objc-auto.h
objc-class.h
objc-exception.h
objc-load.h
objc-runtime.h
objc-sync.h
objc.h
runtime.h

都是和运行时相关的头文件,其中主要使用的函数定义在message.h和runtime.h这两个文件中。在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。runtime.h是运行时最重要的文件,其中包含了对运行时进行操作的方法。

 主要包括:

 1、操作对象的类型的定义

 #if !OBJC_TYPES_DEFINED

 /// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method; /// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar; /// An opaque type that represents a category.
typedef struct objc_category *Category; /// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t; struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif } OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */ #endif

  这些类型的定义,对一个类进行了完全的分解,将类定义或者对象的每一个部分都抽象为一个类型type,这对于操作一个类属性和方法来说都是非常方便的。OBJC2_UNAVAILABLE标记的属性是Objective-C 2.0不支持的。

 2、函数的定义

  对对象进行操作的方法一般以object_开头;
  对类进行操作的方法一般以class_开头;
  对类或对象的方法进行操作的方法一般以method_开头;
  对成员变量进行操作的方法一般以ivar_开头;
  对属性进行操作的方法一般以property_开头开头;
  对协议进行操作的方法一般以protocol_开头;

  根据以上的函数的前缀,可以大致了解到层级关系。对于以object_开头的方法,则是runtime最终的管家,可以获取内存中类的加载信息、类的列表、关联对象和关联属性等操作。

  例如:使用runtime对当前的应用中加载的类进行打印。

 - (void)viewDidLoad
{
[super viewDidLoad]; unsigned int outCount;
Class *classes = objc_copyClassList(&outCount);
for (int i = ; i < outCount; i++)
{
const char *cname = class_getName(classes[i]);
printf("%s\n", cname);
}
}

三、技术点和应用场景

 在开始这部分之前,我们需要先定义一个Person类,方便后面的叙述。Person类只是简单的定义了一个成员变量和两个属性。

 @interface Person : NSObject
{
double _height;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end

 1、获取属性/成员变量列表

  对于获取成员变量列表,可以使用class_copyIvarList函数;对于获取属性列表,可以使用class_copyPropertyList函数。使用示例如下:

 - (void)viewDidLoad
{
[super viewDidLoad]; unsigned int outCount;
Ivar *ivarList = class_copyIvarList([Person class], &outCount);
for (int i = ; i < outCount; i++)
{
Ivar *ivar = &ivarList[i];
NSLog(@"%s---%s", ivar_getName(*ivar), ivar_getTypeEncoding(*ivar));
} NSLog(@"--------------------"); objc_property_t *propertyList = class_copyPropertyList([Person class], &outCount);
for (int i = ; i < outCount; i++)
{
objc_property_t *property = &propertyList[i];
NSLog(@"%s", property_getName(*property));
}
}

  以上代码的输出为:

 -- ::51.038 RunTimeTest[:] _height---d
-- ::51.039 RunTimeTest[:] _name---@"NSString"
-- ::51.039 RunTimeTest[:] --------------------
-- ::51.039 RunTimeTest[:] name
-- ::51.039 RunTimeTest[:] age

  class_copyIvarList函数,官方解释是这样的:return An array of pointers of type Ivar describing the instance variables declared by the class. Any instance variables declared by superclasses are not included. 大致意思就是,这个方法会返回一个包含了所有成员变量的数组,但是所有父类的成员变量都不包含在内,这是需要注意的一点。同时官方解释还有一句话:You must free the array with free(). 我们必须手动释放这个数组,这是需要注意的第二点。

  ivar_getTypeEncoding函数获取到的是成员变量的类型编码。类型编码是苹果对数据类型、对象类型规定的另一个表现形式,比如"@"代表的是对象,":"表示的是SEL指针,"v"表示的是void。具体可以看苹果官方文档对类型编码的具体规定:戳我!!!

  我们都知道,@property会做三份工作:

  (1)生成一个带下划线的成员变量  (2)生成这个成员变量的get方法  (3)生成这个成员变量的set方法。因此会输出三个成员变量_height、_age和_name。

  因此可以说,class_copyIvarList可以获取到所有的成员变量和属性,class_copyPropertyList获取不到成员变量。

  但是如果属性是readonly的并且重写了getter,此时生成的带下划线的成员变量就不在了(这里暂时还不清楚为什么),通过class_copyIvarList获取不到对应的属性,所以无论使用class_copyIvarList还是使用class_copyPropertyList都无法获取全部的成员变量和属性。

  有了上面的结论,下面我们假设一个不合理的需求,以此来论证在执行KVC时是使用copyIvarList好还是使用copyPropertyList好。

  这个不合理的需求就是,已经确定了对象的某个属性是readonly的并且重写了getter,在进行KVC时,想要获取全部的成员变量和属性,该怎么办呢?为什么说它不合理呢?因为这个属性已经是readonly的了,却还是想要获取到它然后执行赋值操作,这是不合常理的。

  在开始之前,首先要了解setValue: forKeyPath:方法的底层实现,以name属性为例:

  (1)首先去类的方法列表中寻找有没有setName,如果有,就直接调用[person setName:value];

  (2)继续寻找有没有带下划线的成员变量_name,如果有,_name = value;

  (3)继续寻找有没有成员变量name,如果有,name = value;

  (4)如果都没有,就直接报错。

  因此对于readonly的并且重写了getter的属性而言,如果使用copyPropertyList执行KVC必然报错,因此为保证代码正常,不能使用copyPropertyList为属性执行KVC。并且copyPropertyList无法获取到成员变量,无法对成员变量进行赋值。而copyIvarList的好处在于,它恰恰获取不到readonly的并且重写了getter的属性,所以很自然的为其他获取的成员变量和属性执行赋值操作。

 - (void)viewDidLoad
{
[super viewDidLoad]; unsigned int outCount;
Ivar *ivarList = class_copyIvarList([Person class], &outCount);
for (int i = ; i < outCount; i++)
{
Ivar *ivar = &ivarList[i];
NSLog(@"%s", ivar_getName(*ivar)); // 这里能够获取到除了readonly并且重写了getter的所有属性和成员变量,所以可以执行KVC。
} NSLog(@"--------------------"); objc_property_t *propertyList = class_copyPropertyList([Person class], &outCount);
for (int i = ; i < outCount; i++)
{
objc_property_t *property = &propertyList[i];
NSLog(@"%s", property_getName(*property)); // 这里能够获取所有的属性,如果属性是readonly并且重写了getter的,这里照样可以获取,
// 此时如果执行KVC,必然报错。
}
}

  可是如果只是想对public的成员变量执行KVC,而不想对private的成员变量执行KVC,那又该怎么办呢?上面已经论证过了,如果有的成员变量是readonly并且重写了getter的话,不能使用copyPropertyList,而我们又不想对private的成员变量执行KVC,那是不是就没有办法了呢?当然不是,此时我们可以通过copyIvarList获取所有的成员变量和属性,然后去掉copyPropertyList没有的成员变量,那么剩下的就是我们想要的成员变量了。

  例如:Person类有一个private成员变量_height和两个public成员变量name、age,其中age是readonly并且重写了getter的了,那么利用copyIvarList可以获取_height和_name,利用copyPropertyList可以获取name和age,然后去掉copyPropertyList没有的成员变量,也即去掉_height,剩下的_name就是我们想要的结果。

 1.1 应用1 KVC字典转模型

  获取属性/成员列表一个重要的应用就是,一次取出模型中的属性/成员变量,根据它的名字获取字典中的key,然后取出字典中这个key对应的value,使用setValue: forKeyPath:方法设置值。

 - (void)viewDidLoad
{
[super viewDidLoad]; NSDictionary *dict = @{@"name":@"zhangsan", @"age":@, @"height":@, @"test1":@"asdfasdf", @"test2":@"asdfsadfsad"}; Person *person = [[Person alloc] init]; unsigned int outCount;
Ivar *ivarList = class_copyIvarList([Person class], &outCount);
for (int i = ; i < outCount; i++)
{
Ivar *ivar = &ivarList[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)];
NSString *key = [name substringFromIndex:]; // 去掉成员变量前面的'_'
[person setValue:dict[key] forKey:key];
} NSLog(@"%@", person); // 已经重写了description
}

  为什么要这样,而不再使用方法setValuesForKeysWithDictionary:,因为在setValuesForKeysWithDictionary:方法内部会执行这样一个过程:遍历字典里面的所有key,取出key的value,即dict[key],使用方法setValue: forKeyPath:进行赋值(这个方法的执行过程在前面已经提及)。这也就解释了当字典中的key比模型中多时,会出现" this class is not key-value compliant for 'xxx' "的bug了。那么当模型中的属性比字典中多时,使用setValuesForKeysWithDictionary:会不会有bug呢?经测试:当多出来的属性是对象数据类型时,为null;当属性是基本数据类型时,会有一个系统默认值(如int为0)。

  使用运行时KVC字典转模型,即使字典中的key比模型中多的时候也不会有bug,但是新的问题出现了,如果模型中的属性比字典中的key多便会出现bug,而且是这样一种情况:如果多的是对象类型,则不会有bug,该属性的值为null;如果多的是基本数据类型,就会出错" could not set nil as the value for the key 'xxx' "。

  那么如何解决上面的bug呢?可以在setValue: forKeyPath:方法调用之前进行如下处理:取出属性对应的类型,如果类型是基本数据类型,value替换为默认值(如int对应默认值为0)。

 - (void)viewDidLoad
{
[super viewDidLoad]; NSDictionary *dict = @{@"name":@"zhangsan", @"age":@, @"test1":@"asdfasdf", @"test2":@"asdfsadfsad"}; Person *person = [[Person alloc] init]; unsigned int outCount;
Ivar *ivarList = class_copyIvarList([Person class], &outCount);
for (int i = ; i < outCount; i++)
{
Ivar *ivar = &ivarList[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)];
NSString *key = [name substringFromIndex:]; // 去掉成员变量前面的'_' id value = dict[key]; NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(*ivar)]; // 获取属性类型
if ([type isEqualToString:@"d"]) // 判断属性类型是否为基本类型
{
value = @0.0;
} // 这样即便dict中没有height这个key,也不会报错了
[person setValue:value forKey:key];
} NSLog(@"%@", person); // 已经重写了description
}

 1.2 应用2 NSCoding归档和解归档

  获取属性/成员列表另外一个重要的应用就是进行归档和解归档,其原理和上面的KVC基本上一样,这里只是展示一些代码:

 - (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init])
{
unsigned int outCount;
Ivar *ivarList = class_copyIvarList(self.class, &outCount);
for (int i = ; i < outCount; i++)
{
Ivar *ivar = &ivarList[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)];
NSString *key = [name substringFromIndex:];
id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key];
}
}
return self;
} - (void)encodeWithCoder:(NSCoder *)aCoder
{
unsigned int count = ;
Ivar *ivarList = class_copyIvarList(self.class, &count);
for (int i = ; i < count; i++)
{
Ivar *ivar = &ivarList[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)];
NSString *key = [name substringFromIndex:]; id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
}

ps:后续再继续学习Runtime的其他技术点和应用场景。

  代码地址:GitHub,每一个知识点都对应一个版本,需要的小伙伴可以下载查看,同时也欢迎评论交流,共同进步。

iOS学习之Runtime(一)的更多相关文章

  1. iOS学习之Runtime(二)

    前面已经介绍了Runtime系统的概念.作用.部分技术点和应用场景,这篇将会继续学习Runtime的其他知识. 一.Runtime技术点之类/对象的关联对象 关联对象不是为类/对象添加属性或者成员变量 ...

  2. iOS学习路线图

    一.iOS学习路线图   二.iOS学习路线图--视频篇       阶 段 学完后目标 知识点 配套学习资源(笔记+源码+PPT) 密码 基础阶段 学习周期:24天       学习后目标:    ...

  3. 【IOS学习基础】NSObject.h学习

    一.<NSObject>协议和代理模式 1.在NSObject.h头文件中,我们可以看到 // NSObject类是默认遵守<NSObject>协议的 @interface N ...

  4. ios 学习路线总结

    学习方法 面对有难度的功能,不要忙着拒绝,而是挑战一下,学习更多知识. 尽量独立解决问题,而不是在遇到问题的第一想法是找人. 多学习别人开源的第三方库,能够开源的库一定有值得学习的地方,多去看别的大神 ...

  5. iOS学习系列 - 扩展机制category与associative

    iOS学习系列 - 扩展机制category与associative category与associative作为objective-c的扩展机制的两个特性,category即类型,可以通过它来扩展方 ...

  6. iOS学习-压缩图片(改变图片的宽高)

    压缩图片,图片的大小与我们期望的宽高不一致时,我们可以将其处理为我们想要的宽高. 传入想要修改的图片,以及新的尺寸 -(UIImage*)imageWithImage:(UIImage*)image ...

  7. 【原】iOS学习之事件处理的原理

    在iOS学习23之事件处理中,小编详细的介绍了事件处理,在这里小编叙述一下它的相关原理 1.UITouch对象 在触摸事件的处理方法中都会有一个存放着UITouch对象的集合,这个参数有什么用呢? ( ...

  8. iOS学习笔记——AutoLayout的约束

    iOS学习笔记——AutoLayout约束 之前在开发iOS app时一直以为苹果的布局是绝对布局,在IB中拖拉控件运行或者直接使用代码去调整控件都会发上一些不尽人意的结果,后来发现iOS在引入了Au ...

  9. 【原】iOS学习47之第三方-FMDB

    将 CocoaPods 安装后,按照 CocoaPods 的使用说明就可以将 FMDB 第三方集成到工程中,具体请看博客iOS学习46之第三方CocoaPods的安装和使用(通用方法) 1. FMDB ...

随机推荐

  1. android 组件内部实现触摸事件,更改背景

    当我们需要实现触摸事件的时候,通常就会想到OnTouchEvent这个方法,它能够获取触摸点的坐标,而组件同样也有这样的一个功能,使用这个功能,可以实现组件在父布局界面上移动,缩放,更改背景图片.这里 ...

  2. java调用存储过程超时及DBCP参数配置说明

    问题:            生产环境实时打标超时: 分析原因:        “实时打标java服务中,只创建数据库Connection,没有关闭数据库Connection,导致数据库连接池耗尽,无 ...

  3. 用android代码显示图片的一部分源码

    ShowPoritionPictureActivity代码: [java] <span style="font-size:16px;"> package com.iwi ...

  4. 30+WordPress古典风格的主题-古典却不失时尚

    通过WordPress古典风格主题,你可以快速的让你的网站呈现古典情调,并在竞争中脱颖而出.古典风格的网页设计是非常受欢迎的,现在,你可能想知道为什么.很多人被古典风格吸引,他们将一些陈旧的,过时的, ...

  5. 用docker cluster store实现kuryr的共享subnet

    kuryr共享网络 在上一篇分享中,提到共享网络需要在10.8.65.80和10.8.65.81上分别通过docker network create来创建网络.而实际上,通过对docker源码的解读, ...

  6. 看完这篇文章才对【GIT】有了大彻大悟的认识

    git定义:版本管理工具 历史:没有git的时候,我们修改完一个文件,要先把文件拷贝一个副本到备份文件夹中,还要建个文档来记录当前文件的信息和操作记录.当文件多的时候,人工操作起来将是个非常辛苦非常庞 ...

  7. 结构-行为-样式-css&html横纵居中最佳实践

    最近在做手机端的H5项目,有个标题是在一根横线中的,就是水平居中加垂直居中(如图一).这应该是前端开发中经常遇到的一个场景了,做的次数多了就有一些体会,我今天就总结了下这种结构的实现思路:首先,用元素 ...

  8. 不惧面试:HTTP协议(1) - 基础扫盲

    v博客前言 先交代下背景,之前在一家公司面试.NET开发,去了先是做一份笔试题,有一半是关于HTTP协议的,技术总监直接面试,也是问一大堆HTTP协议的知识点,可能跟该公司的产品有关,该公司属于互联网 ...

  9. C#Redis字符串

    上周六通宵打牌周日白天只睡3小时累成狗,从今天起以后不能玩太大的了,小赌怡情大赌伤身,和同事朋友有空玩玩还是好的.今天公司外面马路上有人挂灯笼时死了一个人,哎,快过年了悲剧又发生了,真是生命是脆弱的. ...

  10. Mybatis第二天(其他)

    Mybatis第二天 框架课程 课程计划 动态sql If标签 Where标签 Sql片段 Foreach标签 关联查询 一对一关联 一对多关联 Mybatis整合spring 如何整合spring ...