iOS 的实例对象、类、元类

级别:★★☆☆☆
标签:「实例对象」「类」「元类」「实例对象、类、元类」
作者: ITWYW
审校: QiShare团队

前言
笔者最近看了部分 实例对象、类、元类 相关的内容。会在本文中做下分享。

首先我们看下实例对象、类、元类的定义。

类、元类的定义

元类:在面向对象程序设计中,元类(英语:metaclass)是一种实例是的类。普通的类定义的是特定对象的行为,元类定义的则是特定的类及其对象的行为。
引自 360百科 元类

类:类(英语:class)在面向对象编程中是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。 引自维基百科 类

The metaclass is the description of the class object, just like the class is the description of ordinary instances.
引自[objc explain]: Classes and metaclasses

简单来说:

类是实例对象的描述,元类是类对象的描述。
举个例子 NSObject *obj = [NSObject new];
NSObject 是 实例对象 obj 的描述,NSObject 的元类是 NSObject 类对象的描述。

实例对象、类、元类之间的关系

我们看一下实例对象、类、元类之间的关系图。

instance_class_metaClass

图片引自[objc explain]: Classes and metaclasses

根据上图,我们可以梳理一下实例对象、类、元类之间的关系。

一、实例对象、类对象、元类的 isa 指向及父类指向说明

下边笔者说明下上图中 isa 和父类指向。

  1. 实例对象的 isa 指针指向类对象;
  2. 类对象的 isa 指针指向元类对象;
  3. 元类对象的 isa 指针指向根元类(NSObject元类);
  4. 根元类(NSObject元类)的 isa 指针指向自己;
  1. 子类的元类的父类指向父类的元类;
  2. NSObject的父类为nil;
  3. 根元类(NSObject元类)的父类指向NSObject。
0. 下文会用到的函数说明
// 注:使用如下函数需要在自己的文件中引入runtime头文件 即 #import <objc/runtime.h>

// 如果obj为实例对象返回obj对应的类对象,如果obj为类对象返回obj对应的元类对象,如果obj为元类对象,返回obj的元类对象。
OBJC_EXPORT Class _Nullable object_getClass(id _Nullable obj) ;

// 返回指定类名name的元类
OBJC_EXPORT Class _Nullable objc_getMetaClass(const char * _Nonnull name);

// 查看指定类是否为元类
OBJC_EXPORT BOOL class_isMetaClass(Class _Nullable cls);

// 返回指定类的父类
OBJC_EXPORT Class _Nullable class_getSuperclass(Class _Nullable cls) ;

我们可以使用代码验证一下实例对象、类、元类关系图的正确性。
为了验证对象、类对象、元类的isa指向及父类指向,笔者创建了 2 个类,分别是 QiSuperClass、QiSubClass。
QiSuperClass 继承自 NSObject,QiSubClass 继承自 QiSuperClass。

笔者写了如下示例代码做验证。

//! 验证实例对象、类对象、元类对象的isa及父类指向
- (void)instanceClassMetaClassISASuperClicked {
    
    QiSubClass *subClassInstance = [QiSubClass new];
    // 类对象
    Class subClass = object_getClass(subClassInstance);
    // 元类对象
    Class subMetaClass = object_getClass(subClass);
    // 元类对象的元类对象
    Class subMetaClassMetaClass = object_getClass(subMetaClass);
    
    // 1. 实例对象的 isa 指针指向类对象
    QiLog(@"----1. 实例对象的 isa 指针指向类对象----");
    QiLog(@"QiSubClass的类对象地址:%p,QiSubClass类对象地址:%p", subClass, [QiSubClass class]);
    
    // 2. 类对象的 isa 指针指向元类对象;
    QiLog(@"----2. 类对象的 isa 指针指向元类对象;----");
    QiLog(@"QiSubClass元类对象地址:%p,QiSubClass元类对象地址:%p", subMetaClass, objc_getMetaClass("QiSubClass"));
    
    // 3. 元类对象的 isa 指针指向根元类(NSObject元类);
    QiLog(@"----3. 元类对象的 isa 指针指向根元类(NSObject元类)----");
    QiLog(@"QiSubClass元类对象的元类对象地址:%p,根元类(NSObject元类)的对象地址:%p", subMetaClassMetaClass, objc_getMetaClass("NSObject"));
    
    // 4. 根元类(NSObject元类)的 isa 指针指向自己;
    QiLog(@"----4. 根元类(NSObject元类)的 isa 指针指向自己----");
    QiLog(@"根元类(NSObject元类)的对象地址:%p,根元类(NSObject元类)的对象的isa指针地址:%p", objc_getMetaClass("NSObject"), object_getClass(objc_getMetaClass("NSObject")));
    
    // 1. 子类的元类的父类指向父类的元类;
    QiLog(@"----1. 子类的元类的父类指向父类的元类----");
    QiLog(@"QiSubClass的元类的父类对象地址:%p,QiSuperClass的元类地址:%p", class_getSuperclass(objc_getMetaClass("QiSubClass")), objc_getMetaClass("QiSuperClass"));
    
    // 2. NSObject的父类为nil;
    QiLog(@"----2. NSObject的父类为nil----");
    QiLog(@"NSObject的父类对象:%@", class_getSuperclass([NSObject class]));
    
    // 3. 根元类(NSObject元类)的父类指向NSObject。
    QiLog(@"----3. 根元类(NSObject元类)的父类指向NSObject----");
    QiLog(@"根元类(NSObject元类)的父类地址:%p,NSObject类地址:%p", class_getSuperclass(objc_getMetaClass("NSObject")), [NSObject class]);
}

笔者整理后的输出结果如下:

----1. 实例对象的 isa 指针指向类对象----
QiSubClass的类对象地址:0x10dad86c0,QiSubClass类对象地址:0x10dad86c0

----2. 类对象的 isa 指针指向元类对象;----
QiSubClass元类对象地址:0x10dad8698,QiSubClass元类对象地址:0x10dad8698

----3. 元类对象的 isa 指针指向根元类(NSObject元类)----
QiSubClass元类对象的元类对象地址:0x7fff86cb8638,根元类(NSObject元类)的对象地址:0x7fff86cb8638
    
----4. 根元类(NSObject元类)的 isa 指针指向自己----
根元类(NSObject元类)的对象地址:0x7fff86cb8638,根元类(NSObject元类)的对象的isa指针地址:0x7fff86cb8638
    
----1. 子类的元类的父类指向父类的元类----
QiSubClass的元类的父类对象地址:0x10dad8508,QiSuperClass的元类地址:0x10dad8508
    
----2. NSObject的父类为nil----
NSObject的父类对象:(null)
    
----3. 根元类(NSObject元类)的父类指向NSObject----
根元类(NSObject元类)的父类地址:0x7fff86cb8660,NSObject类地址:0x7fff86cb8660

上文中的代码在一起查看可能会有些乱,下边笔者使用LLDB输出对象的地址。来分别查看实例对象、类对象、元类对象之间的关系。

1. 实例对象的 isa 指针指向类对象;

查看 QiSubClass 的类对象地址都是 0x0000000104173580。

(lldb) p/x [QiSubClass class]
(Class) $1 = 0x0000000104173580 QiSubClass

(lldb) p/x object_getClass([QiSubClass new])
(Class _Nullable) $2 = 0x0000000104173580 QiSubClass
2. 类对象的 isa 指针指向元类对象;

查看 QiSubClass 的元类对象地址都是 0x0000000104173558。

(lldb) p/x object_getClass([QiSubClass class])
(Class _Nullable) $3 = 0x0000000104173558
(lldb) p/x objc_getMetaClass("QiSubClass")
(Class _Nullable) $4 = 0x0000000104173558
3. 元类对象的 isa 指针指向根元类(NSObject元类);

查看 QiSubClass 的元类对象的元类对象的地址和NSObject的元类对象的地址都是 0x00007fff86cb8638。

(lldb) p/x object_getClass(objc_getMetaClass("QiSubClass"))
(Class _Nullable) $5 = 0x00007fff86cb8638
(lldb) p/x objc_getMetaClass("NSObject")
(Class _Nullable) $6 = 0x00007fff86cb8638
4. 根元类(NSObject元类)的 isa 指针指向自己;

查看 NSObject的元类对象的地址 和 NSObject 的元类对象的元类对象的地址都是 0x00007fff86cb8638。

(lldb) p/x objc_getMetaClass("NSObject")
(Class _Nullable) $6 = 0x00007fff86cb8638
(lldb) p/x object_getClass(objc_getMetaClass("NSObject"))
(Class _Nullable) $7 = 0x00007fff86cb8638
1. 子类的元类的父类指向父类的元类;

查看 QiSubClass 的元类的父类对象的地址 和 QiSuperClass 的元类对象的地址都是 0x00000001041733c8。

(lldb) p/x class_getSuperclass(objc_getMetaClass("QiSubClass"))
(Class _Nullable) $11 = 0x00000001041733c8
(lldb) p/x objc_getMetaClass("QiSuperClass")
(Class _Nullable) $12 = 0x00000001041733c8
2. NSObject的父类为nil;

查看 NSObject 的父类对象为nil。

(lldb) po class_getSuperclass([NSObject class])
nil
3. 根元类(NSObject元类)的父类指向NSObject。

查看 NSObject 元类的父类对象的地址 和 NSObject 类对象的地址都是 0x00007fff86cb8660。

(lldb) p/x class_getSuperclass(objc_getMetaClass("NSObject"))
(Class _Nullable) $16 = 0x00007fff86cb8660 NSObject
(lldb) p/x [NSObject class]
(Class) $17 = 0x00007fff86cb8660 NSObject

二、修改类对象、元类对象的ISA指向

下边笔者使用的如下的QiSuperClass、QiSubClass的代码,进行修改ISA指向的操作。

@interface QiSuperClass : NSObject
    
- (void)superMethod;
+ (void)superClassMethod;

@end

NS_ASSUME_NONNULL_END
#import "QiSuperClass.h"

@implementation QiSuperClass

- (void)superMethod {
    
    NSLog(@"superMethod");
}

+ (void)superClassMethod {
    
    NSLog(@"SuperClassMethod");
}

@end

NS_ASSUME_NONNULL_BEGIN

@interface QiSubClass : QiSuperClass

- (void)subMethod;
+ (void)subClassMethod;

@end

NS_ASSUME_NONNULL_END
#import "QiSubClass.h"

@implementation QiSubClass

- (void)subMethod {
    
    NSLog(@"subMethod");
}

+ (void)subClassMethod {
    
    NSLog(@"subClassMethod");
}

@end

推测一下如下代码的执行结果

    QiSuperClass *superCls = [QiSuperClass new];
    [superCls superMethod];
    QiLog(@"superCls对象当前类型:%@", superCls.class);

    [((QiSubClass *)superCls) subMethod];
    [QiSuperClass performSelector:@selector(subClassMethod)];

测试一下,会发现上述代码会出现崩溃,因为 superCls 是 父类 QiSuperClass 的实例对象,superCls 没有子类QiSubClass 的实例方法 subMethod。同样父类 QiSuperClass 没有子类 QiSubClass 的类方法。进一步来说大家如果查看过类的方法列表、元类的方法列表就会了解其中原因(其中原因,笔者会在后续文章中做说明)。

推测一下如下代码的执行结果

    QiSuperClass *superCls = [QiSuperClass new];
    [superCls superMethod];
    
    QiLog(@"superCls对象当前类型:%@", superCls.class);
    // Sets the class of an object. 设置一个对象的类
    object_setClass(superCls, QiSubClass.class);
    QiLog(@"superCls对象当前类型:%@", superCls.class);
    [((QiSubClass *)superCls) subMethod];
    
    QiLog(@"QiSuperClass的元类对象类型:%@", objc_getMetaClass("QiSuperClass"));
    object_setClass(QiSuperClass.class, objc_getMetaClass("QiSubClass"));
    QiLog(@"QiSuperClass的元类对象类型:%@", objc_getMetaClass("QiSuperClass"));
    
    [QiSuperClass performSelector:@selector(subClassMethod)];

输出内容如下:

superMethod
superCls对象当前类型:QiSuperClass
superCls对象当前类型:QiSubClass
subMethod
QiSuperClass的元类对象地址:0x10d6e3a10
QiSuperClass的元类对象地址:0x10d6e3ba0
QiSubClass的元类对象地址:0x10d6e3ba0
subClassMethod

昨天同事大成小栈 问到笔者对 object_setClass 的应用场景。笔者自己平时项目中没有使用过 object_setClass。(大家如果项目中有 object_setClass 的应用场景,欢迎评论)。笔者说下个人想法,KVO的实现过程中用到了 object_setClass。大家如果看过KVO的部分实现,可能会了解到当我们给监听指定类的后,系统会为指定的类动态创建一个监听类的子类,并且修改监听类对象的isa指针到新创建的子类。笔者用如下代码做个初步示意。以后的文章会再做更多介绍。

@interface QiRuntimeTableViewController ()

@property (nonatomic, copy) NSString *observeString;

@end

调用如下代码

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"self class:%@", [self class]);
    NSLog(@"object_getClass:%@", object_getClass(self));
    [self addObserver:self forKeyPath:@"observeString" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"self class:%@", [self class]);
    NSLog(@"object_getClass:%@", object_getClass(self));
}

上述代码的结果为:

self class:QiRuntimeTableViewController
object_getClass:QiRuntimeTableViewController

self class:QiRuntimeTableViewController
object_getClass:NSKVONotifying_QiRuntimeTableViewController

查看结果就会发现实例对象 self 的 isa 指针发生了改变。指向了运行时新创建的类 NSKVONotifying_QiRuntimeTableViewController,笔者认为这一步就是调用了 object_setClass(self, NSClassFromString(@"NSKVONotifying_QiRuntimeTableViewController"));

笔者就不在这里放置object_setClass的源码了,笔者在下方贴出了object_getClass 的相关源码,如有兴趣,大家可继续查看或下载runtime源码查看详情。
runtime源码下载地址:https://opensource.apple.com/tarballs/objc4/

三、object_getClass 相关源码

1. object_getClass 源码
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

object_getClass 的源码的字面意思表明该函数用于获取指定对象的 isa 指针。具体的调用细节源码如下。

2. objc_class 结构体源码

如下代码表明 Class 类型是相当于是 objc_class *类型。

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

objc_class 结构体源码如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }

    void setInfo(uint32_t set) {
        ASSERT(isFuture()  ||  isRealized());
        data()->setFlags(set);
    }

    void clearInfo(uint32_t clear) {
        ASSERT(isFuture()  ||  isRealized());
        data()->clearFlags(clear);
    }
    // 省略若干代码.....
}
3. objc_object 结构体源码
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
    Class rawISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    
    uintptr_t isaBits() const;
    // 更多其他代码...
}
4. getIsa() 源码
#if SUPPORT_TAGGED_POINTERS

inline Class 
objc_object::getIsa() 
{
    if (fastpath(!isTaggedPointer())) return ISA();

    extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    uintptr_t slot, ptr = (uintptr_t)this;
    Class cls;

    slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
    cls = objc_tag_classes[slot];
    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}
5. ISA() 源码
#if SUPPORT_NONPOINTER_ISA

inline Class 
objc_object::ISA() 
{
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

参考学习网址

[objc explain]: Classes and metaclasses
What is a meta-class in Objective-C?
Friday Q&A 2010-11-6: Creating Classes at Runtime in Objective-C
Objective-C Runtime 运行时之一:类与对象
Objc Runtime 总结
神经病院 Objective-C Runtime 入院第一天—— isa 和 Class

推荐文章

Flutter中的RenderObjectElement与RenderObjectWidget
Flutter中的StatelessWidget及其生命周期
Flutter中的Widget
Flutter中的Element(下篇)
Flutter中的Element(上篇)
iOS 解决 [NSURL fileURLWithPath:] 对 # 的编码问题
Xcode 调整导航目录字体大小b
Swift 5.1 (21) - 泛型
Swift 5.1 (20) - 协议
Swift 5.1 (19) - 扩展
Swift 5.1 (18) - 嵌套类型
浅谈编译过程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容