Objective-C类之关系

时间:2021-01-01 03:53:35

1、NSObject是所有类的根类

我们知道,Objective-C是面向对象的语言,不论你使用任何类,比如NSString、UIView、NSWindowController、UIViewController、NSViewController……,也就是不论是基于macOS的Cocoa类库,还是基于iOS的Cocoa Touch类库,还是Fundation库,它们所有的类都会指向NSObject这个根类(root class),如同道家所说的一生二,二生三,三生万物,这个NSObject就是一,所有类的起源。同时,根类/父类拥有的特性也会由子类继承下去。

比如iOS中的UIButton,类的继承关系如下:

Objective-C类之关系


更详细的类继承关系,参看下图。

先来看Fundation库,Fundation是支撑iOS和macOS的基础库,其中蓝色部分的是iOS才支持的,macOS全部支持(本来iOS就是从Mac OSX改过来的,OSX现在更名为macOS)

Objective-C类之关系

Objective-C类之关系

Objective-C类之关系


iOS UIKit的类继承图如下:

Objective-C类之关系


2、NSProxy也是一个根类

当然,OC里还有一个根类,就是NSProxy,有且仅有这两个,但是这个NSProxy很少使用,尤其是在iOS的Cocoa Touch中从未使用过,官方描述:

Note: The Foundation framework defines another root class, NSProxy, but this class is rarely used in Cocoa applications and never in Cocoa Touch applications.

NSProxy不是本文重点,暂不细说,有兴趣的可以看官方文档。


3、判断类的关系

判断一个类的实例是不是某类的子类或就是某类,使用isKindOfClass。

假如类B继承于类A,如下:

Objective-C类之关系

B *objB = [[B alloc] init];

BOOL rev = [objB inKindOfClass:[A class]]; //判断实例对象objB是否是类A的子类或就是类A
//如果rev=YES则表示实例对象objB是类A的子类或类A

BOOL rev = [objB isMemberOfClass:[A class]]; //判断实例对象objB是否是类A,结果rev=NO
BOOL rev = [objB isMemberOfClass:[B class]]; //判断实例对象objB是否是类B,结果rev=YES

//isSubclassOfClass等同于inKindOfClass,是判断一个类是否是某类的子类或就是某类,但它是类方法,适用于类间判断
BOOL rev = [B isSubclassOfClass:[A class]]; //判断类B是否是类A的子类或就是类A,结果rev=YES
BOOL rev = [B isSubclassOfClass:[B class]]; //判断类B是否是类A的子类或就是类A,结果rev=YES
BOOL rev = [A isSubclassOfClass:[B class]]; //判断类A是否是类B的子类或就是类A,结果rev=NO


  A(类A) 结果
B(类B) [B isSubclassOfClass:[A class]]; YES
objB(类B的实例) [objB inKindOfClass:[A class]]; YES
objB(类B的实例) [objB isMemberOfClass:[A class]]; NO
     

4、类和元类

要讲类和元类,就得先说OC的动态性、消息机制、以及类的结构、类的isa指针、方法列表等。


我们知道,Objective-C是一门面向对象动态语言,动态是最重要也是其区别于其他面向对象语言最有特色的一面,与C++/JAVA这些静态编译语言不同的是,OC的函数/方法不是在静态编译阶段就确定地址的,而是在运行时(Runtime)通过消息机制(objc_msgSend)来实时确定并调用的。

比如,平常OC开发中,调用类A实例clsA的一个方法,如[clsA doSomething]; 其实会被runtime转换为objc_msgSend(receiver, selector)进行执行,即objc_msgSend([clsA class], @selector(doSomething))。

如果有参数,则是objc_msgSend(receiver, selector, arg1, arg2, ...)。

也就是说,对一个方法的调用,其实就是runtime执行消息发送,那runtime怎么知道发给谁,那个目标方法又在哪呢?这就需要我们了解下Objective-C中一个类是怎么定义的,类结构怎样的。

我们在任意一个类上按cmd键+鼠标左键点击进去,就可以看到这个类的定义,如果我们不断这样溯源这个类的父类一直到根类,也就是前面我们说的NSObject,我们可以看到如下定义:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
再点Class进去看Class的定义,其实就是C结构的别名:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY; //指向类或元类地址

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE; //指向父类地址
    const char *name                                         OBJC2_UNAVAILABLE; //类名
    long version                                             OBJC2_UNAVAILABLE; //类版本
    long info                                                OBJC2_UNAVAILABLE; //信息
    long instance_size                                       OBJC2_UNAVAILABLE; //实例的大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE; //成员变量列表
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE; //方法列表
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE; //缓存
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE; //协议列表
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

由定义我们知道几点:

  1. 每个Objective-C的类都有一个Class isa的成员变量,这个isa指向其类或它的元类地址;
  2. 如果这个类有父类,则super_class就会记录父类的地址;
  3. 每个类都会在objc_ivar_list中记录其所有成员变量的地址;
  4. 每个类都会在objc_method_list中记录其所有方法的地址;
  5. 每个类都会在objc_protocol_list中记录其所有协议的地址;

那么,元类是什么,元类就是每一个我们看得见的类的背后如影随形的一个类,它会记录这个类的所有的类方法(即静态方法)等一些内容。我们平时创建一个类,其实同时也会创建这个元类,只是这个元类是工作于runtime底层的,对上层应用开发者来说,不用管也不用操心。

实例对象、类、元类、根类之间的isa指向关系如下:

Objective-C类之关系

小结:

  • isa指向顺序是:实例变量isa --> 类--> 类的元类 --> NSObject的元类
  • 任何一个类的元类的isa都会指向根类NSObject的元类。
  • 根类NSObject的isa指向自己。表示到头了。

5、子类、父类、根类、元类

子类、父类、根类、元类之间是什么样的关系呢?再加上一个类的实例变量,貌似这一大家子够热闹的,怎么扯清他们之间的关系呢,我们写个代码例子来看看。

新建3个类,Test类,Test2类,Test3类,其继承关系为:NSObject --> Test类--> Test2类 --> Test3类

@interface Test : NSObject
@interface Test2 : Test
@interface Test3 : Test2

引入头文件:

#import <objc/runtime.h>
#import "Test.h"
#import "Test2.h"
#import "Test3.h"

写下如下测试方法:

- (void)testClassRelation {

    // Test
    NSLog(@"--------- Test -----------");
    Test *intanceT = [[Test alloc] init];
    Class a_currentClass = object_getClass(intanceT);
    for (int i=1; i<=4; i++) {
        BOOL isMetaClass = class_isMetaClass(a_currentClass);
        NSLog(@"Test类isa指针第%d次是%p, 类名是%@, 是否是元类=%d, 父类指针是%p", i, a_currentClass, NSStringFromClass(a_currentClass), isMetaClass, [a_currentClass superclass]);
        a_currentClass = object_getClass(a_currentClass);
    }
    
    // Test2
    NSLog(@"--------- Test2 -----------");
    Test2 *intanceT2 = [[Test2 alloc] init];
    Class b_currentClass = object_getClass(intanceT2);
    for (int i=1; i<=4; i++) {
        BOOL isMetaClass = class_isMetaClass(b_currentClass);
        NSLog(@"Test2类isa指针第%d次是%p, 类名是%@, 是否是元类=%d, 父类指针是%p", i, b_currentClass, NSStringFromClass(b_currentClass), isMetaClass, [b_currentClass superclass]);
        b_currentClass = object_getClass(b_currentClass);
    }
    
    // Test3
    NSLog(@"--------- Test3 -----------");
    Test3 *intanceT3 = [[Test3 alloc] init];
    Class c_currentClass = object_getClass(intanceT3);
    for (int i=1; i<=4; i++) {
        BOOL isMetaClass = class_isMetaClass(c_currentClass);
        NSLog(@"Test3类isa指针第%d次是%p, 类名是%@, 是否是元类=%d, 父类指针是%p", i, c_currentClass, NSStringFromClass(c_currentClass), isMetaClass, [c_currentClass superclass]);
        c_currentClass = object_getClass(c_currentClass);
    }
    
    // NSObject
    NSLog(@"--------- NSObject -----------");
    NSLog(@"NSObject类的指针是%p", [NSObject class]);
    NSLog(@"NSObject元类的指针是%p", object_getClass([NSObject class]));

}

运行代码后,执行结果如下:

--------- Test -----------
Test类isa指针第1次是0x10477d6e0, 类名是Test, 是否是元类=0, 父类指针是0x104fd9170
Test类isa指针第2次是0x10477d6b8, 类名是Test, 是否是元类=1, 父类指针是0x104fd9198
Test类isa指针第3次是0x104fd9198, 类名是NSObject, 是否是元类=1, 父类指针是0x104fd9170
Test类isa指针第4次是0x104fd9198, 类名是NSObject, 是否是元类=1, 父类指针是0x104fd9170
--------- Test2 -----------
Test2类isa指针第1次是0x10477d7d0, 类名是Test2, 是否是元类=0, 父类指针是0x10477d6e0
Test2类isa指针第2次是0x10477d7a8, 类名是Test2, 是否是元类=1, 父类指针是0x10477d6b8
Test2类isa指针第3次是0x104fd9198, 类名是NSObject, 是否是元类=1, 父类指针是0x104fd9170
Test2类isa指针第4次是0x104fd9198, 类名是NSObject, 是否是元类=1, 父类指针是0x104fd9170
--------- Test3 -----------
Test3类isa指针第1次是0x10477d780, 类名是Test3, 是否是元类=0, 父类指针是0x10477d7d0
Test3类isa指针第2次是0x10477d758, 类名是Test3, 是否是元类=1, 父类指针是0x10477d7a8
Test3类isa指针第3次是0x104fd9198, 类名是NSObject, 是否是元类=1, 父类指针是0x104fd9170
Test3类isa指针第4次是0x104fd9198, 类名是NSObject, 是否是元类=1, 父类指针是0x104fd9170
--------- NSObject -----------
NSObject类的指针是0x104fd9170
NSObject元类的指针是0x104fd9198

在面向对象的Objective-C语言中,类的实例变量是对象(实例对象);类也是对象(类对象);类的元类也是对象(元类对象)。
从打印结果和比对指针地址来看,我们可以看到:

  • Test1 / Test2 / Test3 各自的实例变量的isa指针第1次都指向各自的类对象的地址,即
    intanceT指向了Test类对象的地址0x10477d6e0;intanceT2指向了Test2类对象的地址0x10477d7d0;intanceT3指向了Test3类对象的地址0x10477d780。
  • Test1 / Test2 / Test3 各自的实例变量的isa指针第2次都指向各自的类对象的元类地址,即
    intanceT指向了Test类的元类对象地址0x10477d6b8;intanceT2指向了Test2类的元类对象的地址0x10477d7a8;intanceT3指向了Test3类元类对象的地址0x10477d758。
  • Test1 / Test2 / Test3 各自的实例变量的isa指针第3次都指向了NSObject的元类对象地址,即
    intanceT、intanceT2、intanceT3第3次都指向了NSObject元类对象的地址0x104fd9198。
  • Test1 / Test2 / Test3 各自的实例变量的isa指针第4次重复了第3次的结果,如果循环7次、8次、100次,从第4次开始都是和第3次结果是一样的,说明isa指针从第3次开始,即到了NSObject元类后就指向了自己,进入闭环。

也就是说不管一个类继承了多少次,和根类NSObject隔了多少层,纵是千秋万代,但最后一个X代孙子的isa到祖爷爷(根类NSObject的)的距离始终只有3层,也就是:

X类的实例对象 --> X类 --> X类的元类 --> NSObject类的元类
  第1层   第2层   第3层  

元类isa的关系只有3层,但是正常我们看见的类与父类的关系,继承了多少层那还是多少层,祖宗十八代族谱上还是一个接一个的有数的,从上面打印结果上也可以看到Test3父类指针指向的是Test2,Test2父类指针指向的是Test,Test父类指针指向的是NSObject。


归纳总结下后,根据这个例子画个图,就更清晰了:

Objective-C类之关系


把上面例子的图简化下,就是如下的图:

Objective-C类之关系


6、总结

不罗嗦,简单概括本文就是:


NSObject是类之根本,全靠它来开枝散叶;

元类如同鬼魅丽影,但却实如一人;

类之关系判断有三板斧:isSubclassOfClass,inKindOfClass,isMemberOfClass;

乾坤挪移、斗转星移,大法还是runtime好。


其实Objective-C类之关系远远不止这些,因为很多内容相关联,目前只是沧海一粟,有时间总结出其他内容再分享出来。