runtime中元类的理解

参考文章

清晰理解Objective-C元类
object_getClass(obj)与[obj class]的区别-源代码解析
刨根问底Objective-C Runtime

元类

这几天看了一些runtime底层的一些介绍,感受最深的就是对元类的讲解了。突然有种恍然大悟的感觉,以前我们经常说一切皆对象,在这里体现出来了。

1. 元类是什么

众所周知Objective-C(以下简称OC)中的消息机制,消息的接收者可以是一个对象,也可以是一个类。那么这两种情况统一为一种情况不是更方便吗?苹果当然早就想到了,这也正是元类的用处。苹果统一把消息接收者作为对象。等等,这也是说类也是对象?yes,就是这样。就是说,OC中所有的类都是一种对象。由一个类实例化来的对象叫实例对象,这好理解,那么类作为对象(称之为类对象),又是什么类的对象呢?当然也容易猜到,就是元类(MetaClass)。现在到给元类下定义的时候了:元类就是类对象所属的类。所以,实例是类的实例,类作为对象又是元类的实例。已经说了,OC中所有的类都是一种对象,所以元类也是对象,那么元类是什么的实例呢?答曰:根元类,同时根元类是其自身的实例。

上面讲到了实例对象、类对象、元类对象,有什么区别?
实例对象:当我们在代码中new一个实例对象时,拷贝了实例所属的类的成员变量,但不拷贝类定义的方法,调用实例方法时,调用实例的isa指针去寻找方法对应的函数指针。
类对象:是一个功能完整的对象,特殊之处在于它们是由程序员定义而在运行时由编译器创建的,它没有自己的实例变量(这里区别于类的成员变量,它们是属于实例对象的,而不是属于类对象的,类方法是属于类对象自己的),但类对象中存着成员变量和实例方法列表。
元类对象:OC的类方法是使用元类的根本原因,因为其中存储着对应的类对象调用的方法即类方法。其他时候都倾向于隐藏元类,因此真实世界没有人发送消息给元类对象。元类的定义和创建看起来都是编译器自动完成的,无须人为干涉。要获取一个类的元类,可使用如下定义的函数:

Class objc_getMetaClass(const char *name); // name为类的名字

此外还有一个获取对象所属的类的函数:

Class object_getClass(id obj);

由于类对象是元类的实例,所以当传入的参数为类名时,返回的就是指向该类所属元类的指针。

2. 元类的构建机制

既然所有的类都是对象,那么元类又是什么类的对象?这样下去,不是子子孙孙无穷尽也?当然不行,OC作为一门编程语言,当然要满足完备性----既定的各条规则都要满足,不能相互矛盾,也不能存在漏洞。
OC作为运行时语言,上面提到的类与元类在运行时都是objc_class类型。在Objective-C2.0中,objc_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 *` */

其中,Class定义如下:

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

可以看出,OC中每个类都包含一个isa变量,显然这里的isa是指向另一个类的指针,说白了就是表面这个类是哪个类的实例,以便找到代码中调用的本类或父类的类方法。对于NSObject及其子类,指向的就是它的元类,正如实例中也有个isa指针指向其所属的类一样。而对于元类,每个元类的isa指针都指向根元类。那么根元类的isa指向哪里?---它自己。这样就构成了一个封闭的循环,实现了无懈可击的OC类系统。这种关系在下面的图中有清晰的体现。

除了isa声明了实例与所属类的关系,还有super_class声明了类、元类的继承关系。每个类对象都有对应的元类,每个类(根类除外)都有一个superclass,同样每个元类也有一个superclass,并且子类与子元类、父类与父元类分别在同一个层次。这种关系借用网上的一张图来说明,一目了然。



注意:根元类的superclass不是nil而是根类。对于OC原生的类,根元类的父类就是系统的根类NSObject。但根类不一定是NSObject,因为后面介绍的objc_allocateClassPair函数也可以创建出一个根类。

3. 元类的构建机制

上面讲到了OC运行时类的定义,这里可以看看对象的定义,重点关注objc_object 和 id。

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

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

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

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

这说明OC中每个对象也都包含一个isa变量,这里的isa,指向实例对象所属的类。在运行时,[obj aMessage];被转化为objc_msgSend(obj, @selector(aMessage));,这里,@selector(aMessage)返回一个SEL数据类型,即方法选择器。SEL主要作用是快速的通过方法名字(aMessage)查找到对应方法的函数指针,然后调用其函数。SEL其本身是一个int型的地址,地址中存放着方法的名字。在一个类中,每一个方法对应着一个SEL。iOS类中不能存在两个名称相同的方法,即使参数类型不同,因为SEL是根据方法名生成,相同的方法名称只能对应一个SEL。

当一个消息发送给任何一个对象,方法的检查器从对象的isa指针开始,然后是父类。具体地,在objc_msgSend函数中,首先通过obj的isa指针找到obj对应的class。在class中,有一块最近调用的方法的指针缓存,所以先去cache通过selector查找对应的method,若cache中未找到,再去method list中查找,若method list中未找到,则去superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。对最后一句不了解的话,请看objc_method定义:

struct objc_method { 
  SEL method_name; // 方法名称 
  const char *method_typesE; // 参数和返回类型的描述字串 
  IMP method_imp; // 方法的具体的实现的指针 
}

由上看出,OC中实例方法是通过isa找到object所属的class,再在class中找到要调用的method。此可得出结论:class即类对象中存储着实例方法。实际上,类对象中存储着类定义的一切:成员变量、属性列表、遵守的协议等,但不包括类方法。类方法的定义在哪?就是在元类里面。此外元类中还存在着类的信息(类的版本,名字),比如发送一个类消息[class aMessage];,class中的isa就指向class的元类,在元类中搜索调用的类方法,搜索层次类似于实例方法的搜索。

4. 元类的应用

类对象和元类对象的相关方法:

  1. object_getClass跟随实例的isa指针,返回此实例所属的类,对于实例对象(instance)返回的是类(class),对于类(class)则返回的是元类(metaclass);
  2. -class方法对于实例对象(instance)会返回类(class),但对于类(class)则不会返回元类(metaclass),而只会返回类本身,即[@"instance" class]返回的是__NSCFConstantString,而[NSString class]返回的是NSString。
  3. class_isMetaClass可判断某类是否为元类。
  4. 使用objc_allocateClassPair可在运行时创建新的类与元类对,使用class_addMethod和class_addIvar可向类中增加方法和实例变量,最后使用objc_registerClassPair注册后,就可以使用此类了。这体现了OC作为运行时语言的强大之一:在代码中动态创建类并添加方法。
Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(addedMethod), (IMP)added_method_implementation, "v@:");
void added_method_implementation(id self, SEL __cmd)
{
    // do something
}

说明:objc_allocateClassPair函数的作用是创建一个新类newClass及其元类,三个参数依次为newClass的父类,newClass的名称,第三个参数通常为0。然后可向newClass中添加变量及方法,注意若要添加类方法,需用objc_getClass(newClass)获取元类,然后向元类中添加类方法。接下来必须把newClass注册到运行时系统,否则系统不能识别这个类。

  1. 根据以上理解比较一下object_getClass(obj)和[obj class]的区别
    其实很简单,直接看源代码吧。
    object_getClass(obj)的代码实现:
Class object_getClass(id obj)
{
    return _object_getClass(obj);
}

其中_object_getClass(obj)是一个静态内联函数,代码实现如下:

static inline Class _object_getClass(id obj)
{
    #if SUPPORT_TAGGED_POINTERS
    if (OBJ_IS_TAGGED_PTR(obj)){
        uint8_t slotNumber = ((uint8_t)(uint64_t) obj) & 0x0F;
        Class isa = _objc_tagged_isa_table[slotNumber];
        return isa;
    }
    #endif
        if (obj) return obj->isa;
        else return Nil;
}

简单的说_object_getClass函数就是返回对象的isa指针。
[obj class]的代码实现分为两种情况,分别是obj为实例对象和类对象,代码如下所示:

// 类方法直接返回自身指针
+ (Class)class
{
   return self;
 }
// 实例方法调用object_getClass,返回isa指针
- (Class)class 
{
    return object_getClass(self);
}

通过以上代码可以看出,调用[obj class],不管obj对实例对象还是类对象,结果都是一样的。

  1. 看下这几个个面试题吧



    第一题解析如下:
    在调用[self class]时,会转化为objc_msgSend函数。函数定义如下:

id objc_msgSend(id self, SEL op, ...)

我们把self做为第一个参数传递进去。而在调用[super class]时,会转化为objc_msgSendSuper函数。看下函数定义:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一个参数是objc_super这样一个结构体,其定义如下:

struct objc_super {
    __unsafe_unretained id receiver;
    __unsafe_unretained Class super_class;  
};

结构体有两个成员,第一个成员是receiver,类似于上面的objc_msgSend函数第一个参数self。第二个成员是记录当前类的父类是什么。
所以当调用[self class]时,实际先调用的是objc_msgSend函数,第一个参数是Son当前的这个实例,然后在Son这个类里面去找-(Class)class这个方法,没有就去父类Father里找,也没有,最后在NSObject类中发现这个方法。而-(Class)class的实现就是返回self的类别,故上述输出结果为Son。
objc Runtime开源代码对- (Class)class方法的实现:

- (Class)class {
    return object_getClass(self);
}

而当调用[super class]时,会转换为objc_msgSendSuper函数。第一步先构造objc_super结构体,结构体第一个成员就是self,第二个成员是(id)class_getSuperclass(objc_getClass("son")),实际该函数输出结果为Father。第二部是去Father这个类里去找- (Class)class,没有,然后去NSObject类去找。找到了,最后内部是使用objc_msgSend(objc_super->receiver, @selector(class))去调用,此时已经和[self class]调用相同,故上述输出结果仍然返回Son。
第二题解析如下:
运行结果是
2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0
对于
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject calss]];
首先还是看看isKindOfClass的实现:

- (BOOL)isKindOfClass: aClass
{
    Class cls;
    for (cls = isa; cls; cls = cls->superclass) 
        if (cls == (Class)aClass)
            return YES;
    return NO;
}

当[NSObject class]对象第一次进行比较的时候,得到它的isa为NSObject的Meta Class,这个时候NSObject Meta Class 和 NSObject Class不相等。然后取出NSObject的Meta Class的Super class,这个时候又变成了NSObject Class,所以相等。
对于
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject calss]];
先看看isMemberOfClass:的实现吧,

- (BOOL)isMemberOf:aClass
{
    return isa == (Class)aClass;
}

当前的isa指向NSObject的Meta Class,所以和NSObject Class不相等。所以输出结果为NO。
第三题解析如下:
结果是输出
2017-07-12 10:20:51.067 test[1038:39336] IMP: - [NSObject(Sark) foo] 2017-07-12 10:20:51.068 test[1038:39336] IMP: - [NSObject(Sark) foo]
注意这里有点蹊跷的是如果将这个分类写的一个文件import进来会编译不过的,但是如果直接写在.m文件,像下面这样,就是好的,可以编译过。

#import "ViewController.h"
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo
{
    NSLog(@"IMP: - [NSObject(Sark) foo]");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [NSObject foo];
    [[NSObject new] foo];
}
@end

1)objc runtime加载完后,NSObject的Sark Category被加载。而NSObject的Sark Category的头文件+ (void)foo并没有实质参与到工作中,只是给编译器进行静态检查,所以我们编译上述代码会出现警告,提示我们没有实现+ (void)foo方法。而在代码编译中,它已经被注释掉了。
2)实际被加入到Class的method list的方法是- (void)foo,它是一个实例方法,所以加入到当前对象NSObject的方法列表中,而不是NSObject Meta class的方法列表中。
3)当执行[NSObject foo]时,我们看下整个objc_msgSend的过程:
objc_msgSend第一个参数是"(id)objc_getClass("NSObject")",获得NSObject Class的对象。
类方法在Meta Class的方法列表中找,我们在load Category方法时加入的是- (void)foo实例方法,所以并不在NSObject Meta Class的方法列表中,继续往super class中找,NSObject Meta Class的super class是NSObject本身,所以,这个时候我们能够找到 - (void)foo这个方法。所以输出结果。
当执行[[NSObject new] foo],我们看下整个objc_msgSend的过程:
[NSObject new]生成一个NSObject对象,直接在该对象的类(NSObject)的方法列表里找,能够找到,所以正常输出结果。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • Objective-C语言是一门动态语言,他将很多静态语言在编译和链接时期做的事情放到了运行时来处理。这种动态语言...
    tigger丨阅读 1,330评论 0 8
  • 原文出处:南峰子的技术博客 Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了...
    _烩面_阅读 1,192评论 1 5
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,109评论 0 7
  • Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的...
    有一种再见叫青春阅读 552评论 0 3