iOS学习之Runtime(一)
一、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(一)的更多相关文章
- iOS学习之Runtime(二)
前面已经介绍了Runtime系统的概念.作用.部分技术点和应用场景,这篇将会继续学习Runtime的其他知识. 一.Runtime技术点之类/对象的关联对象 关联对象不是为类/对象添加属性或者成员变量 ...
- iOS学习路线图
一.iOS学习路线图 二.iOS学习路线图--视频篇 阶 段 学完后目标 知识点 配套学习资源(笔记+源码+PPT) 密码 基础阶段 学习周期:24天 学习后目标: ...
- 【IOS学习基础】NSObject.h学习
一.<NSObject>协议和代理模式 1.在NSObject.h头文件中,我们可以看到 // NSObject类是默认遵守<NSObject>协议的 @interface N ...
- ios 学习路线总结
学习方法 面对有难度的功能,不要忙着拒绝,而是挑战一下,学习更多知识. 尽量独立解决问题,而不是在遇到问题的第一想法是找人. 多学习别人开源的第三方库,能够开源的库一定有值得学习的地方,多去看别的大神 ...
- iOS学习系列 - 扩展机制category与associative
iOS学习系列 - 扩展机制category与associative category与associative作为objective-c的扩展机制的两个特性,category即类型,可以通过它来扩展方 ...
- iOS学习-压缩图片(改变图片的宽高)
压缩图片,图片的大小与我们期望的宽高不一致时,我们可以将其处理为我们想要的宽高. 传入想要修改的图片,以及新的尺寸 -(UIImage*)imageWithImage:(UIImage*)image ...
- 【原】iOS学习之事件处理的原理
在iOS学习23之事件处理中,小编详细的介绍了事件处理,在这里小编叙述一下它的相关原理 1.UITouch对象 在触摸事件的处理方法中都会有一个存放着UITouch对象的集合,这个参数有什么用呢? ( ...
- iOS学习笔记——AutoLayout的约束
iOS学习笔记——AutoLayout约束 之前在开发iOS app时一直以为苹果的布局是绝对布局,在IB中拖拉控件运行或者直接使用代码去调整控件都会发上一些不尽人意的结果,后来发现iOS在引入了Au ...
- 【原】iOS学习47之第三方-FMDB
将 CocoaPods 安装后,按照 CocoaPods 的使用说明就可以将 FMDB 第三方集成到工程中,具体请看博客iOS学习46之第三方CocoaPods的安装和使用(通用方法) 1. FMDB ...
随机推荐
- 超详细LAMP环境搭建
一.准备工作 1.安装编译工具gcc.gcc-c++ 注意解决依赖关系,推荐使用yum安装,若不能联网可使用安装光盘做为yum源—— 1)编辑yum配置文件: # mount /dev/cdrom / ...
- python logging info -> 将服务请求记录输出
在tornado 里面这样用 看看logging.warning() , logging.info() , 我们非常想用 zdaemon , 和 logging 将对系统的所有访问转换到服务器里面,作 ...
- MacOSX高分屏图片打包工具tiffutil的简单使用
You can use the man command tiffutil with the option -cathidpicheck. The command lets you manipulate ...
- C#基础之方法和参数
C#基础之方法和参数 接上一篇<C#基础之类型和成员基础以及常量.字段.属性> 实例方法.静态方法 C#中的方法分为两类,一种是属于对象(类型的实例)的,称之为实例方法,另一种是属于类型的 ...
- Oracle体系结构及备份(十七)——bg-others
一 其他进程 Archiver (ARCn) Oneor more archiver processes copy the redo log files to archival storage whe ...
- java实现商品实时录入
//代表各的主页面 package com.gui; import java.awt.*; import javax.swing.*; import java.awt.event.*; import ...
- 使用DBUnit实现对数据库的测试
这是一个JavaProject,有关DBUnit用法详见本文测试用例 首先是用到的实体类User.java package com.jadyer.model; public class User { ...
- dapper 可空bool转换出错及解决方案
最近使用entityframewok生成数据库,使用dapper来访问数据库,产生了一个意外的bug,下面是产生bug的示例以及解决方案. 由于用entityframework生成数据库,默认情况en ...
- 使用vim配置方案spf13中碰到的一些问题
目的:达到我自己自定义安装插件的目的 安装YCM(YouCompleteMe)自动补全神器之前的准备 先安装编译环境: 1 2 sudo apt-get install build-essential ...
- 【百科】CLEO 逐推縮寫命名法
一. 適用場合 1. C# Windows Forms 等窗體開發技術的控件名稱縮寫: 2. 強行縮寫駝峰命名法(Camel-Case).帕斯卡命名法的英文的時候: 二. 命名規則 1. 首字母大寫: ...