iOS寒哥管理的技术专题

读书总结之NSObject

2016-05-08  本文已影响103人  向心

使用 Objectvice-C 进行全面对象编程时,除了需要知道语言本身的语法和面向对象的知识外,还需要了解Objectvice-C的根类 NSObject 的信息。

NSObject

根类的作用

作为一门动态编程语言,Objectstvice-C有很多动态的特性,因此,Objectvice-C不进需要编译环境,同时还需要一个运行时系统(runtime system)来执行编译好的代码。运行时系统扮演的角色类似于Objectvice-C的操作系统,他负责完成对象生成、释放时的内存管理、发来的消息查找对应的处理方法等工作。

通常情况下,程序无法直接使用运行时系统提供的功能。根类方法提供了运行时系统的基本工恩给你。继承了 NSObject 的所有类都可以自由的使用运行时系统的功能,也就是说,根类就想到于系统的一个借口。

根类通过哪些方式提供了哪些功能对系统有很大的影响。因此,根类不同的系统之间是无法开发出通用的程序的。

Cocoa 是以OPENSTEPDE的核心 API 为基础发展起来的。OPENSTEP的前身为 NeXTstep。在 NeXTstep 时代,根类是累 Object,而在 OPENSTEP 时代,根类则变为了 NSObject,同时类的设计也得到了大幅度的改进。

NSArray,NSString 等等NS前缀类、函数归属于cocoa Fundation基础类库,其"NS”的由来据说是这样的:乔布斯被苹果开除后,创立了NeSt公司,而cocoa Fundation基础类库就是出自于NeST公司,NeST中的"NS"被作为Fundation中所有成员的前缀

类和实例

NSObject 只是一个实例变量,就是 Class 类型的变量 isa。isa 用于表示实例对象属于哪个类对象。因为 isa 决定着实例变量和类的关系,非常重要,所以子类不可以修改 isa 的值。另外,也不能通过直接访问 isa 来查询实例变量到底属于哪个类,而是要通过实例方法 class 来完成查询。

在运行时的代码中我们可以查看到objc_class的定义如下:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

下面对类和实例变量的相关方法进行说明。NSObject 的方法与其说是为自己定义的,不如说是为了其子类和所有实例对象而定义的。

- (class) class
    返回消息接收者所属类的类对象

+ (class) class
    返回类型对
    虽然可以使用类名作为消息的接受者来调用类方法,但类对象是其他消息的参数,或者将类对象赋值给变量的时候,需要通过这个方法来获取类的参数

- (id) self
    返回接受者自身。是一个无任何实际动作但很有用的方法。

-(BOOL) isMemberOfClass: (Class) aClass
    判断消息接受者是不是参数 aClass 类的对象
    
-(BOOL) isKindOfClass: (Class) aClass
    判断消息接受者是否是参数 aClass 类或者 aClass 的子类的实例。这个函数和 isMemberOfClass:的区别在于当消息的接受者是 aClass 的子类的实例时也返回 YES。
    
- (BOOL) isSubclassOfClass: (Class) aClass
    判断消息接受者是不是参数 aClass 的子类或自身,如果是则返回 YES
    
- (Class) superclass
    返回消息接受者所在类的父类的类对象。
    
+ (Class) superclass
    返回消息接收类的父类和类对象

实例对象的生成和释放

+ (id) alloc
    生成消息接收类的实例对象。通常和 init 或者 init 开头的方法连用,生成实例化对象的同事需要对其进行初始化。子类中不润徐重写 alloc 方法

+ (void) dealloc
    释放实例对象。dealloc 被称之为 release 的结果调用。除了在子类中重写 dealloc 的情况之外,程序不润徐直接调用 dealloc

- (oneway void)release
    将消息接受者的引用计数减1.引用计数变为0时,dealloc 方法被调用,消息接受者被释放

- (id)retain
    为消息接收者的引用计数加1,同事返回消息接收者

- (id)autorelease
    把消息的接受者加入到自动释放池中,同事返回消息接受者
    
- (NSUinteger) retainCount
    返回消息接受者的引用计数,可在调试时使用这个方法。NSUInteger 是无符号证书类型

- (void)finalize
    垃圾收集器在释放接受者对象之前会执行该 finalize 方法。

上面从 dealloc 到 retainCount 都是手动引用计数管理内存时使用的方法,使用 ARC 时不可用。finalize 仅供垃圾回收有效时使用。

初始化

- (id) init
    init 可对 alloc 生成的实例对象进行初始化。子类中可以重写 init 或者自定义的心的以 init 开头的初始化函数。

+ (void)initialize
    被用于类的初始化,也就是对类中共同使用的变量进行初始化设定等。这个方法会在类收到第一个消息之前被自动执行,不需手动调用
    
+ (id) new
    new 是 alloc 和 init 的组合。new 方法返回的实例对象的所有者就是调用 new 方法的对象。但是把 alloc 和 init 组合定义为 new 没有什么优点,并不建议使用。
    根据类的实现不同,new 方法并不会每次都返回一个全新的实例对象。有时new 方法会返回对象池中预先生成的对象,也有可能每次都返回同一个对象。

对象的比较

-(BOOL) isEqual: (id) anObject
    消息的接受者如果和参数 anObject 相等则返回 YES
    
- (NSUInteger) hash
    把对象放入容器的时候,返回系统内部用的散列表

原则上来讲,具有相同 id 值也就是同一个指针指向的对象被认为是相等的。而子类在这个基础上进行了扩展,把拥有相同值认为是相等。我们可以根据需求对“想通知”激进型定义,但一般都会让具备“相同值”的对象返回相同的散列表,因此就需要对散列表方法进行重新定义。而反之则并不成立,也就是说,散列值相等的两个对象不一定相等。

另外,有的累中还自定义了 compare:或者isEqualTo 之类的方法。至于到底是哪个方法或者自定义类的时候是否需要定义比较的方法,都需要根据目的和类的内容做具体分析。

对象的内容描述

+ (NSString*) description
    返回一个 NSString 类型的字符串,表示消息接受者所属类的内容。通常是这个类的名称。

- (NSString*)description
    返回一个 NSString 类型的字符串,表示消息接受者的实例对象的内容。通常是类名家 id 值。子类中可以重新定义 description 的返回值。例如 NSString的实例会返回字符串的内容,NSArray 的实例会对数组中的每一个元素调用 description,然后将调用结果用句号进行分割,并且一起返回。

消息发送机制

选择器和 SEL 类型

至今为止我们把选择器(方法名)和消息关键字放在一起进行说明。程序中的方法名(选择器)在便有被一个内部标识符所代替,这个内部标识符所对应的数据类型就是 SEL 类型。

Objective-C为了能够在程序中操作编译后的选择器,定义了@selector()指令。通过使用@selector()指令,就可以直接饮用编译后的选择器。使用方法如下:

@selector(mutableCopy)
@selector(compare:)
@selector(replaceObjectAtIndex:withObject:)

也可以声明 SEL 类型的变量

选择器不同的情况下,编译器转换后生成的 SEL 类型的值也一定不同,相同的算择期对应的 SEL 类型的值一定相同。Objective-C的程序员不需要知道选择器对应的 SEL 类型的值到底是什么,具体的值是和处理器相关的。但是如果 SEL 类型的便利功能无效的话,可设其为 NULL,或者也可以使用(SEL)0这种常见的表达方式

可以使用 SEL 类型的变量来发送消息,为此,NSObject 中准备了如下方法

-(id)performSelector: (SEL) aSelector
    向消息的接收者发送 aSelector代表的消息,返回这个消息的执行结果
    
-(id)performSelector: (SEL) aSelector 
            withObject: (id) anObject
    与上面的方法一直,不过可以传递参数

例如下面两个消息表达式进行的处理是相同的

[target description]
[targ performSelector: @selector(description)];

下面的例子展示了如何根据条件动态决定执行那个方法

SEL method = (void1) ? @selector(activate:) : @selector(hide:);
id obj = (cond2) ? MyDocument : defaultDocument;
[target performSelector:method withObject:obj]

这种调用方式很想 C 语言中函数指针的用法,使用函数指针可以实现和上面的程序同样的功能。

函数指针是函数在内存中的地址。指针对应的函数是在编译的时候决定的,不能够执行指定之外的函数。SEL 类型就相当于方法名,根据消息接受者的不同(上例子中 target 的赋值),来动态执行不同的方法。

通过 SEL 类型来指定要中子星的方法,这就是 Objectivce-C消息发送的方式。也正是通过这种方法才实现了 Objectivce-C的动态性

消息搜索

对象收到一个消息后执行哪个方法是动态决定的。

所有的实例变量都存在一个 Class 类型的 isa 变量,它就是类对象。当收到消息后,运行时系统会检查类内是否有和这个消息选择器相同的方法,如果有就执行对应的方法,如果没有就通过类对象中指向父类的指针来查找父类中是否有对应的方法。如果一直找到根类都没有找到对应的方法,就会提示执行时错误。

如果每次收到消息都需要查找对应的方法的话,消息发送过程的开销就会很大,是针对这种情况,运行时内部会缓存一个散列表,表中记录着某个类拥有和什么样的选择器相对应的方法、方法被定义在何处等信息。这样一来,当下次在收到同样的消息时,直接利用上次缓存好的信息即可。

NSObject 中定义了可以动态检查一个对象是否能够响应某个选择器的方法。

- (BOOL) respondsToSelector: (SEL) aSelector
    查询消息的接收者中是否能够响应 aSelector 的方法,包括从父类继承来的方法,如果存在的话则返回 YES

- (BOOL) instancesRespondToSelector: (SEL) aSelector
    查询消息的接收者所属的类中是否能够响应 aSelector 的实例方法,如果存在的话则返回 YES

一函数的形式来调用方法

类中定义的方法通常是以函数的形式来实现的,但通常在编程的时候并不会直接操作方法所对应的函数。

但如果想尽可能第让程序更快一点,或者需要按照 C 语言的管理传递函数指针的时候,可以直接调用方法对应的函数,以节省发送消息的开销。另外执行时动态加载方法的定义等时,也可以将方法作为函数调用。但是需要注意的是,如果以函数的形式来调用方法的话,将无法利用面向对象的动态绑定等功能。虽然消息发送同函数调用相比确实慢一点,但却有面向对象的动态绑定、多态等优点。同这些优点相比,速度上略微的损失是不值得一提的。而其实消息发送的速度也非常的快。

通过使用下面的方法,可以获得某个对象持有的方法的函数指针,这些方法都被定义在 NSObject 中。

- (IMP) methodForSelector: (SEL) aSelector
    搜索和执行选择器对应的方法,并返回指向该方法实现的函数指针。实例对象和类对象都可以使用这个方法。对实例对象使用时,会返回实例方法对应的函数,对类对象使用时,会返回类对象对应的函数
+ (IMP) instanceMethodForSelector: (SEL)aSelector;
    搜索和制定选择器相对应的实例方法,并返回该指向实例方法实现的函数指针

IMP 是“implemention”的缩写,它是一个函数指针,指向了方法实现代码的入口

IMP 的定义为:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

这个被指向的函数包括 id(self 指针)、调用的 SEL(方法名),以及其他参数

例如:

- (id)setBox:(id)obj1 title:(id)obj2;

foo 是这个方法所属类的一个实例变量。获取指向 setBox 的函数指针,并且通过该指针进行函数调用的过程如下:

    IMP funcp;
    funcp = [foo methodForSelector:@selector[setBox:title]];
    xyz = (*funcp)(foo,@selector[setBox:title:],param1,param2)

类对象和跟对象

因为累对象也是一个对象,所以累对象可以作为根类 NSObject 的某个子类的对象来使用。下面这句话看上去好像比较奇怪,但是实际上他是正确的,会返回 YES

    [NSString class] isKindOfClass:[NSObject class]]

这就说明了相当于类对象的类的对象是存在的。而类对象的类就被叫做元类(metaclass)。实例对象(instance object)所属的类是 class,类对象(class object)所属的类是 metaclass。

Objective-C 中的很多概念都来源于 Smalltalk,元类的概念就是其中之一。但现在的 Objective-C中已经不存在元类的概念了,程序中不能操作元类。用于表示对象的 id 类型和表示类的 Class 类实际上都是指向结构体的指针。被详细定义在/usr/include/objc 下面的 objc.h 头文件中。通过查看 objc.h 中 id和 Class 的定义,就会发现类和元类的关系如图所示。Objective-C2.0更新了基本的数据结构,但是没有改变类和元类的关系。

类 A 是 NSObject 的子类,类 B是 A 的子类。类对象和实例对象都存在一个成员变量 isa,它是一个 objc_class 类型的指针

图中带有 isa 的实现表明了 isa 指向的对象,带有 super_class 的虚线则表明了父类的关系。

类对象中保存的是实例方法,元对象中保存的是累方法。通过这样的定义能够统一实现实例方法和类方法的调用机制。

因为编程时不可以直接操作元类,所以并不需要完全了解图中的细节。大家只需要记住任何一个类对象都是继承了根类的元对象的一个实例即可。也就是说,类对象可以执行根类对象的实例方法。

例如,类对象可以执行 NSObject 的实例方法 performSelector:和 respondsToSelector:。当然提前是没有将这些方法作为类方法再次定义。

下面让我们总结一下。其中(1)和(2)我们已经介绍过了。(3)比较不容易理解,

  1. 所有类的实例对象都可以执行根类的实例方法
  1. 所有类的类对象都可以执行根类的类方法
  1. 所有类的类对象都可以执行根类的实例方法

参考文献

① Objectivce-C编程全解(第三版) [日]荻原刚志 著 唐璐 翟俊杰 译

更多内容请关注我的独立博客:http://www.aircrayon.xyz

上一篇下一篇

猜你喜欢

热点阅读