iOS~runtime理解

作者: 猩语 | 来源:发表于2015-08-11 22:26 被阅读61981次

    Runtime是想要做好iOS开发,或者说是真正的深刻的掌握OC这门语言所必需理解的东西。最近在学习Runtime,有自己的一些心得,整理如下,
    一为 查阅方便
    二为 或许能给他人一些启发,
    三为 希望得到大家对这篇整理不足之处的一些指点。

    什么是Runtime

    • 我们写的代码在程序运行过程中都会被转化成runtime的C代码执行,例如[target doSomething];会被转化成objc_msgSend(target, @selector(doSomething));
    • OC中一切都被设计成了对象,我们都知道一个类被初始化成一个实例,这个实例是一个对象。实际上一个类本质上也是一个对象,在runtime中用结构体表示。
    • 相关的定义:
    /// 描述类中的一个方法
    typedef struct objc_method *Method;
    
    /// 实例变量
    typedef struct objc_ivar *Ivar;
    
    /// 类别Category
    typedef struct objc_category *Category;
    
    /// 类中声明的属性
    typedef struct objc_property *objc_property_t;
    
    • 类在runtime中的表示
    //类在runtime中的表示
    struct objc_class {
        Class isa;//指针,顾名思义,表示是一个什么,
        //实例的isa指向类对象,类对象的isa指向元类
    
    #if !__OBJC2__
        Class super_class;  //指向父类
        const char *name;  //类名
        long version;
        long info;
        long instance_size
        struct objc_ivar_list *ivars //成员变量列表
        struct objc_method_list **methodLists; //方法列表
        struct objc_cache *cache;//缓存
        //一种优化,调用过的方法存入缓存列表,下次调用先找缓存
        struct objc_protocol_list *protocols //协议列表
        #endif
    } OBJC2_UNAVAILABLE;
    /* Use `Class` instead of `struct objc_class *` */
    

    获取列表

    有时候会有这样的需求,我们需要知道当前类中每个属性的名字(比如字典转模型,字典的Key和模型对象的属性名字不匹配)。
    我们可以通过runtime的一系列方法获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)。

      unsigned int count;
        //获取属性列表
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        for (unsigned int i=0; i<count; i++) {
            const char *propertyName = property_getName(propertyList[i]);
            NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
        }
        
        //获取方法列表
        Method *methodList = class_copyMethodList([self class], &count);
        for (unsigned int i; i<count; i++) {
            Method method = methodList[i];
            NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
        }
        
        //获取成员变量列表
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (unsigned int i; i<count; i++) {
            Ivar myIvar = ivarList[i];
            const char *ivarName = ivar_getName(myIvar);
            NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]);
        }
        
        //获取协议列表
        __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
        for (unsigned int i; i<count; i++) {
            Protocol *myProtocal = protocolList[i];
            const char *protocolName = protocol_getName(myProtocal);
            NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
        }
    

    在Xcode上跑一下看看输出吧,需要给你当前的类写几个属性,成员变量,方法和协议,不然获取的列表是没有东西的。
    注意,调用这些获取列表的方法别忘记导入头文件#import <objc/runtime.h>

    方法调用

    让我们看一下方法调用在运行时的过程(参照前文类在runtime中的表示)

    如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)操作。
    如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中操作。

    1. 首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
    2. 如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
    3. 如果没找到,去父类指针所指向的对象中执行1,2.
    4. 以此类推,如果一直到根类还没找到,转向拦截调用。
    5. 如果没有重写拦截调用的方法,程序报错。

    以上的过程给我带来的启发:

    • 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了。
    • 如果想调用已经重写过的方法的父类的实现,只需使用super这个编译器标识,它会在运行时跳过在当前的类对象中寻找方法的过程。

    拦截调用

    在方法调用中说到了,如果没有找到方法就会转向拦截调用。
    那么什么是拦截调用呢。
    拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。

    + (BOOL)resolveClassMethod:(SEL)sel;
    + (BOOL)resolveInstanceMethod:(SEL)sel;
    //后两个方法需要转发到其他的类处理
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    • 第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
    • 第二个方法和第一个方法相似,只不过处理的是实例方法。
    • 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
    • 第四个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。

    动态添加方法

    重写了拦截调用的方法并且返回了YES,我们要怎么处理呢?
    有一个办法是根据传进来的SEL类型的selector动态添加一个方法。

    首先从外部隐式调用一个不存在的方法:

    //隐式调用方法
    [target performSelector:@selector(resolveAdd:) withObject:@"test"];
    

    然后,在target对象内部重写拦截调用的方法,动态添加方法。

    void runAddMethod(id self, SEL _cmd, NSString *string){
        NSLog(@"add C IMP ", string);
    }
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        
        //给本类动态添加一个方法
        if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
            class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
        }
        return YES;
    }
    

    其中class_addMethod的四个参数分别是:

    1. Class cls 给哪个类添加方法,本例中是self
    2. SEL name 添加的方法,本例中是重写的拦截调用传进来的selector。
    3. IMP imp 方法的实现,C方法的方法实现可以直接获得。如果是OC方法,可以用+ (IMP)instanceMethodForSelector:(SEL)aSelector;获得方法的实现。
    4. "v@:*"方法的签名,代表有一个参数的方法。

    关联对象

    现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。
    这种情况的一般解决办法就是继承。
    但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。
    这个时候,runtime的关联属性就发挥它的作用了。

    //首先定义一个全局变量,用它的地址作为关联对象的key
    static char associatedObjectKey;
    //设置关联对象
    objc_setAssociatedObject(target, &associatedObjectKey, @"添加的字符串属性", OBJC_ASSOCIATION_RETAIN_NONATOMIC); //获取关联对象
    NSString *string = objc_getAssociatedObject(target, &associatedObjectKey);
    NSLog(@"AssociatedObject = %@", string);
    

    objc_setAssociatedObject的四个参数:

    1. id object给谁设置关联对象。
    2. const void *key关联对象唯一的key,获取时会用到。
    3. id value关联对象。
    4. objc_AssociationPolicy关联策略,有以下几种策略:
    enum {
        OBJC_ASSOCIATION_ASSIGN = 0,
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
        OBJC_ASSOCIATION_RETAIN = 01401,
        OBJC_ASSOCIATION_COPY = 01403 
    };
    

    如果你熟悉OC,看名字应该知道这几种策略的意思了吧。

    objc_getAssociatedObject的两个参数。

    1. id object获取谁的关联对象。
    2. const void *key根据这个唯一的key获取关联对象。

    其实,你还可以把添加和获取关联对象的方法写在你需要用到这个功能的类的类别中,方便使用。

    //添加关联对象
    - (void)addAssociatedObject:(id)object{
        objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    //获取关联对象
    - (id)getAssociatedObject{
        return objc_getAssociatedObject(self, _cmd);
    }
    

    注意:这里面我们把getAssociatedObject方法的地址作为唯一的key,_cmd代表当前调用方法的地址。

    方法交换

    方法交换,顾名思义,就是将两个方法的实现交换。例如,将A方法和B方法交换,调用A方法的时候,就会执行B方法中的代码,反之亦然。
    话不多说,这是参考Mattt大神在NSHipster上的文章自己写的代码。

    #import "UIViewController+swizzling.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (swizzling)
    
    //load方法会在类第一次加载的时候被调用
    //调用的时间比较靠前,适合在这个方法里做方法交换
    + (void)load{
        //方法交换应该被保证,在程序中只会执行一次
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            
            //获得viewController的生命周期方法的selector
            SEL systemSel = @selector(viewWillAppear:);
            //自己实现的将要被交换的方法的selector
            SEL swizzSel = @selector(swiz_viewWillAppear:);
            //两个方法的Method
            Method systemMethod = class_getInstanceMethod([self class], systemSel);
            Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
            
            //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
            BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
            if (isAdd) {
                //如果成功,说明类中不存在这个方法的实现
                //将被交换方法的实现替换到这个并不存在的实现
                class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
            }else{
                //否则,交换两个方法的实现
                method_exchangeImplementations(systemMethod, swizzMethod);
            }
            
        });
    }
    
    - (void)swiz_viewWillAppear:(BOOL)animated{
        //这时候调用自己,看起来像是死循环
        //但是其实自己的实现已经被替换了
        [self swiz_viewWillAppear:animated];
        NSLog(@"swizzle");
    }
    
    @end
    

    在一个自己定义的viewController中重写viewWillAppear

    - (void)viewWillAppear:(BOOL)animated{
        [super viewWillAppear:animated];
        NSLog(@"viewWillAppear");
    }
    

    Run起来看看输出吧!

    我的理解:

    • 方法交换对于我来说更像是实现一种思想的最佳技术:AOP面向切面编程。
    • 既然是切面,就一定不要忘记,交换完再调回自己。
    • 一定要保证只交换一次,否则就会很乱。
    • 最后,据说这个技术很危险,谨慎使用。

    相关文章

      网友评论

      • 草原野马:写的很到位,很全,很久了,看一遍在复习一下。
      • 48d063e3b39b:楼主,真心感觉你写的不错,但是我太菜了。感觉还是有些地方不是很理解。可以私下帮着理解一下吗。。。
        草原野马:@我若为女丶骚给你看 哈哈,我是被你的名字,逗笑了。你这是勾引楼主,私下搞基的节奏啊。
        48d063e3b39b:@草原野马 就这么现实么。。。。
        草原野马:你要是女的,楼主肯定愿意私下让你理解透彻:wink:
      • f48ab9e63152:怎么理解 “ 既然是切面,就一定不要忘记,交换完再调回自己。”?
        猩语:@orli_xdx 对的。就是这样
        f48ab9e63152:@兴宇是谁 嗯 其实swiz_viewWillAppear()方法里调用的swiz_viewWillAppear()最终调用的是viewWillAppear
        猩语:@orli_xdx - (void)swiz_viewWillAppear:(BOOL)animated{
        //这时候调用自己,看起来像是死循环
        //但是其实自己的实现已经被替换了
        [self swiz_viewWillAppear:animated];
        NSLog(@"swizzle");
        }
        就是这块代码。首先你的vc中调用了viewWillAppear其实是调的这个方法(因为进行了方法交换)。这时候如果你不调用自己一下的话。你vc里面override的viewWillAppear就不会被调用。所谓切面的意思就是在一个方法执行之前或者之后做一些事情。这是我的理解,不知道说清楚了没:smile:
      • tmachc://后两个方法需要转发到其他的类处理
        这句话有点小问题,NSInvocation中有两个方法
        - (void)invoke;
        - (void)invokeWithTarget:(id)target;
        可以只改方法,不转发到其他类,调用invoke就行。
        猩语:@tmachc 受教了。我在研究一下NSInvocation去哈
      • 荔枝lizhi_iOS程序猿:赞,👍 ,解释的到位。
      • 1cdb19c230be:你好 我想问一下 最后一个方法替换 为什么系统的方法还会走啊
        猩语:@superWWWWWWW 你可以打断点一步步分析下哈。我也不知道你具体的逻辑是啥样的:smile::smile:
      • ba57726c2921:能够讲讲class_addMethod最后一个参数方法的签名具体怎么写吗?谢谢
      • lanshijie:看到评论里有好多人在说交换方法,拦截调用等等有什么用,我的理解是这样的:
        如果学会runtime不是为了装逼,那将毫无意义!!!!
        如果不装逼,那和咸鱼又有什么区别!!!!!!!!
        猩语:@lanshijie 装逼有一万种方法。这个并不是最好的。比如我给你装一个,哦不,是举一个例子:我研究runtime并不是为了在各种实践中用到。而是一定程度的理解这个,可以让我更好的的去理解我工作上的其他东西。
      • 1effe140ac3c:好文 谢作者分享
      • iChuck:使用C 的指针不用的时候应该free掉吧
      • LSRain:你好,博主,可否转载你的这个文章,到我的博客,做为学习之用?
        LSRain:@兴宇是谁 嗯,感谢
        猩语:@蓝楼古月 可以的。标明出处
      • 清蒸鱼跃龙门:拦截调用这个真的用得到吗。。谁写代码不检查下有没有写调用的方法,而且xcode会提示没有这个方法的- -
        方法交换这个,目的是为什么呢?更改方法的内容不是更好么 为了交换还得写更多的东西。。
        清蒸鱼跃龙门:@兴宇是谁 学习了
        张小凡的青云志:@清蒸鱼跃龙门 用处很多,代码节藕,面向协议编程等等,还有作者提到的统计点,统一更改某些方法属性什么的,都用得到!
        猩语:@清蒸鱼跃龙门 特殊需求会用得到啊。比如说方法交换:如果你要在每个VC的viewWillappear里做一些事情(例如统计),这时候你就可以写一个category交换一下uiviewcontroller的viewwillappear方法。从而实现不用每个类里面都写重复的代码。
      • 5ae72164b524:新手 在项目中怎么使用
        猩语:@G_GGQ 看需求了啊。我觉得实际项目中用到的应该不多。都是新手。一起学习哈
        猩语:@G_GGQ 看需求了啊。都是新手。一起学习哈。
      • Abnerzj:在获取类的信息时,建议作者有两个小点可以优化一下哈,如下获取方法列表:
        //获取方法列表
        Method *methodList = class_copyMethodList([self class], &count);
        for (unsigned int i; i<count; i++) {
        Method method = methodList[i];
        NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
        }

        1,释放方法列表:free(methodList);
        2,for循环里面的局部变量i最好赋一个初始值,不然在OC项目下,不会进入循环。
        猩语:@Abnerzj 多谢指教。我研究一下哈
      • smirkk:你"v@:*"中的*代表什么意思,v代表返回值,如果是其他类型又该怎么办呢?
        猩语:@风中的温柔 @Abnerzj :smile:忘记回复了哈。就是这个文档。
        smirkk:@Abnerzj 已经解决了哈,多谢!
        Abnerzj:@风中的温柔 可以参考苹果官方文档https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1
      • kakatimo:很不错的一篇文章
      • db8e987e7673:(IMP)instanceMethodForSelector:(SEL)aSelector

        请问为什么没有这个方法?
        db8e987e7673:谢谢,原来是个类方法.
        猩语:@Allensa 有啊。你用[self class]或者类名直接调用就行了啊
      • 311ca7a27754:很好 ,多谢楼主分享
      • 11a232d8b017:有demo 跑一下就更好了。。完美了。
      • GorCat:Mark_Runtime
      • leftwater:消息转发那块讲的有点粗糙
      • _bab:写的不错哦
      • 猩语:@wxxxxxxxxxxxxxx 我的理解是。你可以这样来通过字符串初始化一个类。
        Class aClass = NSClassFromString(@"ClassName");
        id obj = [[aClass alloc] init];
        你也可以这样通过字符串获得一个方法。
        SEL sSel = NSSelectorFromString(@"selName");
        然后这样实现调用
        objc_msgSend(obj, aSel, arg1, arg2, ...)
        大致是这样一个思路。不知道我的理解对不哈。
        bde04638cca8:这样写就可以不用导入头文件了,还可以装逼,不过谨慎使用啊
        leftwater:@兴宇是谁 对的
      • ec40b945e9d3:你好,我想通过 @"类名" @"函数名" 这两个字符串来实现runtime中调用函数该怎么写?
        ec40b945e9d3:@兴宇是谁 系统中已经实例化存在这个类了, 现在我想通过类名字符串来获取这个类的实例,就是不知道用哪个方法获取。
        猩语:@wxxxxxxxxxxxxxx 类名不能直接调用非类方法吧。所以你要想调用。得先有这个类吧。
      • Qiuny:谢谢整理,长姿势了
      • f53cc659b3b1:尽管我是看不懂,但是真的写的很详细,很认真
      • 苏坡乔:学习了
      • 柴桑养竹人:深入浅出 :clap:
      • 9155bab46eb0:其实这里有个问题大家发现没有就是关于方法拦截,原谅我是新手,那个方法拦截感觉没用啊,那个方法没有直接错误提示了,为什么还要用到它呢
        猩语:@招大虾 有些时候。可能有些需求是要动态添加方法的。这时候方法拦截就有用了啊。或者是。有的类找不到这个方法要转移到别的类去执行。这些都是要在运行时处理的事情。我也是新手。可能理解的不够。一起学习哈。
      • 世界的一缕曙光:你好,能不能帮我解释下,打印的顺序是swizzle-swizzle-viewWillAppear-swizzle,如果我把[super viewWillAppear];注释掉,打印结果是swizzle-viewWillAppear-swizzle,这是为什么呢?不是很懂,希望指点一下,谢谢哈。
        猩语:@Pro_iOS 👌🏻好的
        世界的一缕曙光:@兴宇是谁 非常感谢你的解答,我刚睡下,明天我去公司再好好琢磨琢磨,谢谢啦!!!😃😃
        猩语:@Pro_iOS 去掉super的话,那个执行顺序你是懂的哈。super这个关键字其实是一个编译器标识,它会跳过在当前类中寻找方法直接去父类中找相应的方法。(参考一下本文 方法调用 部分)所以加super 后的那个执行顺序中间也是有一个viewWillAppear 的。只是我们并没有在父类UIViewController 中写NSLog ,所以没有输出。不知道这样表述的清楚不。可能有所疏忽。一起学习哈。
      • Raybon_lee:赞一个,分析的很好,前面那个空方法不说我都没注意
      • PeanutGao:非常感谢.....学习了
      • 93d451992f07:我是个新手,不要鄙视我问低级问题 :pensive: ,刚刚开始接触runtime,我想问下关于交换的方法,我没懂我们交换完之后有什么用,现在就是我交换完了 调用的是我自定义的方法,原来的方法就不会被调用了是吧,能举个现实中的例子嘛?坐等回复 :pray:
        猩语:@lee_retain 不客气。一起学习
        93d451992f07:@兴宇是谁 嗯 thank you :grin:
        猩语:@lee_retain 我也做iOS不久。大家一起学习哈哈。关于交换。我是这么理解的。你可以交换系统的方法。比如viewWillAppear。在这里面执行你自己的方法。然后再换回来。这样不影响系统的方法,还加入了你自己的东西。我觉得是一种面向切面的思想。粗浅的理解哈。
      • 风与鸾:谢谢整理,不错
      • ddaa8dae50b0:需要了解, 可以随意测试调试, 但在工程里面少用
      • 1138d8974094:写的不错
      • 6b9f742d23da:🙏🏻🙏🏻
      • 31e0ef9a7fd8:有没有动态添加方法的demon看看
        漫步的小蚂蚁:@18612254213
        void run(id self,SEL sel) {
        NSLog(@"%@ %@",self,NSStringFromSelector(sel));
        }

        + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel==@selector(run))
        {
        class_addMethod(self, @selector(run), (IMP)run, "v@:");
        }
        return [super resolveInstanceMethod:sel];
        }
        猩语:@18612254213 呃 那个代码找不到了 除了继承那几个方法以外 你还可以手动添加 但是我暂时没有想到 手动添加有什么应用场景。 见谅
      • d99e841a942e:真心不错 楼猪 辛苦了
      • Tieria:不错
      • 076674d54c1c:阿鲁 Tom out
        他就n近距离感受着大自然
      • cd1fcb172f50:很好的介绍了runtime
      • 奴良:很不错
      • Pierre_:很好,上工了,用一下
      • 2f3e8481036f:解释的很到位。感谢
      • 风了个1:谢谢整理

      本文标题:iOS~runtime理解

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