编译时与运行时
编译时: 即编译器对语言的编译阶段,编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等,将程序代码翻译成计算机能够识别的语言(例如汇编等),编译通过并不意味着程序就可以成功运行。
运行时: 即程序通过了编译这一关之后编译好的代码被装载到内存中跑起来的阶段,这个时候会具体对类型进行检查,而不仅仅是对代码的简单扫描分析,此时若出错程序会崩溃。
可以说编译时是一个静态的阶段,类型错误很明显可以直接检查出来,可读性也好;而运行时则是动态的阶段,开始具体与运行环境结合起来。
OC语言的动态性
含义
OC语言的动态性主要体现在三个方面:动态类型(Dynamic typing)、动态绑定(Dynamic binding)和动态加载(Dynamic loading)。
动态类型
动态类型指的是对象指针类型的动态性,具体是指使用id任意类型将对象的类型确定推迟到运行时,由赋给它的对象类型决定对象指针的类型。另外类型确定推迟到运行时之后,可以通过NSObject的isKindOfClass方法动态判断对象最后的类型(动态类型识别)。也就是说id修饰的对象为动态类型对象,其他在编译器指明类型的为静态类型对象,通常如果不需要涉及到多态的话还是要尽量使用静态类型(原因上面已经说到:错误可以在编译器提前查出,可读性好)。
示例:
对于语句NSString* testObject = [[NSData alloc] init]; testObject在编译时和运行时分别是什么类型的对象?
首先testObject是一个指向某个对象的指针,不论何时指针的空间大小是固定的。
编译时: 指针的类型为NSString,即编译时会被当成一个NSString实例来处理,编译器在类型检查的时候如果发现类型不匹配则会给出黄色警告,该语句给指针赋值用的是一个NSData对象,则编译时编译器则会给出类型不匹配警告。但编译时如果testObject调用NSString的方法编译器会认为是正确的,既不会警告也不会报错。
运行时: 运行时指针指向的实际是一个NSData对象,因此如果指针调用了NSString的方法,虽然编译时通过了,但运行时会崩溃,因为NSData对象没有该方法;另外,虽然运行时指针实际指向的是NSData,但编译时编译器并不知道(前面说了编译器会把指针当成NSString对象处理),因此如果试图用这个指针调用NSData的方法会直接编译不通过,给出红色报错,程序也运行不起来。
下面给出测试例子:
// 1.编译时编译器认为testObject是一个NSString对象,这里赋给它一个NSData对象编译器给出黄色类型错误警告,但运行时却是指向一个NSData对象
NSString* testObject = [[NSData alloc] init];
// 2.编译器认为testObject是NSString对象,所以允许其调用NSString的方法,这里编译通过无警告和错误
[testObject stringByAppendingString:@"string"];
// 3.但不允许其调用NSData的方法,下面这里编译不通过给出红色报错
[testObject base64EncodedDataWithOptions:NSDataBase64Encoding64CharacterLineLength];
将上面第三句编译不通过的注释掉,然后在第二句打断点,编译后让程序跑起来到断点出会看到testObject指针的类型是_NSZeroData,指向一个NSData对象。继续运行程序会崩溃,因为NSData对象没有NSString的stringByAppendingString这个方法。
那么,假设testObject是id类型会怎样呢?
// 1.id任意类型,编译器就不会把testObject在当成NSString对象了
id testObject = [[NSData alloc] init];
// 2.调用NSData的方法编译通过
[testObject base64EncodedDataWithOptions:NSDataBase64Encoding64CharacterLineLength];
// 3.调用NSString的方法编译也通过
[testObject stringByAppendingString:@"string"];
结果是编译完全通过,编译时编译器把testObject指针当成任意类型,运行时才确定testObject为NSData对象(断点看指针的类型和上面的例子中结果一样还是_NSZeroData,指向一个NSData对象),因此执行NSData的函数正常,但执行NSString的方法时还是崩溃了。通过这个例子也可以很清楚的知道id类型的作用了,将类型的确定延迟到了运行时,体现了OC语言的一种动态性:动态类型。
动态类型识别方法(面向对象语言的内省Introspection特性)
1.首先是Class类型:
- Class class = [NSObject class]; // 通过类名得到对应的Class动态类型
- Class class = [obj class]; // 通过实例对象得到对应的Class动态类型
- if([obj1 class] == [obj2 class]) // 判断是不是相同类型的实例
2.Class动态类型和类名字符串的相互转换:
- NSClassFromString(@”NSObject”); // 由类名字符串得到Class动态类型
- NSStringFromClass([NSObject class]); // 由类名的动态类型得到类名字符串
- NSStringFromClass([obj class]); // 由对象的动态类型得到类名字符串
3.判断对象是否属于某种动态类型:
- -(BOOL)isKindOfClass:class // 判断某个对象是否是动态类型class的实例或其子类的实例
- -(BOOL)isMemberOfClass:class // 与isKindOfClass不同的是,这里只判断某个对象是否是class类型的实例,不放宽到其子类
4.判断类中是否有对应的方法:
- -(BOOL)respondsTosSelector:(SEL)selector // 类中是否有这个类方法
- -(BOOL)instancesRespondToSelector:(SEL)selector // 类中是否有这个实例方法
区别:
上面两个方法都可以通过类名调用,前者判断类中是否有对应的类方法(通过‘+’修饰定义的方法),后者判断类中是否有对应的实例方法(通过‘-’修饰定义的方法)。此外,前者respondsTosSelector函数还可以被类的实例对象调用,效果等同于直接用类名调用后者instancesRespondToSelector函数。
举个例子:假设有一个类Test,有它的一个实例对象test,Test类中定义了一个类函数:+ (void)classFun;和一个实例函数:- (void)objFunc;,那么各种调用情况的结果如下:
[1][Test instancesRespondToSelector:@selector(objFunc)];//YES
[2][Test instancesRespondToSelector:@selector(classFunc)];//NO
[3][Test respondsToSelector:@selector(objFunc)];//NO
[4][Test respondsToSelector:@selector(classFunc)];//YES
[5][test respondsToSelector:@selector(objFunc)];//YES
[6][test respondsToSelector:@selector(classFunc)];//NO
结论: 如果想判断一个类中是否有某个类方法,应该使用[4]; 如果想判断一个类中是否有某个实例方法,可以使用[1]或者[5]。
5.方法名字符串和SEL类型的转换
在编译器,编译器会根据方法的名字和参数序列生成唯一标识改方法的ID,这个ID为SEL类型。到了运行时编译器通过SEL类型的ID来查找对应的方法,方法的名字和参数序列相同,那么它们的ID就都是相同的。另外,可以通过@select()指示符获得方法的ID。常用的方法如下:
SEL funcID = @select(func);// 这个注册事件回调时常用,将方法转成SEL类型
SEL funcID = NSSelectorFromString(@"func"); // 根据方法名得到方法标识
NSString *funcName = NSStringFromSelector(funcID); // 根据SEL类型得到方法名字符串
动态绑定
动态绑定指的是方法确定的动态性,具体指的是利用OC的消息传递机制将要执行的方法的确定推迟到运行时,可以动态添加方法。也就是说,一个OC对象是否调用某个方法不是由编译器决定的,而是由运行时决定的;另外关于动态绑定的关键一点是基于消息传递机制的消息转发机制,主要处理应对一些接受者无法处理的消息,此时有机会将消息转发给其他接收者处理,具体见下面介绍。
动态绑定是基于动态类型的,在运行时对象的类型确定后,那么对象的属性和方法也就确定了(包括类中原来的属性和方法以及运行时动态新加入的属性和方法),这也就是所谓的动态绑定了。动态绑定的核心就该是在运行时动态的为类添加属性和方法,以及方法的最后处理或转发,主要用到C语言语法,因为涉及到运行时,因此要引入运行时头文件:#include <objc/runtime.h>
。
消息传递机制:
在OC中,方法的调用不再理解为对象调用其方法,而是要理解成对象接收消息,消息的发送采用‘动态绑定’机制,具体会调用哪个方法直到运行时才能确定,确定后才会去执行绑定的代码。方法的调用实际就是告诉对象要干什么,给对象(的指针)传送一个消息,对象为接收者(receiver),调用的方法及其参数即消息(message),给一个对象传消息表达为:[receiver message]; 接受者的类型可以通过动态类型识别于运行时确定。
在消息传递机制中,当开发者编写[receiver message];语句发送消息后,编译器都会将其转换成对应的一条objc_msgSend C语言消息发送原语,具体格式为:
void objc_msgSend (id self, SEL cmd, ...)
这个原语函数参数可变,第一个参数填入消息的接受者,第二个参数是消息‘选择子’,后面跟着可选的消息的参数。有了这些参数,objc_msgSend就可以通过接受者的的isa指针,到其类对象中的方法列表中以选择子的名称为‘键’寻找对应的方法,找到则转到其实现代码执行,找不到则继续根据继承关系从父类中寻找,如果到了根类还是无法找到对应的方法,说明该接受者对象无法响应该消息,则会触发‘消息转发机制’,给开发者最后一次挽救程序崩溃的机会。
消息转发机制:
动态加载
动态加载主要包括两个方面,一个是动态资源加载,一个是一些可执行代码模块的加载,这些资源在运行时根据需要动态的选择性的加入到程序中,是一种代码和资源的‘懒加载’模式,可以降低内存需求,提高整个程序的性能,另外也大大提高了可扩展性。
例如:资源动态加载中的图片资源的屏幕适配,同一个图片对象可能需要准备几种不同分辨率的图片资源,程序会根据当前的机型动态选择加载对应分辨率的图片,像iphone4之前老机型使用的是@1x的原始图片,而retina显示屏出现之后每个像素点被分成了四个像素,因此同样尺寸的屏幕需要4倍分辨率(宽高各两倍)的@2x图片,最新的针对iphone6/6+以上的机型则需要@3x分辨率的图片。例如下面所示应用的AppIcon,需要根据机型以及机型分辨率动态的选择加载某张具体的图片资源:
官方运行时源码:https://opensource.apple.com/source/objc4/objc4-532/runtime/
OC运行时编程指南:Objective-C Runtime Programming Guide
问题:
我们所说的Objective-C是动态运行时语言是什么意思?
主要指的是OC语言的动态性,包括动态性和多态性两个方面。
- 动态性:即OC的动态类型、动态绑定和动态加载特性,将对象类型的确定、方法调用的确定、代码和资源的装载等推迟到运行时进行,更加灵活;
- 多态:多态是面向对象变成语言的特性,OC作为一门面向对象的语言,自然具备这种多态性,多态性指的是来自不同类的对象可以接受同一消息的能力,或者说不同对象以自己的方式响应相同的消息的能力。
问题:
动态绑定是在运行时确定要调用的方法?
动态绑定将调用方法的确定推迟到运行时。在编译时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,代码每次执行都可以得到不同的结果。运行时负责确定消息的接收者和被调用的方法。运行时的消息分发机制为动态绑定提供支持。当向一个动态类型确定了的对象发送消息时,运行环境会通过接收者的isa指针定位对象的类,并以此确定被调用的方法,方法是动态绑定的。
问题:
解释OC中的id类型?id、nil代表什么?
id表示变量或对象的类型在编写代码时(编译期)不确定,视为任意类型,直到程序跑起来推迟到运行时才最终确定其类型。id类似于C/C++中的void *
,但id和void*
并非完全一样。id是一个指向继承了NSObject的OC对象的指针,注意id是一个指针,虽然省略了*号。id和C语言的void*
之间需要通过bridge关键字来显式的桥接转换。具体转换方式示例如下:
id nsobj = [[NSObject alloc] init];
void *p = (__bridge void *)nsobj;
id nsobj = (__bridge id)p;
OC中的nil定义在objc/objc.h中,表示的是一个指向空的Objctive-C对象的指针。例如weak修饰的弱引用对象在指向的对象释放时会自动将指针置为nil,即空对象指针,防止‘指针悬挂’。
问题:
instancetype和id的区别?
instancetype和id都可以用来代表任意类型,将对象的类型确定往后推迟,用于体现OC语言的动态性,使其声明的对象具有运行时的特性。
它们的区别是:instancetype只能作为返回值类型,但在编译期instancetype会进行类型检测,因此对于所有返回类的实例的类方法或实例方法,建议返回值类型全部使用instancetype而不是id,具体原因后面举例介绍;id类型既可以作为返回值类型,也可以作为参数类型,也可以作为变量的类型,但id类型在编译期不会进行类型检测。
问题: 一般的方法method和OC中的选择器selector有何不同?
selector是一个方法的名字,基于动态绑定环境下,method是一个组合体,包含了名字和实现。
可以理解@selector()就是取类方法的编号,他的行为基本可以等同C语言的中函数指针,只不过C语言中,可以把函数名直接赋给一个函数指针,而Objective-C的类不能直接应用函数指针,这样只能做一个@selector语法来取. 它的结果是一个SEL类型。这个类型本质是类方法的编号(函数地址)。
问题:什么是目标-动作(target-action)机制?
目标是动作消息的接收者。例如一个控件,或者更为常见的是它的单元,
以插座变量的形式保有其动作消息的目标。 动作是控件发送给目标的消息,或者从目标的角度看,它是目标为了响应动作而实现的方法。程序需要某些机制来进行事件和指令的翻译。这个机制就是目标-动作机制。
问题:下列代码取决于Objective-C的哪样特性?
id myobj;
... ...
[myobj draw];
- 预处理机制
- 枚举数据类型
- 静态类型
- 动态类型(right)
问题: Object-C有私有方法吗? 私有变量呢?
首先要看私有的含义。私有主要指的是通过类的封装性,将不希望让外界看到的方法或属性隐藏在类内部,只有该类可以在内部访问,外部不可见不可访问。
表面上OC中是可以实现私有的变量和方法的,即将它们隐藏不暴露在头文件,不可以显式地直接访问,但是OC中这种私有并不是绝对的私有,例如即使将变量和方法隐藏在.m实现文件中,开发者仍然可以利用runtime运行时机制强行访问没有暴露在头文件的变量和方法。
OC中实现变量和方法‘私有‘的方式:
一种是在类的头文件中生命私有变量:
#import <Foundation/Foundation.h>
@interface Test : NSObject {
/* 头文件中定义私有变量,默认为@protected */
@private
NSString *major;
}
@end
另外一种是在.m实现文件头部的类扩展区域定义私有属性或方法,其中方法可不用声明,直接在实现文件中实现即可,只要不在头文件生命的方法都对外不可见:
#import "Test.h"
@interface Test() {
/* 类扩展区域定义私有变量,默认就是@private */
int age;
}
/* 类扩展区域定义私有属性 */
@property (nonatomic, copy) NSString *name;
/* 类扩展区域定义私有实例方法(可省略声明,类方法的作用主要就是提供对外接口的,所以一般不会定义为私有) */
- (void)test;
@end
@implementation Test
/**
* 私有实例方法
*/
- (void)test {
NSLog(@"这是个私有实例方法!");
}
@end