hyice / QAPool

收集一些平常遇到的问题,记录解决的过程和一些总结性思考
MIT License
2 stars 0 forks source link

NSObject 和它的 MetaClass #9

Open hyice opened 6 years ago

hyice commented 6 years ago

在开始本篇的具体内容前,大家需要对上图表达的东西有个大概的了解,大家可以看一下这篇文章的 Class 部分。我这里简单罗列下之后会重点涉及的几个概念:

  1. 每个类都会有一个对应的MetaClass,对类方法的调用,实际上是对该MetaClass实例方法的调用。也就是说,当我们对一个类调用类方法时,实际上是去获取了MetaClass全局且唯一的实例对象,然后在该实例上调用了实例方法。
  2. 一般来说,如果ChildClass的父类是ParentClass,那么ChildMetaClass的父类就是ParentMetaClass。但NSObject的父类为nil,而它的MetaClass的父类却是NSObject。也就是说,NSObject基本上可以说是所有类的根父节点,当然也有例外,如NSProxy
  3. 为了下文读写的便利性,我先把NSObjectMetaClass称呼为RootMetaClass,之所以我敢这么称呼,是因为它真的是根节点,连它自己的MetaClass都是自己。

好了,如果你已经完全理解我上面说的几个概念,那么我们就开始今天的主题吧。

NSObject的类方法与实例方法

首先,我们来聊聊NSObject上的类方法与实例方法。相信你这时候应该很困惑,这有什么好聊的,不就是我们上面提到过的一个基本概念里的内容嘛。

每个类都会有一个对应的MetaClass,对类方法的调用,实际上是对该MetaClass实例方法的调用。也就是说,当我们对一个类调用类方法时,实际上是去获取了MetaClass全局且唯一的实例对象,然后在该实例上调用了实例方法。

没错,对于大部分的类来说,这两者就是这么泾渭分明。只要你理解清楚了MetaClass的概念,这里并不会有什么理解障碍。但对NSObjectRootMetaClass来说,类方法和实例方法并没有这么泾渭分明。

假设我们现在通过CategoryNSObject加了一个-test方法,那么我们如果进行[NSObject test]调用的话会发生什么呢?

不知道你有没有注意到一点,RootMetaClass的父类是NSObject。所以说,在方法查找过程中,如果一个方法在RootMetaClass上没有找到,就会继续到它的父类NSObject上去继续找。回到上面的例子上来的话,首先会在RootMetaClass上查找test方法,没有找到,接着就会到NSObject上继续找,于是就找到了我们新增的这个方法。

所以说,NSObject上的每一个实例方法的声明,也是相当于一个类方法的声明。并且,每一个实例方法,都是可以当做类方法进行使用的,只不过对应到的实例是一个全局唯一的实例罢了。

Method Forwarding

方法调用过程中,如果在类的缓存及方法列表中没有找到对应的方法,那么就会进行动态加载和转发。如果你对这一过程不是很清楚的话,可以先看一下这篇文章的消息转发部分。

对于动态加载来说,实例方法和类方法分别定义有+resolveInstanceMethod:+resolveClassMethod:来进行对应的处理。但对动态转发的两个步骤来说,NSObject上只定义了以下几个实例方法:

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

那么,对于类方法的动态转发过程,我们又该如何处理呢?

通过上一小节我们知道,NSObject上实例方法的声明和类方法的声明是等价的。所以,对于NSObject的子类,如果需要对类方法进行动态转发,只需要实现以下方法,并在合适的地方调用super即可:

+ (id)forwardingTargetForSelector:(SEL)aSelector;
+ (void)forwardInvocation:(NSInvocation *)anInvocation;
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

那么,如果我想重写NSObject上的这几个方法,来实现方法转发,我是不是只要通过Method Swizzling替换实例的3个方法就可以了呢?

这就取决于NSObject本身到底是只实现了实例的方法,还是实际上也实现了类方法,只是没有显式声明。验证方法很简单,我们先替换3个实例方法,然后看看类方法的转发会不会进来即可。当然,我们也可以直接阅读这部分的开源代码进行验证。

不管你通过哪种方法,都可以以下结论:NSObject上几乎所有方法都实现了实例方法和类方法,只不过很多没有进行显式声明。

所以,我们的认知又可以进一步。NSObject的类方法虽然可以是对应的实例方法,但NSObject自身并没有这么去做。从源代码来看,很多实例方法和对应的类方法实现其实是完全一致的,但代码还是写了两份。这样可以让逻辑更清楚,实例方法和类方法各自有独立的继承链和根节点,不会有任何混合的可能性。在我们自己写代码的时候,也不推荐利用这层隐式的关联。

结论

虽然因为NSObjectRootMetaClass之间的关系,导致了NSObject的类方法可以是对应的实例方法,但这一点并不推荐在实际开发中进行应用。需要注意的是,这一特点导致了NSObject中的很多类方法并没有进行显式声明,实际开发中大家可以根据实际情况进行试验。

Leppard commented 6 years ago

给NSObject加了一个-test方法这里是不是应该是添加一个+test

添加类方法最后找到的是从NSObject继承过来的实例方法

hyice commented 6 years ago

给NSObject加了一个-test方法这里是不是应该是添加一个+test

添加类方法最后找到的是从NSObject继承过来的实例方法

居然有人看!!!

NSObject加的就是实例方法- test。然后后面调用的却是类方法 [NSObject test]。这时候直观来看,NSObject实际上是没有+ test方法的(或者说是RootMetaClass上没有- test方法),应该报错才对。但实际上RootMetaClass的父类就是NSObject,所以会继承到这个方法。