Home Objective-C Runtime
Post
Cancel

Objective-C Runtime

我的观点是,人与人的讨论是需要有某些双方一致认同的基础的,否则,最后也难以得出一个一致认同的结果,自说自话毫无意义。所以我觉得需要把很多概念弄清楚,不要混用。平时在网络上和工作中,与各个工种的开发人员进行交流的时候,我发现过一个问题,很多人对面对对象的一些基础概念含糊不清,甚至一无所知。具体到iOS开发领域,常见的开发人员也只是在盲目的堆砌业务功能,而不去考虑这些代码在基础层面的含义。当然基础具体到哪一层次,我只能尽力。


Runtime 前置知识

  • Runtime的版本有很多
  • 早期的runtime版本不能原生地支持对某个类新增变量
  • Mac OS X 10.6 之后的Runtime被称为modern runtime
  • modern runtime实现了原生支持动态增加属性,使用到了称之为Associated References的技术,本文讲述的是 modern runtime的相关知识

Objective-C 是一门动态语言

动态语言是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。C、C++等语言则不属于动态语言。Objective-C 的动态特性是由其Runtime环境来实现的

动态性的表现

C++里类别与方法的关系严格清楚,一个方法必定属于一个类别,而且在编译时(compile time)就已经紧密绑定,不可能调用一个不存在类别里的方法。

在Objective-C,类别与消息的关系比较松散,调用方法视为对对象发送消息,所有方法都被视为对消息的回应。

所有消息处理直到运行时(runtime)才会动态决定,并交由类别自行决定如何处理收到的消息。

也就是说,一个类别不保证一定会回应收到的消息,如果类别收到了一个无法处理的消息,程序只会抛出异常,不会出错或崩溃。

前面的两篇文章讲到了Objective-C类的结构,现在将对结构展开讲述其内部,会首先讲到平时开发中常用的实例、类的常用方法,然后再深入到runtime层面去理解这些方法。

所以,接下来会讲 Objective-C基本代码的常见表达,然后再去深入到runtime层面去理解你平时常写的这些代码。

当然常用的代码的讨论需要建立一些基础的概念,后面会一一提到。

实例变量和属性的关系

实例变量instance variables,也就是常见的ivars,是对类创建的时候用以描述其实例的组成中的数据的封装。是面对对象的基础概念之一。

  • 每一个类某一个实例变量是唯一的,不能重复

  • 实例变量只有类和其子类的实例可以访问

  • 属性和实力变量并没有唯一确定的关系,属性算是一种访问类的内部信息的一个接口 属性可以关联到实例变量,用来访问类内部的实例变量

  • 默认情况下,编译器会为你定义的每一个属性自动生成一个实例变量和配套的setter、getter方法。

实例变量的细节

前面的两篇文章已经讲述过,Objective-C的对象,类,类对象,元类都是由结构体实现的

拿C语言进行举例,前面提到过C语言结构体成员的两种访问方式,其实还有另外一种方式,就是得到结构体变量在内存中的首地址,根据每个成员的长度根据偏移量来访问其任何一个成员变量。

事实上这种方式对C语言系的编程语言都是通用的。

但是Objective-C 融和 runtime 使用一些巧妙的办法,使得程序再运行期间去确定偏移量,这就是动态性的表现之一。

再看一看 OC2.0以前 的实例变量的写法

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>

@interface MTTStudent : NSObject {
    @public
    NSString *_name;
    NSUInteger _age;
    CGFloat _height;
}

@end

采用这种方式使得MTTStudent的某个实例的内存布局在编译时期就已经固定了,碰到要访问_name变量的代码,编译器就会把访问代码转换会偏移量,表示该变量距离MTTStudent的实例的存储地址有多远,通过操作指针中地址来访问这块存储了_name的内存。

这个偏移量是一个硬编码,编译时候用某种长度的整数记录下来。

可是,如果你要插入了一个新的实例变量

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

@interface MTTStudent : NSObject {
    @public
    NSDate *_birthDay;//新增一个实例变量
    NSString *_name;
    NSUInteger _age;
    CGFloat _height;

}

@end

新增并插入的的实例变量_birthDay,如以上代码所示,这个如果继续是访问_name的时候使用原来的偏移量将得到错误的地址,重新编译才能获得新的内存布局,使用心得偏移量

Objective-C的做法是,把实例变量当作 一种存储偏移量的“特殊变量”,由类对象来保存,可以调用runtime的借口获取到具体的偏移量。简单来讲就是在运行时分配内存确定偏移量,并且这个偏移量和实例变量绑定起来,后面讲runtime的接口会看到。

偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也会变,能保证无论何时访问实例变量,都能使用正确的偏移量。

属性的细节

你需要时刻记住面对对象的特性之一:封装,实例变量是对数据的封装,属性是对实例变量的封装,每一层的封装都隐藏了一些融合的细节在其下一层。

属性最终是采用实例变量来实现的,可以把属性理解为一种简称:编译器会自动写出一套存取方法用以访问类中的实例变量。

  • 在类的实现代码中使用@synthesize关键字来指定实例变量的名字

  • 在类的实现代码中使用@dynamic关键字来告诉编译器不要自动生成setter和getter方法,有时候你想使用懒加载 改写属性的setter和getter方法是IDE会提示你有错误,只能重写两者之一,可以同过@dynamic关键字来解决这一问题

属性的特质

所谓属性的特质,是指对属性设置某些修饰,这样编译器在自动实例变量和其存取方法时会考虑这些修饰来生成

属性的特质分为四类:原子性、读写控制、内存管理、方法名

以下的例子是一个具备四类修饰的属性的声明

1
@property (nonatomic,readwrite,strong,setter=setName:)NSString * name;
  • 原子性,缺省为atomic,iOS平台上的代码一般都要指定nonatomic来提升性能

  • 读写权限,缺省为readwrite,编译器会自动生成读写方法,指定readonly时要配合@synthesize编译器才会生成getter方法。
  • 内存管理语义,有五种 assignstrongweakunsafe_unretainedcopy
  • 方法名字,getter=<name>,setter=<name>

关于内存管理语义的细节后面会专门开文章写一写,这些修饰关系到指针的操作,使用不当会导致很多莫名其妙的bug

参考:

  • 《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》
  • 维基百科
This post is licensed under CC BY 4.0 by the author.

How To Understand ObjC Class ?

Post with Header Image