《Effective OC》读书笔记(二):对象、消息、运行期

在Objective-C中,“对象”就是“基本构造单元”,开发者可以通过对象来存储并传递数据,在对象之间传递数据并执行任务的过程叫做“消息传递”。

当应用程序运行起来以后,为其提供相关支持的代码叫做“运行时环境”,它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。

第六条:理解“属性”这一概念

属性”是Objective-C的一项特性,用于封装对象中的数据,OC对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问,其中getter方法用于读取变量值,而setter用于写入变量值。属性这一特性成为Objective-C2.0的一部分,开发者可以令编译器自动编写与属性相关的存取方法。并且引入了点语法,开发者可以更容易使用类对象来访问存放于其中的数据。

在描述个人信息的类中,也许会存放人名,生日,地址等内容:

@interface Person : NSObject{
@public
    NSString *_firstName;
    NSString *_lastName;
@private
    NSString *_someInternalData;
}
@end

JAVA和C++中这种写法比较常见,在这些语言中可以定义实例变量的作用域,OC中则很少这么做,这种写法的问题是:对象布局在编译期就已经固定了,只要碰到访问_firsName变量的代码,编译期就把其替换为“偏移量”,这个偏移量是“硬编码”,表示该变量距离存放对象内存区域的起始地址有多远,这样做目前看来没问题,但是如果又加了一个实例变量就可能出现问题。

@interface Person : NSObject{
@public
    NSString *_birthday;
    NSString *_firstName;
    NSString *_lastName;
@private
    NSString *_someInternalData;
}
@end

原来表示_firstName的偏移量现在指向_birthday,如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错,例如某个代码库中使用了一份旧的类定义,如果和其相链接的代码使用了新的类定义,那么运行时就会出现不兼容现象。各种编程语言都有应对的办法。OC的做法是把实例变量当做一种存储偏移量所用的“特殊变量”,交由“类对象”保管,偏移量会在运行时查找,如果类定义变了,那么偏移量也就变了,这样的话无论何时访问实例变量,总能使用正确的偏移量,甚至可以在运行时新增实例变量,这就是稳固的ABI(应用程序二进制接口)。

使用@property实现属性的定义,编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。

访问属性可以使用点语法,编译器会把点语法转换为对存取方法的调用。编译器也会自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。也可以使用@synthesize来指定实例变量的名字:

@synthesize firstName = _myFirstName;

若不想令编译器自动合成存取方法,则可以自己实现,如果你只实现了其中的一个存取方法,那么另一个还是会由编译器来合成。可以使用@dynamic来告诉编译器,不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。

@interface Person:NSObject
@property NSString *name;
@end

@implementation Person
@dynamic name;
@end

属性还有各种特质,其设定会影响编译器生成的存取方法。

原子性:默认情况下编译器会通过锁定机制确保原子性atomic,如果属性具备nonatomic,则不使用同步锁。

读写权限:readwrite特质的属性拥有getter,setter方法,readonly仅拥有getter方法,可以用此特质把某个属性对外公开为只读属性,然后在class-continuation中重新定义为readwrite

内存管理语义:这个特质仅仅会影响setter方法,在赋值时是将其赋值给底层变量就好,还是应该retain此值呢。

assign:针对纯量类型进行简单赋值操作。

strong:定义了一种拥有关系,这种属性在设置新值时会先保留新值,并释放旧值,然后再将新值设置上去。

weak:非拥有关系,设置方法既不保留新值也不会释放旧值,属性所指的对象遭到摧毁后,属性值也会清空。

unsafe_unretained:和assign相同,但它适用于对象类型,目标对象销毁时,属性值不会清空。

copy:与strong类似,然而设置方法不保留新值,而是将其拷贝,经常用来保护一些类型属性的封装性。

再来说说atomicnonatomic的区别,具备atomic特质的getter会通过锁定机制来确保其原子性,如果两个线程读写同一属性,那么不论何时总能看到有效的属性值,若是不加锁,一个线程正在改写属性,另一个线程很可能会闯入把尚未修改好的属性值读取出来。

但是iOS中同步锁开销较大,会带来性能问题,而且同步锁并不能保证线程安全,若要实现真正意义的线程安全还需要更为深层的锁定机制才行。例如一个线程在连续多次读取某属性的过程中,有别的线程在同时改写该值,即便将属性声明为原子的,还是会读到不同的属性值。

第七条:在对象内部尽量直接访问实例变量

“通过属性访问”与“直接访问”这两种做法应该搭配着使用,建议在读取实例变量的时候采取直接访问的形式,而在设置实例变量的时候通过属性来做。

直接访问不经过OC的“方法派发”步骤,所以直接访问实例变量的速度比较快,编译器所生成的代码会直接访问保存对象实例变量的那块内存。

直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值释放旧值。

直接访问实例变量,那么不会触发“键值观察”。

如果使用了“懒加载”,则必须通过存取方法来访问属性。

第八条:理解“对象等同性”这一概念

按照==操作符比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象,应该使用“isEqual”方法来判断两个对象的等同性。一般来说两个类型不同的对象总是不相等的。某些对象提供了特殊的“等同性判断方法”,如果已知两个对象都属于同一个类,那么就可以使用这种方法。

NSString *fool = @"badge 123";
NSString *bar = [NSString stringWithFormat:@"badge %I",123];
BOOL equalA = (fool = bar);//result = NO
BOOL equalB = [fool isEqual:bar];//result = YES
BOOL equalC = [fool isEqualToString:bar];//result = YES

NSString类实现了一个自己独有的等同性判断方法,名叫“isEqualToString”。传递给该方法的对象必须是NSString,该方法比调用“isEqual”方法快。后者还要实现额外的步骤,因为它不知道受测对象的类型。

NSObject协议中有两个用于判断等同性的方法:

-(BOOL)isEqual:object;
-(NSUInterger)hash;

NSObject类对这两个方法的默认实现是:仅当指针值完全相同时,这两个对象才相等,若想在自定义的对象中正确复写这些方法,就必须先理解其约定。如果“isEqual”方法判断两个对象相等,那么其hash方法也必须返回同一个值,但是如果两个对象的hash返回同一个值,那么“isEqual”方法未必会认为两者相等。

比如下面这个类:

@interface Person : NSObject
@property (nonatomic,copy)NSString *firstName;
@property (nonatomic,copy)NSString *lastName;
@property (nonatomic,copy)NSUInterger age;
@end

如果两个Person类的所有字段均相等,那么这两个对象就向相等,于是“isEqual”方法可以写成:

- (BOOL)isEqual:object{
  if(self == object){
    return YES;
  }
  if([self class]!=[object class]){
    return NO;
  }

  Person *otherPerson = (Person*)object;
  if(![_firstName isEqualToString: otherPerson.firstName){
    return NO;
  }
  if(![_lastName isEqualToString: otherPerson.lastName){
    return NO;
  }
  if(_age != otherPerson.age){
    return NO;
  }

  return YES;
}

首先直接判断两个指针是否相等,若相等则其均指向同一对象,所以受测的对象也必定相等。接下来比较两个对象所属的类,若不属于同一个类,则两对象不相等。最后检测每个属性是否相等,只要有不等的属性,就判定两对象不等,否则两对象相等。

根据等同性约定,若两对象相等,其hash码也相等,但是两个哈希码相同的对象却未必相等,这是能否正确复写“isEqual”方法的关键所在。

-(NSUInteger)hash{
    return 1337;
}

不过若是这么写的话,在Collection中使用这种对象将产生性能问题,因为Collection在检索哈希表时,会用对象的哈希码做索引。假如某个Collection是用NSSet实现的,那么set可能会根据哈希码把对象分装到不同的数组中,在向set中添加新对象时,要根据其哈希码找到与之相关的那个数组,依次检查其中各个元素,看数组中已有的对象是否和将要添加的新对象相等。如果令每个对象都返回相同的哈希码,那么在set中已有10000个对象的情况下,若是向其添加对象,则需要将这10000个对象全部遍历一遍。

有一种哈希码的计算方式既能保持高效又能使生成的哈希码位于一定范围之内,不会过于频繁的重复。

- (NSUInteger)hash{
  NSUInteger firstNameHash = [_firstName hash];
  NSUInteger lastNameHash = [_lastName hash];
  NSUinteger ageHash = [_age hash];
  return firstNameHash^ lastNameHash^ ageHash;
}

此算法生成的哈希码还是会碰撞,不过至少可以保证哈希码有多种可能的取值。

除了NSString之外,数组和字典类页游具有特殊的等同性判定方式,“isEqualToArray”和“isEqualToDictionary”。如果比较的对象不是数组或者字典则会报错,因此开发者应该保证所传对象的类型是正确的。

在可变类型的等同性判断上一定要注意,就是在容器中放入可变类型对象的时候,把某个对象放入Collection之后,就不应该再改变其哈希码了,Collection会把各个对象按照其哈希码分装到不同的“箱子数组”中,如果某对象在放入箱子之后哈希码又变了,那么其现在所处的箱子对它来说就是“错误”的。

用源码来进行一下测试:

NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arrayA = [@[@1,@2] mutableCopy];
[set addObject:arrayA];

//Output:set = {((1,2))}

再向set中加入一个数组,此数组与前一个数组所含的对象相同,顺序也相同,于是待加入的数组与set中已有的数组是相等的

NSMutableArray *arrayB = [@[@1,@2] mutableCopy];
[set addObject:arrayB];

//Output:set = {((1,2))}

此时set里仍然只有一个对象,因为set中不会存在相同的元素。我们再向其添加一个不同的数组:

NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];

//Output:set = {((1),(1,2))}

这时我们改变数组C的内容,令其和最初加入的那个数组相等:

[arrayC addObject:@2];

//Output:set = {((1,2),(1,2))}

set中出现了两个彼此相等的数组,这是set中不允许的,如果我们copy了这个set,情况会更糟糕:

NSMutableSet *setB = [set copy];

//Output:setB = {((1,2))}

set中又只剩下一个对象了,将某对象放入Collection中后改变其内容将会造成一些意想不到的后果,因此一定要慎重。

第九条:以“类族模式”隐藏实现细节

“类族”是一种很有用的模式,可以隐藏“抽象基类”背后的实现细节,Objective-C中普遍使用此模式。比如我们的UIButton类:

+(UIButton *)buttonWithType:(UIButtonType)type;

该方法返回的对象,取决于传入的按钮类型,然而无论返回什么类型的对象,他们都继承自同一个基类:UIButton。这么做的意义在于使用者无需关系创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。

我们可以将多种type重构为多个子类,把对应的绘制方法放到相关子类中去。这时我们使用“类族模式”就可以灵活应对多个类,保持接口简洁,用户无需自己创建子类,只需调用基类方法即可。

假设有一个处理员工的类,每个雇员都有“名字”和“薪水”这两个属性,管理者可以命令其执行日常工作,但是各种雇员的工作内容不同,我们在安排任务的时候可以无需关心每个人是如何完成工作的,只需指示其开工就可以。

typedef NS_ENUM(NSUInteger,EmployeeType){
  EmployeeTypeDeveloper,
  EmployeeTypeDesigner,
  EmployeeTypeFinance,
};

@interface Employee:NSObject
@property(nonatomic,copy)NSString *name;
@property NSUInteger salary;

+(Employee *)employeeWithType:(EmployeeType)type;

-(void)doADaysWork;

@end

@implementation Employee

+(Employee *)employeeWithType:(EmployeeType)type{
  switch(type){
    case EmployeeTypeDeveloper:
      return [EmployeeTypeDeveloper new];
      break;
    case EmployeeTypeDesigner:
      return [EmployeeTypeDesigner new];
      break;
    case EmployeeTypeFinance:
      return [EmployeeTypeFinance new];
      break;
  }
}

- (void)doADaysWork{

}
@end

每个实体子类都从基类继承而来,例如:

@interface EmployeeTypeDeveloper: Employee
@end

@implementation EmployeeTypeDeveloper
-(void) doADaysWork{
  [self writeCode];
}
@end

在本例中,基类实现了一个“类方法”,该方法根据创建的雇员类别分配好对应的雇员类实例,这种“工厂模式”是创建类族的办法之一。

如果对象所属的类位于某个类族中,那么在查询其类型信息时就要当心了,你可能觉得自己创建了某个类的实例,然而实际上创建的是其子类的实例。在这个例子中,[employee isMemberOfClass:[Employee class]]似乎会返回YES,但实际是返回的NO。

系统框架中有许多类族,大部分Collection类都是类族,例如NSArray与其可变版本NSMutableArray,这样看来实际上有两个抽象基类,一个用于不可变数组,一个用于可变数组,尽管具备公共接口的类只有两个,但仍然可以合起来算作一个类族。

NSArray这样的类背后其实是个类族(对于大部分Collection来说都是这样的),明白这一点很重要,否则就会写出来如下代码:

id maybeAnArray = /*...*/;
if ([maybeAnArray class] == [NSArray class]){
  //never be hit
}

这个判断内的类型绝不可能是NSArray本身,因为由NSArray的初始化方法所返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类型。

不过仍有办法判断实例所属的类是否位于类族之中,如果想判断某对象是否位于类族中,不要直接检测两个“类对象”是否等同:

id maybeAnArray = /*...*/;
if ([maybeAnArray isKindOfClass:[NSArray class]]){
  //will be hit
}

第十条:在既有类中使用关联对象存放自定义数据

有时候在对象中存放相关信息,这时我们通常会从对象所属的内存的类中继承一个子类,然后改用这个子类对象,有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。Objective-C中的“关联对象”可以解决此问题。

可以给对象关联许多其他对象,这些对象通过“键”来区分,存储对象值的时候,可以指明“存储策略”,用以维护相应的“内存管理”语义。存储策略由名为objc_AssociationPolicy的枚举所定义。

OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY

下列方法可以管理管理对象:

//此方法可以给定的键和策略为某对象设置关联对象值
objc_setAssociated(id object,void*key,id value, objc_AssociationPolicy policy)
//此方法根据给定的键从某对象中获取相应的关联对象值
objc_getAssociatedObject(id object,void*ket)
//此方法移除指定对象的全部关联对象
objc_removeAssociatedObjects()

设置关联对象所用的键是个“不透明的指针”。如果在两个键上调用“isEqual”方法的返回值时YES,那么NSDictionary就认为二者相等,然而在设置关联对象时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象时,通常使用静态全局变量做键。

这种用法很有用,但是只有在其他办法行不通的时候才会去考虑,若是滥用这种方式,很快就会令代码失控,使其难于调试。“保留环”产生的原因很难查明,因为关联对象之间的关系并没有正式的定义,其内存管理语义是在关联的时候才定义的,而不是在接口中预先定好的,使用这种写法时要小心,不能仅仅因为某处可以用该写法就一定要用它。

第十一条:理解objc_msgSend的作用

在对象上调用方法是Objective-C中经常使用的功能,用Objective-C的术语来说,这叫做“消息传递”。消息“名称”(name)或“选择子”(selector),可以接受参数,而且可能还有返回值

在Objective-C中,如果向某对象传递消息,那就会使用动态绑定机制,来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。

//给对象发送消息可以这样写:
id returnValue = [someObject messageName:parameter];

someObject叫做接收者,messageName叫做“选择子”,选择子与参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其原型如下:

void objc_msgSend(id self,SEL cmd,...)

这是一个参数可变的函数,能接受两个或两个以上的参数,第一个参数代表接收者,第二个参数代表选择子(SEL是选择子的类型),后续参数就是消息中的那些参数,其顺序不变,选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parament);

objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后在跳转。如果还是找不到,那就执行消息转发操作。

这样看来调用一个方法似乎需要很多步骤,所幸objc_msgSend会将匹配结果缓存在“快速映射表”里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。

objc_msgSend等函数一旦找到应该调用的方法实现后,就会跳转过去,Objective-C对象的每个方法都可以视为简单的C函数:

<return_type>Class_selector(id self,SEL _cmd,...)

每个类里面都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表用的“键”。这个函数的原型很像objc_msgSend函数,这不是巧合,而是为了利用“尾调用优化”技术,令跳转方法实现这一操作变得更简单些。

如果一个函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”,编译器会生成调转至另一函数所需的指令码,而不会向堆栈中推入新的“栈帧”,这里要注意某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另做他用时才会执行“尾调用优化”。

这项技术对objc_msgSend非常关键,如果不这么做的话,每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,在“栈踪迹”中可以看到这种“栈帧”。

第十二条:理解消息转发机制

当对象在接收到无法解读的消息之后会发生什么情况?若想令类理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编译器向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现,当对象接收到无法解读的消息后,就会启动消息转发机制,程序员可以经由此过程告诉对象应该如何处理未知消息。

-[__NSCFNumber lowercaseString]:unrecognized selector send to instance 0x87

这段异常信息是由NSObject的“doesNotRecognizedSelector:”方法所抛出的,表示接收者无法理解名为lowercaseString的选择子,在消息转发过程中以程序崩溃而告终,开发者在编写自己的类时,可于转发过程中设置挂钩,用以执行预定的逻辑,而不应该使程序崩溃。

消息转发分为两大阶段。第一阶段先征询接收者,所属的类,看看其是否能动态添加方法,以处理当前这个未知的选择子,这叫做动态方法解析。第二阶段涉及完整的消息转发机制。如果运行期系统已经把第一节点执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用,这又分为两小步。首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转发给那个对象,于是消息转发过程结束,一切如常。若没有备用的接收者,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息

对象在收到无法解读的消息后,首先将调用其所属类的下列方法:

+ (BOOL)resolveInstanceMethod:(SEL)selector

返回值为BOOL类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法叫做“resolveClassMethod”。

使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。

当前接收者还有第二次机会能处理未知的选择子,能否把这条消息转给其他接收者来处理,方法如下:

- (id)forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到就返回nil在一个对象内部可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的

如果前两部分都没能成功调用方法,那么就只能启用完整的消息转发机制,首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封装在其中,此对象包含选择子,目标以及参数,在触发NSInvocation对象时,“消息派发系统”将消息指派给目标对象。

- (void)forwardingInvocation:(NSInvocation *)invocation

这个方法可以实现的很简单:只需改变调用目标,使消息得以在新目标上调用。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数,或是改换选择子等等。

实现此方法时,若发现某调用操作不应该由本类处理,则需要调用父类的同名方法,这样的话继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继续调用“doesNotRecognizeSelector”以抛出异常表明此选择子最终没能得到处理。

第十三条:用“方法调配技术”调试“黑盒方法”

类的方法列表会把选择子的名称映射到相关方法实现之上,使得“动态消息派发系统”能够根据此找到应该调用的方法,这些方法均以函数指针的形式来表示,这种指针叫做IMP,类型如下:

id (*IMP)(id,SEL,...)

Objective-C运行期系统提供的几个方法都能够用来操作这张表,开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。

void method_exchangeImplementation(Method m1,Method m2)

此函数的两个参数表示待交换的两个方法实现,而方法实现可以通过下列函数获得:

Method class_getInstanceMethod(Class aClass,SEL aSelector)

此函数根据给定的选择从类中取出与之相关的方法,执行下列代码即可交换方法实现:

Method originalMethod = class_getInstanceMethod([NSString class],@selector(lowercaseString));

Method swappedMethod = class_getInstanceMethod([NSString class],@selector(uppercaseString));

method_exchangeImplementation(originalMethod, swappedMethod)

在实际开发中这样直接替换两个方法意义不大,但是可以通过这个手段来为既有的方法实现增添新功能,我们可以在分类中新增一个方法,然后与原有方法进行替换,这样便可以实现为现有的方法增加功能

通过这个方案,开发者可以为那些“完全不知道具体实现的”黑盒方法增加日志记录功能,非常有助于远程调试,但是不要滥用,这样会使代码难以维护。

第十四条:理解“类对象”的用意

每个Objective-C对象实例都是指向某块内存数据的指针,所以声明变量时,类型后面要跟一个“*”字符:

NSString *pointerVariable = @"Some string";

对于通用的对象类型id,由于其本身已经是指针了,所以我们能够这样写:

id genericTypedString = @"Some string";

描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:

typedef struct objc_object{
  Class isa;
}*id;

由此可见,每个对象结构体的首个成员是Class类的变量,该变量定义了对象所属的类,通常称为“is a”的指针,例如刚才例子中的对象“是一个”(is a)NSString,所以其“isa”就指向NSStringClass对象也定义在运行期程序库的头文件中:

struct objc_class {
    //metaclass
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    //父类
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    //类名
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    //版本号
    long version                                             OBJC2_UNAVAILABLE;
    // 类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
    long info                                                OBJC2_UNAVAILABLE;
    // 该类的实例变量大小(包括从父类继承下来的实例变量)
    long instance_size                                       OBJC2_UNAVAILABLE;
    // 该类的成员变量地址列表
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    // 方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    // 缓存最近使用的方法地址,用于提升效率;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    // 存储该类声明遵守的协议的列表
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

此结构体存放类的元数据,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为Objective-C对象。结构体里还有个变量叫做super_class,它定义了本类的父类。类对象所属的类型是另外一个类,叫做“元类”,用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个“类对象”仅有一个与之相关的“元类”

super_class指针确立了继承管理,而isa指针描述了实例所属的类,我们可以查出对象是否能响应某个选择子,是否遵从某项协议,并且能看出来此对象位于“类继承体系”的哪一部分。

可以用类型信息查询方法来检视类继承体系,“isMemberOfClass”能够判断出对象是否为某个特定类的实例,而“isKindOfClass”能够判断出对象是否为某类或其派生类的实例:

    NSMutableDictionary *dict = [NSMutableDictionary new];
    [dict isMemberOfClass:[NSMutableDictionary class]];//YES
    [dict isMemberOfClass:[NSDictionary class]];//NO
    [dict isKindOfClass:[NSDictionary class]];//YES
    [dict isKindOfClass:[NSArray class]];//NO

类对象是单例,在应用程序范围内,每个类的Class仅有一个实例,也就是说另外一种可以精确判断对象是否为某类实例的办法是:

if ([object class] == [SomeClass class]){

}

尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能

推荐阅读更多精彩内容