美文网首页
学习Runtime动态方法解析碰到的问题

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

作者: wilsonhan | 来源:发表于2018-06-27 20:26 被阅读0次

    关于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]返回的指针不同

    相关文章

      网友评论

          本文标题:学习Runtime动态方法解析碰到的问题

          本文链接:https://www.haomeiwen.com/subject/ygviyftx.html