iOS Runtime理解与运用

ping怎么这么高?


哈哈,进入正题!


什么是Runtime?

这还要说?run( 运行)、time(时),runtime(运行时),没毛病!好了,我们都知道Objective-C是基于C衍生出来的动态语言,加入了面向对象特征和消息机制,这都归功于Runtime,它将静态语言在编译和链接时期做的事放到了运行时来处理。在我们Objective-C中,runtime是一个运行时库,是一套纯C的API。

  • 面向对象,在OC中一切都被设计成对象,它们的基础数据结构在Runtime库中用C语言的结构体表示。

    当一个类被初始化成一个实例,这个实例就是一个对象,在runtime中用objc_object结构体表示,可以到objc/objc.h查看定义
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY; //结构体指针,指向类对象,这样,当我们向对象发送消息时,runtime库会根据这个isa指针找到对象所属的类,然后从类的方法列表及父类方法列表中查找消息对应的selector指向的函数实现,然后执行。
};
/// A pointer to an instance of a class.
typedef struct objc_object *id; //该类型对象可以转换成任意对象

当然类也是对象,由Class类型表示,它实际上是一个指向 objc_class结构体的指针,可以到objc/runtime.h中查看定义

typedef struct objc_class *Class;
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;  //结构体的指针,每个对象都有一个isa指针,实例的isa指向类对象,类对象的isa指向元类。

    #if !__OBJC2__
    Class super_class                                        //父类
    const char *name                                         //类名
    long version                                             //类的版本信息,默认为0
    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
}

上面引出一个元类(Meta Class):类对象的类,它储存着一个类所有的类方法,每个类都有一个单独的meta-class,因为每个类的类方法基本不可能完全相同,那么细想,元类也是有isa指针的,它指向谁呢?为了不让这种结构无限延伸下去,isa指向基类的meta-class,而基类的meta-class的isa指针指向它自己。

  • 消息机制,在OC中任何的方法调用,其本质都是消息发送,id objc_msgSend(id self, SEL op, ...),属于动态调用的过程,比如[receiver doSomething],在运行时就会转成objc_msgSend(receiver,@selector(doSomething)),receiver作为一个消息接收对象,@selector(doSomething)是一个消息体,函数内部执行顺序:
  1. 检查消息对象是否为nil,如果是,则什么都不做。
  2. 通过receiver 的isa指针找到receiver对应的类,从类的方法缓存中通过SEL查找IMP,有,调用;没有,往下。(类的方法很多,如果每次都去方法列表中查找就会影响到效率,所以每一个类都会有一个方法缓存)。
  3. 从方法列表中查找,有,调用;没有,往下。
  4. 查找父类的方法缓存,有,直接调用;没有,往下。
  5. 查找父类的方法列表,有,直接调用;没有,往下,一直找到基类,以上就是一个正常的消息发送过程。
  6. 如果在基类也没有找到,则会调用NSObject的决议方法 + (BOOL)resolveInstanceMethod:(SEL)sel+ (BOOL)resolveClassMethod:(SEL)sel,返回YES则重启一次消息的发送过程,返回NO则会进入消息转发
  7. 调用- (id)forwardingTargetForSelector:(SEL)aSelector,如果实现了这个方法,并返回一个非nil的对象,则这个对象会作为消息的新接收者,且消息会被分发到这个对象,当然这个对象不能是self自身,否则就是出现无限循环;如果返回的是nil,往下继续。
  8. 调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector ,生成一个方法签名,接着会创建一个NSInvocation(消息调用对象,包含target,selector,以及方法签名),并传给- (void)forwardInvocation:(NSInvocation *)anInvocation,进行转发调用。

消息异常处理

当消息异常的时候,会执行方法决议以及消息转发,在上面的消息发送过程中也具体介绍了,这里借用一张图片来更好的理解


  • 在这个过程中,我们可以在方法决议中添加方法实现并返回YES,来阻止crash

    @implementation NSObject (ZMSafe)
    
    +(void)load{
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      [self zm_swizzleInstanceMethodWithSrcClass:[self class]
                                          srcSel:@selector(forwardInvocation:)
                                     swizzledSel:@selector(zm_forwardInvocation:)];
      
      [self zm_swizzleInstanceMethodWithSrcClass:[self class]
                                          srcSel:@selector(methodSignatureForSelector:)
                                     swizzledSel:@selector(zm_methodSignatureForSelector:)];
      
      [self zm_swizzleInstanceMethodWithSrcClass:[self class]
                                          srcSel:@selector(forwardingTargetForSelector:)
                                     swizzledSel:@selector(zm_forwardingTargetForSelector:)];
      
      [self zm_swizzleClassMethodWithSrcClass:[self class]
                                       srcSel:@selector(resolveInstanceMethod:)
                                  swizzledSel:@selector(zm_resolveInstanceMethod:)];
      
      });
    }
    +(BOOL)zm_resolveInstanceMethod:(SEL)sel{
      if(sel == NSSelectorFromString(@"push")){
         NSLog(@"unrecognized selector -[%@ %@]\n%s",NSStringFromClass(self),NSStringFromSelector(sel),__FUNCTION__);
         /* 
            //这是method的数据结构,在method其实就相当于在SEL跟IMP之间作了一个映射,有了SEL,我们便可以找到对应的IMP
            struct objc_method {
                SEL method_name                 //方法名                          
                char *method_types              //方法类型                           
                IMP method_imp                  //实现地址                            
            }   
         */
         Method method = class_getClassMethod([self class], @selector(empty));
         //  获取函数类型,有没有返回参数,传入参数
         const char *type = method_getTypeEncoding(method);
         // 添加方法,将未实现的方法编号sel跟自定义的方法实现imp关联
         class_addMethod([self class], sel, method_getImplementation(method), type);
         // 返回YES,重启一次消息的发送过程,现在已经添加了方法实现empty,所以会直接调用它
         return YES;
      
      } 
      return [[self class]zm_resolveInstanceMethod:sel];
    }
    - (void)empty{
       NSLog(@"empty");
    }
    

    看调用结果,执行了决议方法和自定义的方法实现empty,并没有crash。

    其实在这里还可以做很多的事情,比如版本的适配,在低版本中调用了高版本的方法,在这里就可以把方法名提取出来,再指向我们自定义的方法实现,等等。

  • 也可以在- (id)forwardingTargetForSelector:(SEL)aSelector替换消息接收对象

    - (id)zm_forwardingTargetForSelector:(SEL)aSelector{
        if(aSelector == NSSelectorFromString(@"push")){
      
          NSLog(@"unrecognized selector -[%@ %@]\n%s",NSStringFromClass([self class]),NSStringFromSelector(aSelector),__FUNCTION__);
          // 我这里就直接动态创建一个类
          Class ZMClass = objc_allocateClassPair([NSObject class], "ZMClass", 0);
          // 注册类
          objc_registerClassPair(ZMClass);
          // 获取自定义empty方法
          Method method = class_getClassMethod([self class], @selector(empty));
          // 获取函数类型,有没有返回参数,传入参数
          const char *type = method_getTypeEncoding(method);
          // 添加方法,将未实现的方法编号sel跟自定义的方法实现imp关联
          class_addMethod(ZMClass, aSelector, method_getImplementation(method), type);
          // 返回该对象来接收消息
          return [[ZMClass alloc]init];
      
       }
       return [self zm_forwardingTargetForSelector:aSelector];
    
    }
    

再看调用结果,效果是一样的,只是不同的处理方式而已,从打印上可以看出,这是在- (id)zm_forwardingTargetForSelector:(SEL)aSelector中进行处理的,也是替换的- (id)forwardingTargetForSelector:(SEL)aSelector方法,找到返回的备用对象去执行调用的方法。

  • 或者在最后一步也就是消息真正转发的方法中做处理,重写- (void)forwardInvocation:(NSInvocation *)anInvocation,同时一定要重写- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,因为anInvocation对象是通过返回方法签名来创建的。

    /**
     消息转发方法
    
     @param anInvocation 消息转发对象
    */
    - (void)zm_forwardInvocation:(NSInvocation *)anInvocation{
    
        NSLog(@"unrecognized selector -[%@ %@]\n%s",anInvocation.target,NSStringFromSelector([anInvocation selector]),__FUNCTION__);
    
        //如果自定义实现方法中什么都没做,只是为了能在运行时找到该实现方法,不至于crash,那么这里可以不进行消息发送,可以注释掉
        if (![self respondsToSelector:anInvocation.selector]) {
           //  拿到方法对象
           Method method = class_getClassMethod([self class], @selector(empty));
           //  获取函数类型,有没有返回参数,传入参数
           const char *type = method_getTypeEncoding(method);
           // 添加方法
           class_addMethod([self class], anInvocation.selector, method_getImplementation(method), type);
           // 转发给自己,没毛病
           [anInvocation invokeWithTarget:self];
      
        }
    
    }
    /**
     构造一个方法签名,提供给- (void)forwardInvocation:(NSInvocation *)anInvocation方法,如果aSelector没有对应的IMP,则会生成一个空的方法签名,最终导致程序报错崩溃,所以必须重写。
    
     @param aSelector 方法编号
     @return 方法签名
    */
    - (NSMethodSignature *)zm_methodSignatureForSelector:(SEL)aSelector {
    
        if ([self respondsToSelector:aSelector]) {
           // 如果能够响应则返回原始方法签名
           return [self zm_methodSignatureForSelector:aSelector];
      
        }else{
          // 构造自定义方法的签名,不实现则返回nil,导致crash
          return [[self class] instanceMethodSignatureForSelector: @selector(empty)];
      
        }
    
    }
    

调用结果也是一样的,这也是异常消息处理最后的机会,错过了就没机会了。


小结

这里主要是通过一个异常的消息来演示消息发送以及转发的过程,并在消息转发过程中对异常消息的捕捉及处理,我把这些写到NSObject的类目中主要为了防止开发中调用了不存在的方法导致的crash,当然如果在子类中重写了这些方法,可以调用super,也是一样的。


基础用法

  • 对象关联objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy),这也是我在实际开发中使用<objc/runtime.h>的第一个API,方法的意思就是将两个不相关的对象通过一个特定的key关联起来,这样我们拿到对象object可以通过key找到对象Value,最具有代表性的运用就是给类目添加属性了。

    @interface NSObject (Property)
    
    @property (nonatomic,copy)NSString *text;
    
    @end
    
    @implementation NSObject (Property)
    // 手动构造Set方法,让text对象通过SEL指针跟self关联起来
    - (void)setText:(NSString *)text{
    
         objc_setAssociatedObject(self, @selector(setText:), text, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    // 手动构造Get方法,通过SEL指针获取text对象
    - (NSString *)text{
    
        return objc_getAssociatedObject(self, @selector(setText:));
    }
    // 移除该对象下所有关联的对象
    - (void)removeProperty{
    
        objc_removeAssociatedObjects(self);
    }
    
  • 获取属性列表objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount),cls表示获取该类的属性列表,outCount表示属性的总个数。
    举栗:模型转字典
    - (NSDictionary *)dictionary{

        NSMutableDictionary *dic = [NSMutableDictionary dictionary];
        unsigned int count;
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        for (int i = 0; i < count; i ++) {
      
            //生成key
            NSString *key = [NSString stringWithUTF8String:property_getName(propertyList[i])];
            //获取value
            id value = [self valueForKey:key];
      
            if (!value) break;
      
            [dic setObject:value forKey:key];
        }
       free(propertyList);
       return dic;
    }
    
  • 获取成员变量列表Ivar *class_copyIvarList(Class cls, unsigned int *outCount),跟获取属性列表一个意思,不同的是这里会获取该类所有的成员变量,当然其中也包括所有的属性。
    举栗:NSCoding协议,我们想要把模型直接写成本地文件,是要实现编解码协议的,而且要一个一个的写,这里通过拿到属性列表来对所有属性来编解码,一劳永逸。
    - (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);

            for (int i = 0; i < count; i ++) {
                //拿到成员变量
                Ivar ivar = ivarList[i];
                //生成key
                NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                //获取value
                id value = [aDecoder decodeObjectForKey:key];
                [self setValue:value forKey:key];
            }
    
            free(ivarList);
        }
    
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder{
    
        unsigned int count;
        Ivar *ivarList = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i ++) {
      
            //拿到成员变量
            Ivar ivar = ivarList[i];
            //获取key
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            //获取value
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
      
        }
    
        free(ivarList);
    }
    
  • 获取方法列表Method *class_copyMethodList(Class cls, unsigned int *outCount),可以获取cls类的方法列表,包括私有方法,这样我们就可以调用对象的私有方法。
    @interface Student : NSObject
    @end
    @implementation Student
    - (void)study{

        NSLog(@"学习");
    }
    - (void)goHome{
    
        NSLog(@"回家");
    }
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)findStudentMethods{
    
        Student *student = [[Student alloc]init];
        unsigned int count;
        Method *methodList = class_copyMethodList([Student class], &count);
        for (int i = 0; i < count; i ++) {
            //获取方法对象
            Method method = methodList[i];
            //获取方法名
            SEL sel = method_getName(method);
            NSLog(@"方法名:%@",NSStringFromSelector(sel));
            if (sel == NSSelectorFromString(@"study")) {
               //通过NSInvocation来转发消息
                NSMethodSignature *methodSign = [[Student class] instanceMethodSignatureForSelector:sel];
                NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSign];
                invocation.selector = sel;
                [invocation invokeWithTarget:student];
            }
        }
    }
    

打印结果如我们所料,能够拿到所有的方法,也能调用私有方法。


  • 动态添加方法动态创建类,细心的会发现,我在上面第一二段代码就已经描述过了,这里也不在啰嗦了。
  • Method Swizzling,这个我也不在这里多说了,之前写过一篇关于Method Swizzling的介绍,iOS Method Swizzling理解与运用

总结

这里主要是写了自己对Runtime的理解,以及在平时开发中的运用。Runtime里面的API有很多,目前对它的理解以及运用程度有限,所以借此来抛砖引玉,同时有什么错误的地方,希望朋友们能够指出改正,谢谢。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,673评论 0 9
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 723评论 0 2
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 788评论 0 4
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,161评论 0 7
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 741评论 0 1