学习Runtime动态方法解析碰到的问题

关于SEL和IMP

在学习动态方法解析中遇到的问题

void missingClassPrint()
{
    NSLog(@"调用了missingClassPrint函数");
}

@implementation TestClass
    
+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"调用resolveClassMethod!!!");
    if(sel == @selector(classPrint)) {
        //这里为什么是(IMP)missingClassPrint,如果使用@selector(missingClassPrint)则会导致运行出错
        class_addMethod(object_getClass(self), sel, (IMP)missingClassPrint, "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

@end

其中(IMP)missingClassPrint处直接将函数名强制转换成IMP指针,而不是使用@selector不理解,这里有两个概念需要理解SEL和IMP

SEL

SEL是selector在objc中的表示类型。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL,在objc.h中的定义如下:

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

SEL的本质是映射到方法的C字符串,可以用objc编译器命令@selector()或者Runtime的sel_registerName函数来获得一个SEL类型的方法选择器。

不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型。

IMP

IMP在objc.h中的定义是

typedef void (*IMP)(void /* id, SEL, ... */ );

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。

你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 idSEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 idSEL 参数就能确定唯一的方法实现地址;反之亦然。

对上述问题的解释

上面开始没有注意到传参,class_addMethod的第三个传参的类型是IMP,理所应当需要传入一个IMP类型的参数,IMP本质上就是一个函数指针,上面的例子里定义的missingClassPrint函数是一个C类型函数,其名字便是一个函数指针,所以只要强制转换成IMP类型就可以了。

而SEL是方法的ID,IMP是方法的实现,在OC中,如果想要得到一个类中的方法的IMP,则需要这样来调用

//获取类的方法实现
class_getMethodImplementation([self class], @selector(实例或者类方法名)), "v@:");

第一个参数传入的是类的类型,第二个参数是传入方法的选择器,第三个参数则是types,描述该方法的返回值和传参。这个函数的返回值是IMP类型。

参考文章

Objective-C Runtime

关于[self class]和object_getClass(self)

问题还是出现在使用动态解析时class_addMethod方法传参的问题,这次是第一个传参Class,以下是正确的代码,这段代码是没有问题的:

@implementation TestClass
    
+ (BOOL)resolveClassMethod:(SEL)sel
{
    if(sel == @selector(classPrint)) {
        //第一个传参Class引发的疑问
        class_addMethod(object_getClass(self), sel, (IMP)missingClassPrint, "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if(sel == @selector(print:)) {
        class_addMethod([self class], sel, (IMP)missingPrint, "I@:I");//其中v@:表示方法的参数和返回值,详见Type Encodings
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

其中,resolveClassMethod方法和另一个方法resolveInstanceMethod分别用来为类添加类方法和实例方法,他们都是类方法。

但是这里发现两个方法中,有不一样的传参object_getClass(self)和[self class],于是我把resolveClassMethod中的object_getClass(self)修改为[self class],然后就出现了问题。

在resolveInstanceMethod中

在resolveInstanceMethod方法中使用class_addMethod函数为类添加方法时,第一个参数传入的是[self class],[self class]返回的是类对象,其中存储着该类的实例方法列表,所以这里class_addMethod函数将函数指针加入到[self class]返回的类对象中,最后可以通过实例对象成功调用实例方法。

在resolveClassMethod中

在resolveClassMethod方法中同样使用class_addMethod函数为类添加方法,这里我们需要为类添加一个类方法,不是实例方法,当改成这样的时候就出现了错误。

class_addMethod([self class], sel, (IMP)missingClassPrint, "v@:");

程序在运行期抛出了unrecognized selector sent to class ...的异常,这表明没有在该类中找到调用的方法,也就是添加失败了。经过调试,发现这句代码是运行成功了的,但是在调用classPrint类方法的过程中,没有找到该方法,所以问题就出现在[self class]和object_getClass(self)的区别上,来看看它俩的区别。

[self class]和object_getClass(self)

我们知道在Runtime中,Class的结构体如下,这是一个旧版的类的结构体,在OC 2.0中已经不使用,但基本思路是一样的:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    //其余省略
    ...                                                      
#endif

} OBJC2_UNAVAILABLE;

一个实例对象中包含两个指针,isa和superClass,isa指向自己的类对象,superClass指向自己的父类。而类对象本质上也是一个Class结构体,其中也包含isa和superClass指针,isa指向的是元类,superClass指向的是父类对象。

用一张图来表示

Runtime类的关系.png
object_getClass函数的实现
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

object_getClass就是通过isa指针返回传入对象的类:

  • 若obj为实例对象,返回的必然是类对象,object_getClass(self)得到的是类对象,
  • 若obj为类对象,返回的是元类对象,object_getClass(object_getClass(self))得到的是元类对象,
  • 若obj为元类对象,返回的是根元类对象,object_getClass(object_getClass(object_getClass(self)))得到的是根元类对象,
  • 若obj为根元类对象,返回自身。

在上面的图中可以看到这条isa指针链,一直从实例对象,延伸到

class方法的实现

class的方法有两个,分别是类方法和实例方法:

  • 类方法很容易理解,调用类方法的是类对象,返回自身即返回了类,
  • 实例方法调用了object_getClass函数,该函数返回self的类对象。
+ (Class)class {
    return self;
}

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

在实例方法中,self是当前实例对象,若调用[self class]方法,则调用的是当前类的实例方法,返回的是类对象。此时对返回的结果继续调用class,如[[self class] class],则是继续对类对象调用class方法,调用的必然是类方法class而不是实例方法class,返回的结果就是类对象。

回到上面的问题

首先放上出错的代码

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if(sel == @selector(classPrint)) {
        //错误的传参 [self class]
        class_addMethod([self class], sel, (IMP)missingClassPrint, "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if(sel == @selector(print:)) {
        //与resolveClassMethod中相同的传参但是正确
        class_addMethod([self class], sel, (IMP)missingPrint, "I@:I");//其中v@:表示方法的参数和返回值,详见Type Encodings
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

在resolveClassMethod方法里,目的是为了给TestClass类添加一个类方法,类方法由TestClass的元类记录,所以这里需要传给class_addMethod函数的第一个参数是TestClass类的元类,而不是TestClass的类对象。

根据上文中说明的class方法和object_getClass函数的区别,在resolveClassMethod这个类方法中,self表示的是TestClass的类对象:

  • 调用[self class]返回的是类对象本身,也就是self
  • 调用object_getClass(self)函数,返回的是self->isa,也就是TestClass这个类对象的元类

而这里我们需要将类方法动态添加到元类的函数列表中,所以需要传入的参数是object_getClass(self)。

参考文章

为什么object_getClass(obj)与[OBJ class]返回的指针不同

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