NSObject之一

Objective-C中有两个NSObject,一个是NSObject类,另外一个是NSObject协议。而其中NSObject类采用了NSObject协议。在本文中,咱们主要整理一下NSObject类的使用。html

说到NSObject类,写Objective-C的人都应该知道它。它是大部分Objective-C类继承体系的根类。这个类提供了一些通用的方法,对象经过继承NSObject,能够从其中继承访问运行时的接口,并让对象具有Objective-C对象的基本能力。如下咱们就来看看NSObejct提供给咱们的一些基础功能。ios

+load与+initialize

这两个方法可能平时用得比较少,但颇有用。在咱们的程序编译后,类相关的数据结构会保留在目标文件中,在程序运行后会被解析和使用,此时类的信息会经历加载和初始化两个过程。在这两个过程当中,会分别调用类的load方法和initialize方法,在这两个方法中,咱们能够适当地作一些定制处理。不当是类自己,类的分类也会经历这两个过程。对于一个类,咱们能够在类的定义中重写这两个方法,也能够在分类中重写它们,或者同时重写。git

load方法

对于load方法,当Objective-C运行时加载类或分类时,会调用这个方法;一般若是咱们有一些类级别的操做须要在加载类时处理,就能够放在这里面,如为一个类执行Swizzling Method操做。程序员

load消息会被发送到动态加载和静态连接的类和分类里面。不过,只有当咱们在类或分类里面实现这个方法时,类/分类才会去调用这个方法。github

在类继承体系中,load方法的调用顺序以下:objective-c

  1. 一个类的load方法会在其全部父类的load方法以后调用
  2. 分类的load方法会在对应类的load方法以后调用

在load的实现中,若是使用同一库中的另一个类,则多是不安全的,由于可能存在的状况是另一个类的load方法尚未运行,即另外一个类可能还没有被加载。另外,在load方法里面,咱们不须要显示地去调用[super load],由于父类的load方法会自动被调用,且在子类以前。数组

在有依赖关系的两个库中,被依赖的库中的类其load方法会优先调用。但在库内部,各个类的load方法的调用顺序是不肯定的。安全

initialize方法

当咱们在程序中向类或其任何子类发送第一条消息前,runtime会向该类发送initialize消息。runtime会以线程安全的方式来向类发起initialize消息。父类会在子类以前收到这条消息。父类的initialize实现可能在下面两种状况下被调用:数据结构

  1. 子类没有实现initialize方法,runtime将会调用继承而来的实现
  2. 子类的实现中显示的调用了[super initialize]

若是咱们不想让某个类中的initialize被调用屡次,则能够像以下处理:app

+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

由于initialize是以线程安全的方式调用的,且在不一样的类中initialize被调用的顺序是不肯定的,因此在initialize方法中,咱们应该作少许的必须的工做。特别须要注意是,若是咱们initialize方法中的代码使用了锁,则可能会致使死锁。所以,咱们不该该在initialize方法中实现复杂的初始化工做,而应该在类的初始化方法(如-init)中来初始化。

另外,每一个类的initialize只会被调用一次。因此,若是咱们想要为类和类的分类实现单独的初始化操做,则应该实现load方法。

若是想详细地了解这两个方法的使用,能够查看《Effective Objective-C 2.0》的第51条,里面有很是详细的说明。若是想更深刻地了解这两个方法的调用,则能够参考objc库的源码,另外,NSObject的load和initialize方法一文从源码层面为咱们简单介绍了这两个方法。

对象的生命周期

一说到对象的建立,咱们会当即想到[[NSObject alloc] init]这种经典的两段式构造。对于这种两段式构造,唐巧大神在他的”谈ObjC对象的两段构造模式“一文中做了详细描述,你们能够参考一下。

本小节咱们主要介绍一下与对象生命周期相关的一些方法。

对象分配

NSObject提供的对象分配的方法有alloc和allocWithZone:,它们都是类方法。这两个方法负责建立对象并为其分配内存空间,返回一个新的对象实例。新的对象的isa实例变量使用一个数据结构来初始化,这个数据结构描述了对象的信息;建立完成后,对象的其它实例变量被初始化为0。

alloc方法的定义以下:

+ (instancetype)alloc

而allocWithZone:方法的存在是由历史缘由形成的,它的调用基本上和alloc是同样的。既然是历史缘由,咱们就不说了,官方文档只给了一句话:

This method exists for historical reasons; memory zones are no longer used by Objective-C.

咱们只须要知道alloc方法的实现调用了allocWithZone:方法。

对象初始化

咱们通常不去本身重写alloc或allocWithZone:方法,不用去关心对象是如何建立、如何为其分配内存空间的;咱们更关心的是如何去初始化这个对象。上面提到了,对象建立后,isa之外的实例变量都默认初始化为0。一般,咱们但愿将这些实例变量初始化为咱们指望的值,这就是init方法的工做了。

NSObject类默认提供了一个init方法,其定义以下:

- (instancetype)init

正常状况下,它会初始化对象,若是因为某些缘由没法完成对象的建立,则会返回nil。注意,对象在使用以前必须被初始化,不然没法使用。不过,NSObject中定义的init方法不作任何初始化操做,只是简单地返回self。

固然,咱们定义本身的类时,能够提供自定义的初始化方法,以知足咱们本身的初始化需求。须要注意的就是子类的初始化方法须要去调用父类的相应的初始化方法,以保证初始化的正确性。

讲完两段式构造的两个部分,有必要来说讲NSObject类的new方法了。

new方法其实是集alloc和init于一身,它建立了对象并初始化了对象。它的实现以下:

+ (instancetype)new {
    return [[self alloc] init];
}

new方法更多的是一个历史遗留产物,它源于NeXT时代。若是咱们的初始化操做只是调用[[self alloc] init]时,就能够直接用new来代替。不过若是咱们须要使用自定义的初始化方法时,一般就使用两段式构造方式。

拷贝

说到拷贝,相信你们都很熟悉。拷贝能够分为“深拷贝”和“浅拷贝”。深拷贝拷贝的是对象的值,两个对象相互不影响,而浅拷贝拷贝的是对象的引用,修改一个对象时会影响到另外一个对象。

在Objective-C中,若是一个类想要支持拷贝操做,则须要实现NSCopying协议,并实现copyWithZone:【注意:NSObject类自己并无实现这个协议】。若是一个类不是直接继承自NSObject,则在实现copyWithZone:方法时须要调用父类的实现。

虽然NSObject自身没有实现拷贝协议,不过它提供了两个拷贝方法,以下:

- (id)copy

这个是拷贝操做的便捷方法。它的返回值是NSCopying协议的copyWithZone:方法的返回值。若是咱们的类没有实现这个方法,则会抛出一个异常。

与copy对应的还有一个方法,即:

- (id)mutableCopy

从字面意义来说,copy能够理解为不可变拷贝操做,而mutableCopy能够理解为可变操做。这便引出了拷贝的另外一个特性,便可变性。

顾名思义,不可变拷贝即拷贝后的对象具备不可变属性,可变拷贝后的对象具备可变属性。这对于数组、字典、字符串、URL这种分可变和不可变的对象来讲是颇有意义的。咱们来看以下示例:

NSMutableArray *mutableArray = [NSMutableArray array];
NSMutableArray *array = [mutableArray copy];
[array addObject:@"test1"];

实际上,这段代码是会崩溃的,咱们来看看崩溃日志:

-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x100107070
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x100107070'

从中能够看出,通过copy操做,咱们的array实际上已经变成不可变的了,其底层元类是__NSArrayI。这个类是不支持addObject:方法的。

偶尔在代码中,也会看到相似于下面的状况:

@property (copy) NSMutableArray *array;

这种属性的声明方式是有问题的,即上面提到的可变性问题。使用self.array = **赋值后,数组实际上是不可变的,因此须要特别注意。

mutableCopy的使用也挺有意思的,具体的还请你们本身去试验一下。

释放

当一个对象的引用计数为0时,系统就会将这个对象释放。此时run time会自动调用对象的dealloc方法。在ARC环境下,咱们再也不须要在此方法中去调用[super dealloc]了。咱们重写这个方法主要是为了释放对象中用到的一些资源,如咱们经过C方法分配的内存空间。dealloc方法的定义以下:

- (void)dealloc

须要注意的是,咱们不该该直接去调用这个方法。这些事都让run time去作吧。

消息发送

Objective-C中对方法的调用并非像C++里面那样直接调用,而是经过消息分发机制来实现的。这个机制核心的方法是objc_msgSend函数。消息机制的具体实现咱们在此不作讨论,能够参考Objective-C Runtime 运行时之三:方法与消息

对于消息的发送,除了使用[obj method]这种机制以外,NSObject类还提供了一系列的performSelector**方法。这些方法可让咱们更加灵活地控制方法的调用。接下来咱们就来看看这些方法的使用。

在线程中调用方法

若是咱们想在当前线程中调用一个方法,则可使用如下两个方法:

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes

这两个方法会在当前线程的Run loop中设置一个定时器,以在delay指定的时间以后执行aSelector。若是咱们但愿定时器运行在默认模式(NSDefaultRunLoopMode)下,可使用前一个方法;若是想本身指定Run loop模式,则可使用后一个方法。

当定时器启动时,线程会从Run loop的队列中获取到消息,并执行相应的selector。若是Run loop运行在指定的模式下,则方法会成功调用;不然,定时器会处于等待状态,直到Run loop运行在指定模式下。

须要注意的是,调用这些方法时,Run loop会保留方法接收者及相关的参数的引用(即对这些对象作retain操做),这样在执行时才不至于丢失这些对象。当方法调用完成后,Run loop会调用这些对象的release方法,减小对象的引用计数。

若是咱们想在主线程上执行某个对象的方法,则可使用如下两个方法:

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array

咱们都知道,iOS中全部的UI操做都须要在主线程中处理。若是想在某个二级线程的操做完成以后作UI操做,就可使用这两个方法。

这两个方法会将消息放到主线程Run loop的队列中,前一个方法使用的是NSRunLoopCommonModes运行时模式;若是想本身指定运行模式,则使用后一个方法。方法的执行与以前的两个performSelector方法是相似的。当在一个线程中屡次调用这个方法将不一样的消息放入队列时,消息的分发顺序与入队顺序是一致的。

方法中的wait参数指定当前线程在指定的selector在主线程执行完成以后,是否被阻塞住。若是设置为YES,则当前线程被阻塞。若是当前线程是主线程,而该参数也被设置为YES,则消息会被当即发送并处理。

另外,这两个方法分发的消息不能被取消。

若是咱们想在指定的线程中分发某个消息,则可使用如下两个方法:

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array

这两个方法基本上与在主线程的方法差很少。在此就再也不讨论。

若是想在后台线程中调用接收者的方法,可使用如下方法:

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

这个方法会在程序中建立一个新的线程。由aSelector表示的方法必须像程序中的其它新线程同样去设置它的线程环境。

固然,咱们常常看到的performSelector系列方法中还有几个方法,即:

- (id)performSelector:(SEL)aSelector
- (id)performSelector:(SEL)aSelector withObject:(id)anObject
- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

不过这几个方法是在NSObject协议中定义的,NSObject类实现了这个协议,也就定义了相应的实现。这个咱们将在NSObject协议中来介绍。

取消方法调用请求

对于使用performSelector:withObject:afterDelay:方法(仅限于此方法)注册的执行请求,在调用发生前,咱们可使用如下两个方法来取消:

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument

前一个方法会取消因此接收者为aTarget的执行请求,不过仅限于当前run loop,而不是全部的。

后一个方法则会取消由aTarget、aSelector和anArgument三个参数指定的执行请求。一样仅限于当前run loop。

消息转发及动态解析方法

当一个对象能接收一个消息时,会走正常的方法调用流程。但若是一个对象没法接收一个消息时,就会走消息转发机制。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析
  2. 备用接收者
  3. 完整转发

具体流程可参考Objective-C Runtime 运行时之三:方法与消息,《Effective Objective-C 2.0》一书的第12小节也有详细描述。在此咱们只介绍一下NSObject类为实现消息转发提供的方法。

首先,对于动态方法解析,NSObject提供了如下两个方法来处理:

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

从方法名咱们能够看出,resolveClassMethod:是用于动态解析一个类方法;而resolveInstanceMethod:是用于动态解析一个实例方法。

咱们知道,一个Objective-C方法是实际上是一个C函数,它至少带有两个参数,即self和_cmd。咱们使用class_addMethod函数,能够给类添加一个方法。咱们以resolveInstanceMethod:为例,若是要给对象动态添加一个实例方法,则能够以下处理:

void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
}

+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically))
    {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSel];
}

其次,对于备用接收者,NSObject提供了如下方法来处理:

- (id)forwardingTargetForSelector:(SEL)aSelector

该方法返回未被接收消息最早被转发到的对象。若是一个对象实现了这个方法,并返回一个非空的对象(且非对象自己),则这个被返回的对象成为消息的新接收者。另外若是在非根类里面实现这个方法,若是对于给定的selector,咱们没有可用的对象能够返回,则应该调用父类的方法实现,并返回其结果。

最后,对于完整转发,NSObject提供了如下方法来处理

- (void)forwardInvocation:(NSInvocation *)anInvocation

当前面两步都没法处理消息时,运行时系统便会给接收者最后一个机会,将其转发给其它代理对象来处理。这主要是经过建立一个表示消息的NSInvocation对象并将这个对象看成参数传递给forwardInvocation:方法。咱们在forwardInvocation:方法中能够选择将消息转发给其它对象。

在这个方法中,主要是须要作两件事:

  1. 找到一个能处理anInvocation调用的对象。
  2. 将消息以anInvocation的形式发送给对象。anInvocation将维护调用的结果,而运行时则会将这个结果返回给消息的原始发送者。

这一过程以下所示:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];

    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}

固然,对于一个非根类,若是仍是没法处理消息,则应该调用父类的实现。而NSObject类对于这个方法的实现,只是简单地调用了doesNotRecognizeSelector:。它再也不转发任何消息,而是抛出一个异常。doesNotRecognizeSelector:的声明以下:

- (void)doesNotRecognizeSelector:(SEL)aSelector

运行时系统在对象没法处理或转发一个消息时会调用这个方法。这个方法引起一个NSInvalidArgumentException异常并生成一个错误消息。

任何doesNotRecognizeSelector:消息一般都是由运行时系统来发送的。不过,它们能够用于阻止一个方法被继承。例如,一个NSObject的子类能够按如下方式来重写copy或init方法以阻止继承:

- (id)copy
{
    [self doesNotRecognizeSelector:_cmd];
}

这段代码阻止子类的实例响应copy消息或阻止父类转发copy消息—虽然respondsToSelector:仍然报告接收者能够访问copy方法。

固然,若是咱们要重写doesNotRecognizeSelector:方法,必须调用super的实现,或者在实现的最后引起一个NSInvalidArgumentException异常。它表明对象不能响应消息,因此老是应该引起一个异常。

获取方法信息

在消息转发的最后一步中,forwardInvocation:参数是一个NSInvocation对象,这个对象须要获取方法签名的信息,而这个签名信息就是从methodSignatureForSelector:方法中获取的。

该方法的声明以下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

这个方法返回包含方法描述信息的NSMethodSignature对象,若是找不到方法,则返回nil。若是咱们的对象包含一个代理或者对象可以处理它没有直接实现的消息,则咱们须要重写这个方法来返回一个合适的方法签名。

对应于实例方法,固然还有一个处理类方法的相应方法,其声明以下:

+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector

另外,NSObject类提供了两个方法来获取一个selector对应的方法实现的地址,以下所示:

- (IMP)methodForSelector:(SEL)aSelector
+ (IMP)instanceMethodForSelector:(SEL)aSelector

获取到了方法实现的地址,咱们就能够直接将IMP以函数形式来调用。

对于methodForSelector:方法,若是接收者是一个对象,则aSelector应该是一个实例方法;若是接收者是一个类,则aSelector应该是一个类方法。

对于instanceMethodForSelector:方法,其只是向类对象索取实例方法的实现。若是接收者的实例没法响应aSelector消息,则产生一个错误。

测试类

对于类的测试,在NSObject类中定义了两个方法,其中类方法instancesRespondToSelector:用于测试接收者的实例是否响应指定的消息,其声明以下:

+ (BOOL)instancesRespondToSelector:(SEL)aSelector

若是aSelector消息被转发到其它对象,则类的实例能够接收这个消息而不会引起错误,即便该方法返回NO。

为了询问类是否能响应特定消息(注意:不是类的实例),则使用这个方法,而不使用NSObject协议的实例方法respondsToSelector:。

NSObject还提供了一个方法来查看类是否采用了某个协议,其声明以下:

+ (BOOL)conformsToProtocol:(Protocol *)aProtocol

若是一个类直接或间接地采用了一个协议,则咱们能够说这个类实现了该协议。咱们能够看看如下这个例子:

@protocol AffiliationRequests <Joining>

@interface MyClass : NSObject <AffiliationRequests, Normalization>

BOOL canJoin = [MyClass conformsToProtocol:@protocol(Joining)];

经过继承体系,MyClass类实现了Joining协议。

不过,这个方法并不检查类是否实现了协议的方法,这应该是程序员本身的职责了。

识别类

NSObject类提供了几个类方法来识别一个类,首先是咱们经常使用的class类方法,该方法声明以下:

+ (Class)class

该方法返回类对象。当类是消息的接收者时,咱们只经过类的名称来引用一个类。在其它状况下,类的对象必须经过这个方法相似的方法(-class实例方法)来获取。以下所示:

BOOL test = [self isKindOfClass:[SomeClass class]];

NSObject还提供了superclass类方法来获取接收者的父类,其声明以下:

+ (Class)superclass

另外,咱们还可使用isSubclassOfClass:类方法查看一个类是不是另外一个类的子类,其声明以下:

+ (BOOL)isSubclassOfClass:(Class)aClass

描述类

描述类是使用description方法,它返回一个表示类的内容的字符串。其声明以下:

+ (NSString *)description

咱们在LLDB调试器中打印类的信息时,使用的就是这个方法。

固然,若是想打印类的实例的描述时,使用的是NSObject协议中的实例方法description,咱们在此很少描述。

归档操做

一说到归档操做,你会首先想到什么呢?我想到的是NSCoding协议以及它的两个方法: initWithCoder:和encodeWithCoder:。若是咱们的对象须要支持归档操做,则应该采用这个协议并提供两个方法的具体实现。

在编码与解码的过程当中,一个编码器会调用一些方法,这些方法容许将对象编码以替代一个更换类或实例自己。这样,就可使得归档在不一样类层次结构或类的不一样版本的实现中被共享。例如,类簇能有效地利用这一特性。这一特性也容许每一个类在解码时应该只维护单一的实例来执行这一策略。

NSObject类虽然没有采用NSCoding协议,但却提供了一些替代方法,以支持上述策略。这些方法分为两类,即通用和专用的。

通用方法由NSCoder对象调用,主要有以下几个方法和属性:

@property(readonly) Class classForCoder

- (id)replacementObjectForCoder:(NSCoder *)aCoder

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder

专用的方法主要是针对NSKeyedArchiver对象的,主要有以下几个方法和属性:

@property(readonly) Class classForKeyedArchiver

+ (NSArray *)classFallbacksForKeyedArchiver

+ (Class)classForKeyedUnarchiver

- (id)replacementObjectForKeyedArchiver:(NSKeyedArchiver *)archiver

子类在归档的过程当中若是有特殊的需求,能够重写这些方法。这些方法的具体描述,能够参考官方文档

在解码或解档过程当中,有一点须要考虑的就是对象所属类的版本号,这样能确保老版本的对象能被正确地解析。NSObject类对此提供了两个方法,以下所示:

+ (void)setVersion:(NSInteger)aVersion

+ (NSInteger)version

它们都是类方法。默认状况下,若是没有设置版本号,则默认是0.

总结

NSObject类是Objective-C中大部分类层次结构中的根类,并为咱们提供了不少功能。了解这些功能更让咱们更好地发挥Objective-C的特性。

参考

  1. NSObject Class Reference
  2. Archives and Serializations Programming Guide
  3. NSObject的load和initialize方法
  4. Objective-C Runtime 运行时之三:方法与消息
  5. 《Effective Objective-C 2.0》

http://southpeak.github.io/blog/2015/01/31/nsobjectzhi-%5B%3F%5D/

相关文章
相关标签/搜索