美文网首页iOS之runtimeiOS开发技巧iOS Developer
objcRuntime黑魔法(class_addMethod,c

objcRuntime黑魔法(class_addMethod,c

作者: zhiyi | 来源:发表于2015-12-30 12:45 被阅读1755次

    欢迎转载,请注明出处:
    http://zyden.vicp.cc/about-objcruntime/
    谢谢

    前言:
    陈列一下今天要讲的知识点:class_addMethod,class_replaceMethod,method_getImplementation,object_getClass

    涉及到的知识
    --使用category,通过Runtime实现用自己的函数调换掉原生函数
    --oc的message forwarding
    --使用Runtime为类添加原来没有的方法
    --为什么category里不重写方法

    注明:
    本文章内技术参考当然来自四面八方,来自不同时期,小弟只是做个总结,有不好的地方欢迎大家指导

    先从一个场景问题带出吧,毕业设计的时候小弟做ipad应用,到后面才决定加上旋转屏适配,看着100多个文件20多个页面差点没把血吐出来,哈哈每个controller去修改方法是不可能的了,因为强迫症也不想多创个父类,好吧决定一次过替换掉这些controller里的viewWillAppear: 和 willAnimateRotationToInterfaceOrientation:duration:,换成自己的。




    先看一个category
    通过运用class_addMethodclass_replaceMethod来调换掉系统库里的方法

    #import "NSObject+Swizzle.h"
    
    @implementation NSObject (Swizzle)
    
    + (BOOL)swizzleMethod:(SEL)origSel withMethod:(SEL)aftSel {
    
        Method originMethod = class_getInstanceMethod(self, origSel);
        Method newMethod = class_getInstanceMethod(self, aftSel);
        
        if(originMethod && newMethod) {//必须两个Method都要拿到
            if(class_addMethod(self, origSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
                //实现成功添加后
                class_replaceMethod(self, aftSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
            }
            return YES;
        }
        return NO;
    }
    @end
    

    1.传入两个参数,原方法选择子,新方法选择子,并通过class_getInstanceMethod()拿到对应的Method


    2.class_addMethod,是相对于实现来的说的,将本来不存在于被操作的Class里的newMethod的实现添加在被操作的Class里,并使用origSel作为其选择子(注意参数中的self为被操作的Class,不要忘了这里是类方法).


    3.class_replaceMethod,addMethod成功完成后,从参数可以看出,目的是换掉method_getImplaementation(roiginMethod)的选择子,将原方法的实现的SEL换成新方法的SEL:aftSel,ok目的达成了。想一想,现在通过旧方法SEL来调用,就会实现新方法的IMP,通过新方法的SEL来调用,就会实现旧方法的IMP,好了理一理思路继续往下。




    这次就用NSString做载体来演示吧:

    #import "MyString.h"
    #import "NSObject+Swizzle.h"
    
    @implementation MyString
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class clazz = object_getClass((id)self);
            [clazz swizzleMethod:@selector(resolveInstanceMethod:) withMethod:@selector(myResolveInstanceMethod:)];
        });
    }
    
    + (BOOL)myResolveInstanceMethod:(SEL)sel {
        
        if(! [self myResolveInstanceMethod:sel]) {
            NSString *selString = NSStringFromSelector(sel);
            if([selString isEqualToString:@"countAll"] || [selString isEqualToString:@"pushViewController"]) {
                class_addMethod(self, sel, class_getMethodImplementation(self, @selector(dynamicMethodIMP)), "v@:");
                return YES;
            }else {
                return NO;
            }
        }
        return YES;
    }
    
    - (void)dynamicMethodIMP {
        NSLog(@"我是动态加入的函数");
    }
    
    @end
    

    1.首先这里要提下resolveInstanceMethod:,不了解的朋友可以去补一下oc的message forwarding,就是当运行时对象调用了一个找不到的方法的时候系统会去寻找的机制,这个方法是第一步去到的地方,我们可以在这里面runtime添加方法,是的,首先我们得劫持了这个方法,做我们自己的事,通过刚才category里封装好的swizzleMethod:withMethod:
    -------这个时候有朋友有疑问了,我们可以重写这个方法来做自己的事情啊,其实并不可以,在category里重写现有方法会有警告#Category is implementing a method which will also be implemented by its primary class,这种做法是不提倡的!
    ------------category没有办法去代替子类,它不能像子类一样通过super去调用父类的方法实现。如果category中重写覆盖了当前类中的某个方法,那么这个当前类中的原始方法实现,将永远不会被执行,这在某些方法里是致命的(这里提一下一个特例+(void)load,它会在当前方法里执行完再去category里执行).
    ------------如果两个category重写了同一个方法,我们无法控制哪个优先级更高,一直以来还是提倡通过继承去重写方法


    2.object_getClass拿到当前MyString的Class,调用刚才category里封装好的swizzleMethod:withMethod:,用我们自己的myResolveInstanceMethod:去替换原生的,好了,现在如果我们在运行时调用了一个不存在的方法,系统会去调用我们的myResolveInstanceMethod:,是的不用怀疑。


    3.现在看看myResolveInstanceMethod:里面又调用了一次myResolveInstanceMethod:,有的朋友会以为是递归其实并不是,系统去调用原生的方法,会跑到我们自己的方法实现,是因为我们之前的swizzle操作没问题,而不要忘记了,我们自己的方法selector对应的实现,已经换成了原生方法的实现,ok。。if(! [self myResolveInstanceMethod:sel])是调用原生方法的实现,去检测一次传入的方法是否存在,如果还是没有,则做class_addMethod操作为此类添加对应的方法,return YES,该方法被系统调用,OK,达到目的。


    class_addMethod参数的意义

    class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
    
    class_addMethod(self, sel, class_getMethodImplementation(self, @selector(dynamicMethodIMP)), "v@:");
    

    按顺序是,类--选择子--实现--方法的返回值和参数资料。
    v代表返回值void,@代表id类型对象,:代表选择子。
    why? 其实每一个oc方法都有两个隐式的参数(id self, SEL _cmd),也可以说是由C语言函数再加着两个参数组成一个oc方法。

    最后看看我们的工作的收获:

    NSLog(@"begin test");
    //------------------------------------------------
        
        MyString *string = [[MyString alloc] init];
        [string performSelector:@selector(countAll)];
        [string performSelector:@selector(pushViewController)];
    <pre name="code" class="objc">
    //------------------------------------------------
        NSLog(@"finish test");
    
    -----Log:
    2015-10-29 18:12:56.815 ObjcRuntimeDemo[8875:563683] begin test
    2015-10-29 18:12:56.815 ObjcRuntimeDemo[8875:563683] 我是动态加入的函数
    2015-10-29 18:12:56.815 ObjcRuntimeDemo[8875:563683] 我是动态加入的函数
    2015-10-29 18:12:56.815 ObjcRuntimeDemo[8875:563683] 我是动态加入的函数
    2015-10-29 18:12:56.815 ObjcRuntimeDemo[8875:563683] finish test
    

    相关文章

      网友评论

      • ba57726c2921:能够将将class_addMethod最后一个参数方法的返回值和参数资料具体怎么写吗?谢谢
      • Raybon_lee:是不是可以这么理解,需要动态操作那个类的方法时候就添加一个类别呢,load会执行多少次呢,比如你写的nsobject,是不是每个类都会执行这个方法,这一点存在疑惑,我最近也在用这个,写的不错,load说的是在应用启动之前执行,那么启动之后是每个类调用完自己的load方法再去调用分类的load方法么,可以这么理解吧,还有个initial那个方法
        zhiyi:@Raybon_lee 是哪个方法被调用?
        Raybon_lee:@zhiyi 那个你写的nsobjectNike 以试一下写好放在工程就会掉用,貌似还不需要倒入头文件,我查了一些资料,这个执行在main前面
        zhiyi:@Raybon_lee 理论上每个class的+load方法只会被调用一次(在加载类的时候),我在load里用GCD的方法调用-swizzleMethod再一次保证其只会被调用一次。

        而+initial的话,是在该类被实例化出对象的时候才被调用。

        有一个要注意的地方就是一般不要在category里写+load方法,系统会先调用类里面本身的+load,而后又会去调用category里的load

        而"NSObject+Swizzle.h"这个category我写成NSObject的category是为了所有的类都适用,都可以通过import这个category来 "主动" 调用里面的方法。

        希望能解答到你的问题,谢谢朋友捧场

      本文标题:objcRuntime黑魔法(class_addMethod,c

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