通过OC运行时解析对象、类、元类之间的关系

运行时是OC底层的一套C语言的API,编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。

在OC中类本身被称为类对象,简称类。而类对象的实例被称为实例对象,简称对象。
注意:类对象和类的对象是两个东西。

OC中NSObject的定义

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

...

大家都很熟悉NSObject。它遵守了<NSObject>协议,定义了一些属性和方法外,还有一个Class类型的isa指针。
除了Class外,还有一些idSELMethodIMP等见不到底层结构的类型。但是在运行时中可以找到OC与C之间的桥梁。

typedef struct objc_object *id;

typedef struct objc_class *Class;

typedef struct objc_selector *SEL;

typedef struct objc_method *Method;

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

运行时中对象的定义

#include <objc/objc.h>中,对象的定义为:

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

结构体类型的对象中只有一个Class类型的isa指针。可见与OC中NSObject的定义十分类似。而Class定义为objc_class类型的结构体指针:

//一个代表OC类的不透明类型
typedef struct objc_class *Class;

相当于给指向类objc_class的指针起了个别名为Class。那objc_class又是什么呢?

运行时中类的定义

#import <objc/runtime.h>中类的定义为objc_class类型的结构体:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    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;
/* Use `Class` instead of `struct objc_class *` */

在该结构体中又有一个Class类型的指针。

所以对象的isa指针指向类对象。类对象中Class类型的isa指针指向了和类本身一样类型的类对象。即指向了元类。而元类中的isa指针指向根元类,而根元类中的isa指针指向他自己。

关系图

在类中除了有isa指针之外,还有

  • Class _Nullable super_class //指向父类的指针。
  • const char * _Nonnull name //类名
  • long version //版本
  • long info //信息
  • long instance_size //对象在内存中的大小
  • struct objc_ivar_list * _Nullable ivars //类中成员变量列表
  • struct objc_method_list * _Nullable * _Nullable methodLists //类中方法列表
  • struct objc_cache * _Nonnull cache //缓存
  • struct objc_protocol_list * _Nullable protocols //类所遵循的协议列表

运行时中方法的定义

//定义了objc_method类型的结构体指针Method
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}

可见Method由方法名、方法类型、方法实现这三个部分组成。
而只要知道了方法的名称,和方法所属的类就可以获得该方法的指针。
方法的类型编码规则可参考官方文档
通过以下方法分别获取实例方法和类方法。

Method class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

Method class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);

如何获取类对象

在OC中不支持直接获取isa指针。

获取isa指针

实际上有三种方法可以获取:

//1.OC方法
Class a = [self class];

//2. object_getClass(id _Nullable obj, Class _Nonnull cls)
//传入实例对象
Class b = object_getClass(self);

//3.objc_getClass(const char * _Nonnull name)
//将OC字符串转为C字符串
const char *name = [[[self class] description] UTF8String];
id c = objc_getClass(name);

NSLog(@"类对象地址:%p  %p  %p",a,b,c);

这三种方法得到的是相同的类对象。

如何获取元类对象

//1.object_setClass(id _Nullable obj, Class _Nonnull cls)
Person *p = [[Person alloc] init];
Class a = object_getClass([p class]);

//2.object_setClass(id _Nullable obj, Class _Nonnull cls)
Class b = object_getClass([Person class]); 

//3.objc_getMetaClass(const char * _Nonnull name)
Class c = objc_getMetaClass("Person");

NSLog(@"元类对象地址:%p  %p %p",a,b,c);

方法一和方法二实际上是同一种方式。

object_getClass和objc_getClass有什么区别

- (void)demo1
{
    id currentClass = self;
    const char *a = object_getClassName(currentClass);
    for (int i = 1; i < 7; i++) {
        NSLog(@"Following the isa pointer %d times gives 1 %p---%s", i, currentClass,a);
        currentClass = object_getClass(currentClass);
        a = object_getClassName(currentClass);
    }
}

- (void)demo2
{
    id currentClass = [[Person alloc] init];
    const char *a = object_getClassName(currentClass);
    for (int i = 1; i < 7; i++) {
        NSLog(@"Following the isa pointer %d times gives 2 %p---%s", i, currentClass,a);
        currentClass = object_getClass(currentClass);
        a = object_getClassName(currentClass);
    }
}

- (void)demo3
{
    Class currentClass = [self class];
    const char *a = object_getClassName(currentClass);
    for (int i = 1; i < 5; i++) {
        NSLog(@"Following the isa pointer %d times gives 3 %p---%s", i, currentClass,a);
        currentClass = objc_getClass([NSStringFromClass(currentClass) UTF8String]);
        a = object_getClassName(currentClass);
    }
}
Following the isa pointer 1 times gives 1 0x7fcf61e07060---ViewController
Following the isa pointer 2 times gives 1 0x10a0be1f0---ViewController
Following the isa pointer 3 times gives 1 0x10a0be218---NSObject
Following the isa pointer 4 times gives 1 0x10b068e58---NSObject
Following the isa pointer 5 times gives 1 0x10b068e58---NSObject
Following the isa pointer 6 times gives 1 0x10b068e58---NSObject

Following the isa pointer 1 times gives 2 0x60000020dff0---Person
Following the isa pointer 2 times gives 2 0x10a0be268---Person
Following the isa pointer 3 times gives 2 0x10a0be240---NSObject
Following the isa pointer 4 times gives 2 0x10b068e58---NSObject
Following the isa pointer 5 times gives 2 0x10b068e58---NSObject
Following the isa pointer 6 times gives 2 0x10b068e58---NSObject

Following the isa pointer 1 times gives 3 0x10a0be1f0---ViewController
Following the isa pointer 2 times gives 3 0x10a0be1f0---ViewController
Following the isa pointer 3 times gives 3 0x10a0be1f0---ViewController
Following the isa pointer 4 times gives 3 0x10a0be1f0---ViewController

对比一下不难发现:

在demo1中传入self对象,
第一次打印了实例对象0x7fcf61e07060
第二次打印了类对象0x10a0be1f0
第三次打印了元类对象0x10a0be218,
第四次及以后打印了根元类对象0x10b068e58,因为根元类的isa指针指向它自己。

在demo2中同理,可见第四次及以后打印的根元类对象和demo1中的地址一样,由此可证所有的对象指向同一个根元类对象。

在demo3中,四次打印的都是0x10a0be1f0,和demo1中的类对象地址相同。

得出以下结论:

  • object_getClass返回的是传入对象的isa指针所指向的类。
  • objc_getClass只能返回类对象。
  • objc_getMetaClass只能返回元类对象。

再奉上源码,与我们推测的是一样的。

/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

/***********************************************************************
* objc_getClass.  Return the id of the named class.  If the class does
* not exist, call _objc_classLoader and then objc_classHandler, either of 
* which may create a new class.
* Warning: doesn't work if aClassName is the name of a posed-for class's isa!
**********************************************************************/
Class objc_getClass(const char *aClassName)
{
    if (!aClassName) return Nil;

    // NO unconnected, YES class handler
    return look_up_class(aClassName, NO, YES);
}

/***********************************************************************
* objc_getMetaClass.  Return the id of the meta class the named class.
* Warning: doesn't work if aClassName is the name of a posed-for class's isa!
**********************************************************************/
Class objc_getMetaClass(const char *aClassName)
{
    Class cls;

    if (!aClassName) return Nil;

    cls = objc_getClass (aClassName);
    if (!cls)
    {
        _objc_inform ("class `%s' not linked into application", aClassName);
        return Nil;
    }

    return cls->ISA();
}

通过运行时如何HOOK

+ (void)load
{
    SEL sel_classMethod = @selector(classMethod);
    SEL sel_hook_classMethod = @selector(hook_classMethod);
    
    Class metaClass_obj = object_getClass(self);//元类对象
//    Class class_obj = objc_getClass([[[self class] description] UTF8String]);//类对象
    
    Method m_classMethod = class_getClassMethod(metaClass_obj, sel_classMethod);
    Method m_hook_classMethod = class_getClassMethod(metaClass_obj, sel_hook_classMethod);
    
    BOOL isAdd = class_addMethod(metaClass_obj,
                                 sel_classMethod,
                                 method_getImplementation(m_hook_classMethod),
                                 method_getTypeEncoding(m_hook_classMethod));
    if (isAdd) {
        class_replaceMethod(metaClass_obj,
                            sel_hook_classMethod,
                            method_getImplementation(m_classMethod),
                            method_getTypeEncoding(m_classMethod));
    }else{
        method_exchangeImplementations(m_classMethod, m_hook_classMethod);
    }
}

值得注意的点:

  • class_addMethod的参数。有四个参数,后三个参数拼起来正好是Method的结构定义。如果方法已存在,会返回false
  • 在类方法中self代表类对象,在实例方法中self代表实例对象。object_getClass(self)同样的写法在类方法中和在实例方法中得到的结果不一样。
  • hook类方法需使用class_getClassMethod,参数传入元类对象。
  • hook实例方法需使用class_getInstanceMethod,参数传入类对象。

总结:

对象方法列表是保存在类中的,如果想给类添加一个对象方法,是将该方法添加到类的方法列表中。
类方法列表是保存在元类中的,如果想要给类添加一个类方法,是将该方法添加到元类的方法列表中。

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