对象的类型并非在编译期就绑定好了,而是要在运行期查找。而且还有个特殊的类型叫做id,它能指代任意的Objective-C对象类型。一般情况下,应指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有消息。
如下面代码所示:
对于str1,我指明了其具体类型是NSString *,所以当我给str1发送了一个NSString无法解读的消息后,编译器就产生了警告信息。对于str2,其类型为id,虽然NSString无法解读该消息,但是Foundation框架中有其他的类可以解读该消息,而编译器又假定id类型的str2能响应所有消息,所以就没有报错。
这个时候我们可能就会有个疑惑:_之前的文章中说,编译器无法确定某类型对象到底能解读多少种选择子,因为在运行期还可向其中动态新增;可是我们这里又说,当给一个指明了其具体类型的消息接收者发送了其无法解读的消息的时候,编译器就会产生警告信息,这不是矛盾了吗?_其实不然。即便是使用了动态新增技术,编译器也觉得应该能在某个头文件中找到方法原型的定义。
Objective-C对象的本质
每个Objective-C对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个“ * ”字符:
NSString *str1 = @"Norman";
str1是一个指针变量,我们可以将其理解成是一个存放内存地址的变量,而NSString自身的数据就存在于那个地址中。因此我们可以说,str1指向了NSString实例。所有的Objective-C对象都是如此。如果我们将对象所需的内存分配在栈上,那么编译器就会报错:
对于通用的对象类型id,其数据结构的定义是这样的(关于Objective-C中类、实例对象等数据结构的详细说明,请参考我之前写的文章:Runtime——相关数据结构的说明):
typedef struct objc_object *id;
所以id本身已经是指针了,因此我们能够这样写:
id str2 = @"Lee";
上面这种使用id来定义字符串的方式,与使用NSString *来定义相比,其语法意义是相同的。唯一的区别是:如果声明时指定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会探知此情况,并发出警告信息。
刚才提到,id的数据结构定义是这样的:
typedef struct objc_object *id;
那么实例对象的数据结构是如何的呢?如下:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为“is a”指针。例如,刚才的例子中所用的对象“是一个”(is a)NSString,所以其“is a”指针就指向NSString。
Class的定义如下:
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}
此结构体的首个变量也是isa指针,这说明Class本身也是Objective-C对象。类对象所属的类型(也就是isa所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。****“类方法”就定义在此处,因为这些方法可以理解成类对象的实例方法。
结构体里还有个变量叫做super_class,它定义了本类的父类。
关于这一块内容我之前也专门写过一篇文章,isa指针,实例对象、类对象、元类等之间的关系在那篇文章里都有详细说明。
在类继承体系里查询类型信息
isMemberOfClass能够判断出对象是否为某个特定类的实例,而isKindOfClass则能判断出对象是否为某类或其派生类的实例。例如:
NSMutableArray *arr = [NSMutableArray array];
BOOL a = [arr isMemberOfClass:[NSMutableArray class]];
BOOL b = [arr isMemberOfClass:[NSArray class]];
BOOL c = [arr isKindOfClass:[NSMutableArray class]];
BOOL d = [arr isKindOfClass:[NSArray class]];
BOOL e = [arr isKindOfClass:[NSMutableDictionary class]];
NSLog(@"%d, %d, %d, %d, %d", a, b, c, d, e);
最后打印出来的结果是:
2017-07-02 11:31:38.427 删掉****[20548:2488416] 0, 0, 1, 1, 0
其实b、 c、 d、 e都没啥毛病,可是按道理 a 难道不应该是1吗?其实不然。arr看似是NSMutableArray的实例对象,其实是NSMutableArray的某一个子类的实例对象,NSMutableArray实际上是一个类簇。关于类簇,我前面的文章Effective Objective-C 2.0——以“类族模式”隐藏实现细节有专门写过。
我们如果将对象a所属的类打印出来的话:
NSMutableArray *arr = [NSMutableArray array];
BOOL a = [arr isMemberOfClass:[NSMutableArray class]];
NSLog(@"%d", a);
NSLog(@"%@, %@", [arr class], [NSMutableArray class]);
其结果如下:
2017-07-02 11:50:21.774 删掉****[20606:2500604] 0
2017-07-02 11:50:21.774 删掉****[20606:2500604] __NSArrayM, NSMutableArray
我们发现,arr所属的类其实是__NSArrayM,所以[arr isMemberOfClass:[NSMutableArray class]]肯定就是NO了。
如上是介绍了Objective-C中的类型信息查询方法,类型信息的查询都是在运行期进行的,首先使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。
前面我讲到,可以通过isMemberOfClass和isKindOfClass来判断某实例对象是否为某类或其派生类的实例。如果我们要比较类对象的等同性,那么可以使用 == 操作符,而不要使用比较Objective-C对象时常用的“isEqual:”方法。至于为啥不使用“isEqual:”方法,可以参考这篇文章Effective Objective-C 2.0——理解“对象等同性”这一概念;可以使用 == 操作符,是因为类对象是“单例”(singleton),在应用程序范围内,每个类的Class都仅有一个实例。也就是说,另外可以精确判断出对象是否为某类实例的办法是:
BOOL b = [arr class] == [NSMutableArray class];
但是,即便能这么做,我们也应该尽量使用类型信息查询方法,而不应该直接通过 == 操作符来直接比较两个类对象是否等同,因为使用类型信息查询方法可以正确处理那些使用了消息传递机制的对象。
比如,某个对象A可能会把其收到的所有选择子都转发给另外一个对象B。这样的对象A叫做proxy,此种对象均以NSProxy为基类(NSProxy这个类我之前也没怎么接触过,下一篇文章将重点介绍一下这个基类)。通常情况下,如果在此种proxy对象(即对象A)上调用class方法,那么返回的是proxy对象本身(此类是NSProxy的子类),而非接收proxy的对象(即对象B)所属的类。然而,若是改用isKindOfClass这样的类型信息查询方法,那么proxy对象(即对象A)就会把这条消息转给“接收peoxy的对象”(proxied object,即对象B)。也就是说,这条消息的返回值与直接在接收proxy的对象(即对象B)上面查询其类型所得的结果相同。因此,对于对象A,通过isKindOfClass方法查出来的类对象与通过class方法所返回的那个类对象不同,class方法所返回的类表示NSProxy的某个子类,而非对象B所属的类。
总结
1,每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
2,如果对象的类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
3,尽量使用类型信息查询方法来确定对象类型,而不要直接比较对象,因为某些对象可能实现了消息转发功能。
本文分享自微信公众号 - iOS小生活(iOSHappyLife)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。