Objective-C动态特性

1.相关术语

  • 动态语言(动态编程语言):是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。主要动态语言:Objective-C、C#、JavaScript、PHP、Python、Erlang。
  • 静态语言(静态编程语言):与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。
  • 动态类型语言:动态类型语言是指在运行期间才去做数据类型检查的语言。动态类型语言的数据类型不是在编译阶段决定的,而是把类型绑定延后到了运行阶段。主要语言:Python、Ruby、Erlang、JavaScript、swift、PHP、Perl。
  • 静态类型语言:静态语言的数据类型是在编译其间确定的或者说运行之前确定的,编写代码的时候要明确确定变量的数据类型。主要语言:C、C++、C#、Java、Objective-C。

⚠️注意:动态类型语言和动态语言是完全不同的两个概念。前者关注数据类型,而后者关注代码结构。

2.为什么说OC是动态运行时语言

首先,OC 是动态语言,因为 OC 通过可以通过动态绑定改变自身结构。其次 OC 将数据类型的确定由编译时,推迟到了运行时。

关于类型检查,一般会把类型分为两类:动态的和静态的,分别对应动态类型语言和静态类型语言。

  • 动态类型:动态的在运行时做检查。以往,编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。由于只是在运行时做此类检查,所以叫做动态类型。
  • 静态类型:静态类型在编译时做检查。当在代码中使用 ARC 时,编译器在编译期间,会做许多的类型检查:因为编译器需要知道哪个对象该如何使用。例如,如果 myObject 没有 hello 方法,那么就不能写如下这行代码了。

  • [myObject hello];

⚠️注意:从以上可以看出 OC 是动态语言,也是静态类型语言,但是 OC 中类型、对象属性和方法的真正确定是在运行时(动态类型),所以 OC 是动态运行时语言。

以下介绍 OC 的动态性。

OC 语言的动态性主要体现在三个方面:动态类型(Dynamic typing)、动态绑定(Dynamic binding)和动态加载(Dynamic loading)。

2.1 动态类型

动态类型指的是对象指针类型的动态性,具体是指使用 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 类型的作用了,将类型的确定延迟到了运行时。

动态类型识别方法

  • 首先是Class类型

    • Class class = [NSObject class]; // 通过类名得到对应的 Class 动态类型
    • Class class = [obj class]; // 通过实例对象得到对应的 Class 动态类型
    • if([obj1 class] == [obj2 class]) // 判断是不是相同类型的实例
  • Class动态类型和类名字符串的相互转换

    • NSClassFromString(@”NSObject”); // 由类名字符串得到 Class 动态类型
    • NSStringFromClass([NSObject class]); // 由类名的动态类型得到类名字符串
    • NSStringFromClass([obj class]); // 由对象的动态类型得到类名字符串
  • 判断对象是否属于某种动态类型

    • - (BOOL)isKindOfClass:Class // 判断某个对象是否是动态类型 Class 的实例或其子类的实例
    • - (BOOL)isMemberOfClass:Class // 与 isKindOfClass 不同的是,这里只判断某个对象是否是 Class 类型的实例,不放宽到其子类
  • 判断类中是否有对应的方法

    • - (BOOL)respondsTosSelector:(SEL)selector // 类中是否有这个类方法
    • - (BOOL)instancesRespondToSelector:(SEL)selector // 类中是否有这个实例方法

    注意:上面两个方法都可以通过类名调用,前者判断类中是否有对应的类方法(通过‘+’修饰定义的方法),后者判断类中是否有对应的实例方法(通过‘-’修饰定义的方法)。此外,前者 respondsTosSelector 函数还可以被类的实例对象调用,效果等同于直接用类名调用后者 instancesRespondToSelector 函数。

    举个例子:假设有一个类 Test,有它的一个实例对象 test,Test 类中定义了一个类函数:+ (void)classFun;和一个实例函数:- (void)objFunc;,那么各种调用情况的结果如下:

    [1][Test respondsToSelector:@selector(objFunc)];//NO
    [2][Test respondsToSelector:@selector(classFunc)];//YES
    
    [3][Test instancesRespondToSelector:@selector(objFunc)];//YES
    [4][Test instancesRespondToSelector:@selector(classFunc)];//NO
    
    [5][test respondsToSelector:@selector(objFunc)];//YES
    [6][test respondsToSelector:@selector(classFunc)];//NO

    结论: 如果想判断一个类中是否有某个类方法,应该使用 [2] ; 如果想判断一个类中是否有某个实例方法,可以使用 [3] 或者 [5] 。

  • 方法名字符串和SEL类型的转换

    在编译器,编译器会根据方法的名字和参数序列生成唯一标识改方法的 ID,这个 ID 为 SEL 类型。到了运行时编译器通过 SEL 类型的 ID 来查找对应的方法,方法的名字和参数序列相同,那么它们的 ID 就都是相同的。另外,可以通过 @selector() 指示符获得方法的 ID。常用的方法如下:

    SEL funcID = @selector(func);// 这个注册事件回调时常用,将方法转成 SEL 类型
    
    SEL funcID = NSSelectorFromString(@"func"); // 根据方法名得到方法标识
    
    NSString *funcName = NSStringFromSelector(funcID); // 根据 SEL 类型得到方法名字符串

2.2 动态绑定

基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。在继续之前,需要明确 Objective-C 中消息的概念。由于 OC 的动态特性,在 OC 中其实很少提及“函数”的概念,传统的函数一般在编译时就已经把参数信息和函数实现打包到编译后的源码中了,而在 OC 中最常使用的是消息机制。调用一个实例的方法,所做的是向该实例的指针发送消息,实例在收到消息后,从自身的实现中寻找响应这条消息的方法。

动态绑定所做的,即是在实例所属类确定后,在运行时动态的为类添加属性和方法,以及方法的最后处理或转发,这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要的新加入的实现。

属性的处理主要涉及如下两个方法:

//setter
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

//getter
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key);

方法的处理请参考这里——iOS开发笔记(三):消息传递与转发机制

2.3 动态加载

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

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

    • 为 class pair 分配存储空间 ,使用 objc_allocateClassPair 函数。
    • 增加需要的方法使用 class_addMethod 函数,增加实例变量用 class_addIvar。
    • 用 objc_registerClassPair 函数注册这个类,以便它能被别人使用。

3.参考

  • 深入Objective-C的动态特性
  • 【iOS沉思录】Objective-C语言的动态性总结(编译时与运行时)
  • 编译器做些什么?

原文地址:iOS开发笔记(六):Objective-C动态特性-蒲公英云

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值