[iOS开发]Objective-C对象模型

一、isa指针

Objective-C 是一门面向对象的编程语言,每一个对象都是一个类的实例。在 Objective-C 语言的内部,每一个对象都有一个名为 isa 的指针,指向该对象的类。每一个类描述了一系列它的实例的特点,包括成员变量的列表、成员函数的列表等。每一个对象都可以接收消息,而对象能够接收的消息列表保存在它所对应的类中。
在Xcode中按快捷键 Shift+Command +o,然后输入“NSObject.h”,可以打开NSObject的定义头文件,通过头文件我们可以看到,NSObject 就是一个包含 isa 指针的结构体:

@interface NSObject <NSObject> {
    
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

接下来看下objc.h

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

/// Represents an instance of a class.
struct objc_object {
    
    
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

按照面向对象语言的设计原则,所有事物都应该是对象(严格来说 Objective-C并没有完全做到这一点,因为它有像 intdouble 这样的简单变量类型,而类似 Ruby一类的语言,连int变量也是对象)。在 Objective-C 语言中,每一个类实际上也是一个对象。每一个类也有一个名为isa 的指针。每一个类也可以接收消息,例如代码 [NSObject alloc],就是向 NSObject这个类发送名为“alloc”的消息。苹果将runtime代码开源了,可以打开 Class 的定义头文件,通过头文件我们可以看到,Class 也是一个包含isa指针的结构体,如下(除了isa外还有其他成员变量,但那是为了兼容非2.0版的Objective-C的遗留逻辑,大家可以忽略它)。

struct objc_class {
    
    
    Class isa;

#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 *` */
typedef struct objc_class *Class;

因为类也是一个对象,所以它也必须是另一个类的实列,这个类就是元类(metaclass)。元类保存了类方法的列表。当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,这样可以一直找到继承链的头。
元类也是一个对象,那么元类的isa指针又指向哪里呢?为了设计上的完整,所有的元类的 isa指针都会指向一个根元类(rootmetaclass)。根元类本身的isa指针指向自己,这样就形成了一个闭环。上面提到,一个对象能够接收的消息列表是保存在它所对应的类中的。在实际编程中,我们几乎不会遇到向元类发消息的情况,那它的isa指针在实际上很少用到。不过这么设计保证了面向对象概念在Objective-C语言中的完整,即语言中的所有事物都是对象,都有isa指针。
我们再来看看继承关系,由于类方法的定义是保存在元类中,而方法调用的规则是,如果该类没有一个方法的实现,则向它的父类继续查找。所以,为了保保证父类的类方法在子类中可以被调用,所有子类的元类都会继承父类的元类,换而言之,类类对象和元类对象有着同样的继承关系。
下面看一张图来理解:
请添加图片描述
我们可以从图中看出:

  1. NSObject的类中定义了实例方法,例如-(id)init方法和-(void)dealloc 方法。
  2. NSObject的元类中定义了类方法,例如+(id)alloc 方法、+(void)load方法和+(void)initialize 方法。
  3. NSObject的元类继承自 NSObject 类,所以 NSObject 类是所有类的根,因此 NSObject 中定义的实例方法可以被所有对象调用,例如-(id)init方法和-(void)dealloc 方法。
  4. NSObject的元类的isa指向自己。

博主前面写过一片浅学元类的文章,大家可以浅看一下,多多支持:浅学元类

二、类的成员变量

如果把类的实例看成一个C语言的结构体(struct),上面说的isa指针就是这个结构体的第一个成员变量,而类的其他成员变量依次排列在结构体中。排列顺序如下图所示。
在这里插入图片描述
我们看一段代码:

#import <Foundation/Foundation.h>

@interface Father : NSObject {
    
    
    int _father;
}
@end

@implementation Father

@end

@interface Child : Father {
    
    
    int _child;
}
@end

@implementation Child

@end

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        // insert code here...
        Child *child = [[Child alloc] init];
        
    }
    return 0;
}

在初始化后面打断点,输入p *child,看到输出:

(lldb) p *child
(Child) $0 = {
    
    
  Father = {
    
    
    NSObject = {
    
    
      isa = Child
    }
    _father = 0
  }
  _child = 0
}
(lldb) 

可以看到,和上面说的一致。
因为对象在内存中的排布可以看成一个结松的体,该结构体的大小并不能动态变化,所以无法在运行时动态地给对象增加成员变量。
相应地,对象的方法定义都保存在类的可变区域中。Objective-C 2.0并未在头文件中将实现暴露出来,但在 Objective-C 1.0 中,我们可以看到方法的定义列表是一个名为methodLists的指针的指针:

struct objc_method_list **methodLists

通过修改该指针指向的指针的值,就可以动态地为某一个类增加成员方法。这也是Category实现的原理 同时也说明了为什么Category只可为对象增加成员方法,却不能增加成员变量(通过objc_setAssociatedobjectobjc_getAssociatedObject方法可以变相地给对象增加成员变量,但由于实现机制不一样,所以并不是真真正改变了对象的内存结构)。
因为 isa 本身也只是一个指针,所以除了对象象的方法可以动态地修改外,我们也可以在运行时动态地修改 isa 指针的值,达到替换对象整整个行为的目的,不过该应用场景较少。

三、对象模型的应用

(一)动态创建对象

我们可以使用Objective-C语言提供的与runtime相关的函数, 动态地创建一个新的类,并且 通过相关的方法来获得isa指针的值,从而了解对象的内部结构。
我们先来看动态创建类的代码:

void ReportFunction(id self, SEL _cmd);

int main(int argc, const char * argv[]) {
    
    
    @autoreleasepool {
    
    
        //创建一个MyCustomClass类,是NSObject子类
        Class newClass = objc_allocateClassPair([NSObject class], "MyCustomClass", 0);
        //为类添加一个report方法
        class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
        //注册该类
        objc_registerClassPair(newClass);
        
        //创建一个实例
        id instanceOfNewClass = [[newClass alloc] init];
        //调用report方法
        [instanceOfNewClass performSelector:@selector(report)];
        
    }
    return 0;
}

void ReportFunction(id self, SEL _cmd) {
    
    
    NSLog(@"This object is %p.", self);
    NSLog(@"Class is %@, and super is %@.", [self class], self);
    Class currentClass = [self class];
    for (int i = 1; i < 5; ++i) {
    
    
        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = object_getClass(currentClass);
    }
    NSLog(@"NSObject's class is %p", [NSObject class]);
    NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
}

该代码的关键之处主要有以下几点:

  1. import rumtime相关的头文件:objc/runtime.h
  2. 使用objc_allocateClassPair 方法创建新的类。
  3. 使用class_addMethod方法来给类增加新的方法。
  4. 使用 objc_registerClassPair 来注册新的类。
  5. 使用 object_getClass 方法来获得对象的 isa 指针所指向的对象。

最终,我们得到的运行结果如下:
在这里插入图片描述
将上面的运行结果中的内存地址与对应的类画在了下面的图中,从中可以清楚地看到:

  1. MyCustomClass对象开始,在我们连续读取了3次isa 指针所指向的对象后,isa指针所指向的地址变成了0x1e8b081a0,也就是我们说的NSObject 元类的地址。之后我们第 4次取 isa 指针所指的对象时,其结果仍然为 0x1e8b081a0,这说明 NSObject 元类的isa指针确实是指向它自己的。
  2. 作为对比,我们在代码最后获取了 NSObject 类的isa 指针地址,我们可以看到其值都是0x1e8b081a0,这说明所有的元类对象的isa指针,都是指向 NSObject 元类的。
    在这里插入图片描述

(二)系统相关API及应用

isa swizzling的应用

系统提供的KVO的实现,就利用了动态地修改isa指针的值的技术。在苹果的文档中可以看到如下描述:

Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
自动键值观察是使用一种称为 isa-swizzling 的技术实现的。
顾名思义,isa 指针指向维护调度表的对象类。此调度表实质上包含指向类实现的方法的指针以及其他数据。
当观察者为对象的属性注册时,被观察对象的 isa 指针将被修改,指向中间类而不是 true 类。因此,isa 指针的值不一定反映实例的实际类。
永远不要依赖 isa 指针来确定类成员身份。相反,您应该使用 class 方法来确定对象实例的类。

Method Swizzling API 说明

Objective-C 提供了以下 API来动态替换类方法或实例方法的实现:

  • class_replaceMethod替换类方法的定义。
  • method_exchangeImplementations交换两个方法的实现。
  • method_setImplementation设置一个方法的实现。

这三个方法有一些细微的差别,给大家介绍如下:

  • class_replaceMethod在苹果的文档中能看到,它有两种不同的行为。
    在这里插入图片描述
    在这里插入图片描述
    当类中没有想替换的原方法时,该方法会调用class_addMethod来为该类增加一个新方法,也正因为如此,class_replaceMethod在调用时需要传入 types参数,而 method_exchangeImplementationsmethod_setImplementation 却不需要。
  • method_exchangeImplementations的内部实现其实是调用了两次method_setImplementation方法,从苹果的文档中能清晰地了解到,如下所示。
    在这里插入图片描述
    在这里插入图片描述

从以上的区别我们可以总结出这三个API的使更用场景:

  • class_replaceMethod,当需要替换的方法去有可能不存在时,可以考虑使用该方法。
  • method_exchangeImplementations,当需要交换两个方法的实现时使用。
  • method_setImplementation是最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。

猜你喜欢

转载自blog.csdn.net/weixin_52192405/article/details/124783850