Objective-C语言的动态性总结(编译时与运行时)

编译时与运行时

编译时: 即编译器对语言的编译阶段,编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等,将程序代码翻译成计算机能够识别的语言(例如汇编等),编译通过并不意味着程序就可以成功运行。

运行时: 即程序通过了编译这一关之后编译好的代码被装载到内存中跑起来的阶段,这个时候会具体对类型进行检查,而不仅仅是对代码的简单扫描分析,此时若出错程序会崩溃。

可以说编译时是一个静态的阶段,类型错误很明显可以直接检查出来,可读性也好;而运行时则是动态的阶段,开始具体与运行环境结合起来。

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对象是否调用某个方法不是在编译期决定的,编译期方法的调用不和代码绑定在一起,而是到了运行时根据发出的具体消息而动态确定要调用的代码。利用动态类型和动态绑定可以实现代码每次执行消息和消息的接收者可能会变化,执行结果不一样;另外与动态绑定相关的还有基于消息传递机制的消息转发机制,主要处理应对一些接收者无法处理的消息,此时有机会将消息转发给其他接收者处理。

动态绑定是基于动态类型的,在运行时对象的类型确定后,那么对象的属性和方法也就确定了,包括类中原来的属性和方法,以及运行时动态新加入的属性和方法。可以通过对象的isa指针到对象的类对象中找到方法列表,即可接收的消息列表。
消息传递机制:

在OC中,方法的调用不再理解为对象调用其方法,而是要理解成对象接收消息,消息的发送采用‘动态绑定’机制,具体会调用哪个方法直到运行时才能确定,确定后才会去执行绑定的代码。方法的调用实际就是告诉对象要干什么,给对象(的指针)传送一个消息,对象为接收者(receiver),调用的方法及其参数即消息(message),给一个对象传消息表达为:[receiver message]; 接受者的类型可以通过动态类型识别于运行时确定。

在消息传递机制中,当开发者编写[receiver message];语句发送消息后,编译器都会将其转换成对应的一条objc_msgSend C语言消息发送原语,具体格式为: void objc_msgSend (id self, SEL cmd, ...)

这个原语函数参数可变,第一个参数填入消息的接受者,第二个参数是消息‘选择子’,后面跟着可选的消息的参数。有了这些参数,objc_msgSend就可以通过接受者的的isa指针,到其类对象中的方法列表中以选择子的名称为‘键’寻找对应的方法,找到则转到其实现代码执行,找不到则继续根据继承关系从父类中寻找,如果到了根类还是无法找到对应的方法,说明该接受者对象无法响应该消息,则会触发‘消息转发机制’,给开发者最后一次挽救程序崩溃的机会。

消息转发机制:

如果消息传递过程中,接受者无法响应收到的消息,则会触发进入‘消息转发’机制。

消息转发依次提供了三道防线,任何一个起作用都可以挽救此次消息转发。按照先后顺序三道防线依次为:
动态补加方法的实现

+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel

直接返回消息转发到的对象(将消息发送给另一个对象去处理)

- (id)forwardingTargetForSelector:(SEL)aSelector

手动生成方法签名并转发给另一个对象

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

这里以一个简单的例子展示消息转发的完整个过程。定义一个Test类,类头文件声明一个名为instanceMethod的实例方法但不提供方法实现(消息转发主要就针对实例方法,类方法由于无法在运行时动态添加实现等事实并不能转发给其他类)

/* Test.h */
@interface Test : NSObject
/* 只声明一个实例方法而不在.m文件中实现 */
- (void)instanceMethod;
@end

然后在main函数中实例化Test对象并调用该实例方法,由于方法没有实现,因此在运行时一定会触发消息转发机制:

/* main.m */
#import "Test.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    Test *test = [[Test alloc] init];
    [test instanceMethod];
    return 0;
}

先进入消息转发的第一道防线,我们在Test类的.m文件中提供运行时的转发接应,实现resolveInstanceMethod方法为指定的instanceMethod消息补加对应方法的实现完成补救:

/* Test.m */
#import <objc/runtime.h>
/*
 * 被动态添加的实例方法实现
 */
void instanceMethod(id self, SEL _cmd) {
    NSLog(@"收到消息后会执行此处的函数实现...");
}

/*
 * 动态补加方法实现
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(instanceMethod)) {
        class_addMethod(self, sel, (IMP)instanceMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

如果没有实现resolveInstanceMethod方法就行补救或者直接返回了NO,则进入第二道防线,这里我们要实现forwardingTargetForSelector函数返回另一个实例对象,让该对象代替原对象去处理这个消息。 假设我们让一个叫做Test2的类对象去处理这个消息,Test2类中要有同名的方法和方法的实现,这样就会执行Test2中的同名方法完成消息转发:

/* Test2.h */
@interface Test2 : NSObject
- (void)instanceMethod;
@end

/* Test2.m */
@implementation Test2
- (void)instanceMethod {
    NSLog(@"消息转发到这...");
}
@end

/* Test.m */
- (id)forwardingTargetForSelector:(SEL)aSelector {
    /* 返回转发的对象实例 */
    if (aSelector == @selector(instanceMethod)) {
        return [[Test2 alloc] init];
    }
    return nil;
}

如果没有实现上面的两个补救方法或者forwardingTargetForSelector方法直接返回了nil,则进入最后一道防线,此时我们要手动生成方法签名并实现forwardInvocation方法将消息转发给另一个对象,同第二道防线类似:

/* Test.m */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    /* 为指定的方法手动生成签名 */
    NSString *selName = NSStringFromSelector(aSelector);
    if ([selName isEqualToString:@"instanceMethod"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    /* 如果另一个对象可以相应该消息,则将消息转发给他 */
    SEL sel = [anInvocation selector];
    Test2 *test2 = [[Test2 alloc] init];
    if ([test2 respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:test2];
    }
}

动态加载

动态加载主要包括两个方面,一个是动态资源加载,一个是一些可执行代码模块的加载,这些资源在运行时根据需要动态的选择性的加入到程序中,是一种代码和资源的‘懒加载’模式,可以降低内存需求,提高整个程序的性能,另外也大大提高了可扩展性。

例如:资源动态加载中的图片资源的屏幕适配,同一个图片对象可能需要准备几种不同分辨率的图片资源,程序会根据当前的机型动态选择加载对应分辨率的图片,像iphone4之前老机型使用的是@1x的原始图片,而retina显示屏出现之后每个像素点被分成了四个像素,因此同样尺寸的屏幕需要4倍分辨率(宽高各两倍)的@2x图片,最新的针对iphone6/6+以上的机型则需要@3x分辨率的图片。例如下面所示应用的AppIcon,需要根据机型以及机型分辨率动态的选择加载某张具体的图片资源:


45.png

问题: 我们所说的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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容