NSObject
作为我们构建和使用的cocoa
编程的一部分是几乎所有类的根类。它实际上做了些什么,还有它到底是怎么工作的呢?今天,我将从无到有重建NSObject,就像我的一位作家博友Gwynne Raskind
建议的那样。
根类的组成部份
根类到底是怎么工作的?就Objective-C
本身而言,这里有一个明确的要求:根类的第一个实例变量必须是isa
,一个指向对象所属类的指针。当一个对象发送(调度)消息的时候isa
指针用来指出这个对象所属的类。以上是从一个严格的语言角度来看待。
根类提供的方法往往不会被经常使用到,当然,NSObject
基类提供了大量的这种方法。NSObject
提供的功能可细分为三类:
1、内存管理:像retain
和 release
这种标准的内存管理方法是在NSObject
类中实现的。当然 alloc
方法也在NSObject
类中实现。
2、Introspection(内省):NSObject
提供了为数众多的基本上都是围绕Objective-C Runtime
功能的封装方法,如class
, respondsToSelector :
和isKindOfClass:
。
3、各种方法的缺省实现:这里有一堆的方法我们指望每个对象自己来实现,例如isEqual:
和description
.为了确保每个对象都能实现这些方法,NSObject
为继承它的字类提供了一个缺省的实现,如果子类没有实现这些方法的话。
代码
我将重新实现NSObject
的功能的类取名为MAObject
,我已经将这篇文章的代码放在了这里
注意,我的这份代码不是在ARC下环境下写的,虽然ARC非常棒我们应该尽可能的使用ARC,但MRC环境下用来实现一个基类是非常好的方式,因为基类需要实现内存管理而ARC更愿意将内存管理交给编译器来管理。
实例变量
MAObject
有两个实例变量,第一个是 isa
指针,第二个是对象的引用计数变量。
@implementation MAObject {
Class isa;
volatile int32_t retainCount;
}
为了确保线程安全我们将使用OSAtomic.h
中的功能函数来管理引用计数,这就是为什么retainCount
有一个不同寻常的定义而不是使用NSUInteger
或类似的属性来定义。
NSObject
实际上是从外部持有引用计数。这里有一个全局表用来映射对象的地址和它的引用计数。这样可以节省内存,因为这个表代表了常用的引用计数 1 完全不必在表中拥有一个条目。但是,这种技术很复杂,而且有点慢,所以在我自己的版本中我选择不遵循它。
内存管理
MAObject
首先要决解能够创建实例对象的问题。通过实现+alloc
方法很容易做到。(我将跳过被弃绝并且很少使用的方法:+allocWithZone:
,这是目前经常做的事反正忽略它的参数吧!)
子类很少重写+alloc
方法,它往往是依靠根类来分配内存。这意着MAObject
不仅仅要能够为它自己并且要为其子类提供创建实例对象的方法。通过利用在类方法中self的值实际上是类自身在发送消息这一事实可以解决这个问题。如果执行到这一行[SomeSubclass alloc]
代码时,self
会持有一个指向子类SomeSubclass
的指针。那个类可以用来查询并指出在Runtime
中分配了多少内存,同时将isa
指针指向正确的位置。为了适用一个新创建的对象,引用计数也相应加1:
+ (id)alloc
{
MAObject *obj = calloc(1, class_getInstanceSize(self));
obj->isa = self;
obj->retainCount = 1;
return obj;
}
retain
简单的使用了OSAtomicIncrement32
来增加引用计数,并且返回它自己。
- (id)retain
{
OSAtomicIncrement32(&retainCount);
return self;
}
release方法稍微多做些操作。它首先减少引用计数。如果引用计数为0,那么对象将被销毁,因此将调用dealloc
方法:
- (oneway void)release
{
uint32_t newCount = OSAtomicDecrement32(&retainCount);
if(newCount == 0)
[self dealloc];
}
自动释放autorelease
的实现会调用释放池NSAutoreleasePool
将自己self
加入到当前的释放池。自动释放池目前是Runtime
的一部分,这有点绕,但是自动释放池关于Runtime
的APIs
是私有的,所以下面是我们目前能做到的最好的:
- (id)autorelease
{
[NSAutoreleasePool addObject: self];
return self;
}
retainCount
方法简单的返回了持有该变量的值:
- (NSUInteger)retainCount
{
return retainCount;
}
最后,dealloc
方法。在普通的类中,dealloc
需要清除所有实例变量后调用它父类的dealloc方法。根类实际上更多的是对象本身占用的内存,在此情况下,它仅仅是简单的调用free
函数:
- (void)dealloc
{
free(self);
}
这里有许多非常好的辅助方法。NSObject
提供了一个无为的统一调用的方法init
,因此子类总是调用[super init]
:
- (id)init
{
return self;
}
new
方法仅仅只是封装了alloc
和init
方法:
+ (id)new
{
return [[self alloc] init];
}
这里也有一个 空的finalize
方法。NSObjects
实现这个方法作为它的垃圾回收机制的一部分。MAObject
压根儿就不支持垃圾回收机制,但我写入这个方法是因为NSObject
有这个方法。
- (void)finalize
{
}
内省
许多的內省方法都是围绕Runtime
功能进行封装,因为这不是很有趣,所以我将做一个简洁的讨论关于Runtime
功能幕后是如何工作的。
最简单的內省方法是class
,仅仅只是返回了isa
指针的值:
- (Class)class
{
return isa;
}
技术上来讲,这个方法在以Tagged pointers
方式生成指针时会失败。一个合适的执行语句应该会调用object_getClass
,为Tagged pointers
对象提供正确的行为,并且获得普通指针的isa
值。
superclass
实例方法相当于在对象的 class
方法上唤醒superclass
方法,所以这个方法实际上做了以下操作:
- (Class)superclass
{
return [[self class] superclass];
}
这里也有一些类方法。+class
方法仅仅返回本身,一个类对象。这有一些奇怪,但这的确是NSObject
做的事情。[obj class]
返回的是这个对象的类本身,但是我的方法[MyClass class]
则是返回指向类的一个指针。它不是一成不变的,就像MyClass
也有一个class
,就是MyClass
的metaclass
(元类),但是它是这样工作的:
+ (Class)class
{
return self;
}
+superclass
方法所做的正如它字面上的意思一样。它将会调用class_getSuperclass
,这个方法在类结构里面由Runtime
来维护并将指针指向父类。
+ (Class)superclass
{
return class_getSuperclass(self);
}
这里也有一些方法是用来对两个类进行比较。最简单的一个方法是isMemberOfClass:
,非常严格的检测,忽略了它的子类。它的实现非常简单:
- (BOOL)isMemberOfClass: (Class)aClass
{
return isa == aClass;
}
isKindOfClass:
方法也检测它的子类,因此[subcalssInstance isKindOfClass:[Superclass class]]
返回的是YES
。这个方法的输出本质上和isSubclassOfClass:
是一样的,因此仅仅调用就好:
- (BOOL)isKindOfClass: (Class)aClass
{
return [isa isSubclassOfClass: aClass];
}
isSubclassOfClass:
这个方法有些有趣。从自身开始,它沿着类的层次结构,在每个等级与目标类逐一进行比较。如果发现一个相匹配的对象,返回YES
。如果它从类的层次结构自顶向下没有发现符合的对象,返回NO
:
+ (BOOL)isSubclassOfClass: (Class)aClass
{
for(Class candidate = self; candidate != nil; candidate = [candidate superclass])
if (candidate == aClass)
return YES;
return NO;
}
非常有趣的是我们注意到这个检测并不是特别高效,如果你调用这个方法的类在类层次的很深的位置,那在它返回NO
之前将会有很多次的循环迭代。基于这个原因,isKindOfClass:
检测会比消息发送慢一些,并且在某种确定的情况下将会成为一个重要的瓶颈。只有一个原因能够尽可能的避免这个问题。
就是 respondsToSelector:
方法,该方法仅仅调用Runtime
方法class_respondsToSelector
.它会在类方法列表中依次查询选择器看是否有一个符合条件的条目:
- (BOOL)respondsToSelector: (SEL)aSelector
{
return class_respondsToSelector(isa, aSelector);
}
这里有个类方法instancesRespondToSelector:
,和respondsToSelector:
非常相似。唯一不同的地方是类在这种环境下是通过self
而不是isa
指针,因此调用该方法的类在这里将会是一个元类:
+ (BOOL)instancesRespondToSelector: (SEL)aSelector
{
return class_respondsToSelector(self, aSelector);
}
这里也有两个conformsToProtocol:
方法,一个是实例方法另一个是类方法。这两个方法也仅仅是封装Runtime
功能,在这里只是对每个该类遵循的协议的一张表进行询问以便查看给定的协议是否存在:
- (BOOL)conformsToProtocol: (Protocol *)aProtocol
{
return class_conformsToProtocol(isa, aProtocol);
}
+ (BOOL)conformsToProtocol: (Protocol *)protocol
{
return class_conformsToProtocol(self, protocol);
}
接下来是methodForSelector:
,和她漂亮的表妹(好吧!外国人的幽默)instanceMethodForSelector:
。这两个方法都是通过调用class_getMethodImplementation
方法来实现,这个方法会在类方法列表中查询选择器并返回符合需求的IMP
:
- (IMP)methodForSelector: (SEL)aSelector
{
return class_getMethodImplementation(isa, aSelector);
}
+ (IMP)instanceMethodForSelector: (SEL)aSelector
{
return class_getMethodImplementation(self, aSelector);
}
关于这些方法的一个有趣的部分是class_getMethodImplementation
总是返回一个IMP
,甚至是对未知的选择器也是如此。当这个类实际上不需要实现一个方法时,它返回一个特殊的代理IMP
,这个代理IMP
封装了消息参数开始的路径用来唤醒forwardInvocation:
方法.
methodSignatureForSelector:
方法封装了等价的类方法:
- (NSMethodSignature *)methodSignatureForSelector: (SEL)aSelector
{
return [isa instanceMethodSignatureForSelector: aSelector];
}
instanceMethodSignatureForSelector:
这个类方法依次封装调用了Runtime
方法。它首先取出被给定选择器的Method
。如果方法不能被找到,那么类不会实现那个方法,并且代码返回nil
。相反,它会取出代表这个方法类型的C语言字符串,并且封装成一个NSMethodSignature
对象:
+ (NSMethodSignature *)instanceMethodSignatureForSelector: (SEL)aSelector
{
Method method = class_getInstanceMethod(self, aSelector);
if(!method)
return nil;
const char *types = method_getTypeEncoding(method);
return [NSMethodSignature signatureWithObjCTypes: types];
}
最后,performSelector:
方法和两个带有不同参数的withObject:
方法。这些方法不是严格意义上的内省,但是它们和普通的类别一样封装了低层次的Runtime
功能。他们简单的从被给的选择器中取出IMP
,让IMP
充当合适类型的功能指针并且使用它。
- (id)performSelector: (SEL)aSelector
{
IMP imp = [self methodForSelector: aSelector];
return ((id (*)(id, SEL))imp)(self, aSelector);
}
- (id)performSelector: (SEL)aSelector withObject: (id)object
{
IMP imp = [self methodForSelector: aSelector];
return ((id (*)(id, SEL, id))imp)(self, aSelector, object);
}
- (id)performSelector: (SEL)aSelector withObject: (id)object1 withObject: (id)object2
{
IMP imp = [self methodForSelector: aSelector];
return ((id (*)(id, SEL, id, id))imp)(self, aSelector, object1, object2);
}
缺省实现
MAObject
提供了一堆关于缺省实现的方法。我们将从 isEqual:
和hash
开始,这两个方法仅仅是用对象的指针做目标标识:
- (BOOL)isEqual: (id)object
{
return self == object;
}
- (NSUInteger)hash
{
return (NSUInteger)self;
}
任何子类拥有和这些方法相同或更广泛的概念都将必须重写这些方法,但任何子类对象在任何时候都和它相同就可以使用这些实现。
description
方法是另一个很好的缺省实现方法。它的实现只是从<MAObject: 0xdeadbeef>
中生成一个字符串,这个字符串包含了对象的类和指针值。
- (NSString *)description
{
return [NSString stringWithFormat: @"<%@: %p>", [self class], self];
}
标准的description
方法仅仅是返回类名,因此这里有一个类方法刚好能从Runtime
中取得类名并返回它:
+ (NSString *)description
{
return [NSString stringWithUTF8String: class_getName(self)];
}
doesNotRecognizeSelector:
是一个鲜为人知的实用方法。它会抛出一个异常使它看起来像那个对象实际上并没有响应给定的选择器。这是非常有用的东西,如创建重写点的子类必须实现特定的方法:
- (void)subclassesMustOverride
{
// pretend we don't actually implement this here
[self doesNotRecognizeSelector: _cmd];
}
这部分代码相当简单。唯一有点困难的地方是方法名的格式。我们希望展示的方法就像-[Class method]
一样,但是类方法需要一个+号在前面,就像这样+[Class classMethod]
。为了指清楚它的来龙去脉,代码将会检测查看是否isa
指针是不是一个元类。如果是,那么+
号将会被用到 。否则,它本身就是一个实例方法,-
号将会被用到。剩下的代码仅仅是在raise
合适的异常NSException
:
- (void)doesNotRecognizeSelector: (SEL)aSelector
{
char *methodTypeString = class_isMetaClass(isa) ? "+" : "-";
[NSException raise: NSInvalidArgumentException format: @"%s[%@ %@]: unrecognized selector sent to instance %p", methodTypeString, [[self class] description], NSStringFromSelector(aSelector), self];
}
最后,还有一堆的琐碎的方法就是那些有明显的问题和答案的方法(例如self
方法),存在让子类总是安全的方法它们会调用父类super
(例如空的+initialize
方法),也存在可重写方法(例如copy
的实现后抛出一个异常)。这些方法不是特别有趣,但我将它们完整的概括:
- (id)self
{
return self;
}
- (BOOL)isProxy
{
return NO;
}
+ (void)load
{
}
+ (void)initialize
{
}
- (id)copy
{
[self doesNotRecognizeSelector: _cmd];
return nil;
}
- (id)mutableCopy
{
[self doesNotRecognizeSelector: _cmd];
return nil;
}
- (id)forwardingTargetForSelector: (SEL)aSelector
{
return nil;
}
- (void)forwardInvocation: (NSInvocation *)anInvocation
{
[self doesNotRecognizeSelector: [anInvocation selector]];
}
+ (BOOL)resolveClassMethod:(SEL)sel
{
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
结论
NSObject
是一大束不同功能的集合,但没有特别陌生的东西。它的主要功能是处理内存的开辟和管理所以我们可以切确的创建对象。它也提供了一堆方便的可重写的方法让每个对象可以自定义自己的方法,并且它也提供了一个非常好的API
,这个API
封装了一堆Runtime
功能。
我已经跳过了一大片NSObject
提供的关于KVC
的功能方法。这些方法非常复杂需要单独写一篇文章,因此我将利用另外的时间来描述它们。
原文地址请点击这里
--------------长长的分割线---------------
这理,我们已经考察,本是如此。你需要听,要知道是与自己有益。--约伯记5:27
书到用时方恨少,第一次尝试跟随国外大牛的思维去理解一些OC低层的实现,理解起来非常的困难,因为知识点的匮乏和糟糕英语。我在网上搜索过这篇文章,好像没有关于这篇博文的翻译。如果有人已经翻译,请一定告诉我,我好参考一下。
在理解这篇文章的过程中我发现作者提到的许多概念是关于Runtime
的,所以有Runtime
的知识将会很容易理解文章中的一些内容,现在我在下面整理出一些我之前不理解的概念。
1.什么是isa
指针,它有什么作用,什么是metaClass
(元类),什么是根元类?
关于这些问题可参考这篇文章。
2.什么是Tagged Pointer
?
可参考唐巧大牛的这篇文章。
3.还有许多Runtime
的概念,将在下篇博客中来细细体会。
最后,由于本人对底层的知识非常有限,如果有理解不到位的恳请联系我,我的微信号:ILINGHO。新浪微博在这里