iOS - OC RunTime 运行时

1、运行时

  • 运行时是苹果提供的纯 C 语言的开发库,是一种非常牛逼、开发中经常用到的底层技术。

1.1 运行时原理

  • Objective-C 是一门简单的语言,95% 是 C,只是在语言层面上加了些关键字和语法,真正让 Objective-C 如此强大的是它的运行时。它很小但却很强大,它的核心是消息分发。
  • 1、Messages
    • 执行一个方法,有些语言,编译器会执行一些额外的优化和错误检查,因为调用关系很直接也很明显。但对于消息分发来说,就不那么明显了。在发消息前不必知道某个对象是否能够处理消息。你把消息发给它,它可能会处理,也可能转给其他的Object来处理。一个消息不必对应一个方法,一个对象可能实现一个方法来处理多条消息。
    • 在 Objective-C 中,消息是通过 objc_msgSend() 这个 runtime 方法及相近的方法来实现的。这个方法需要一个 target,selector,还有一些参数。理论上来说,编译器只是把消息分发变成 objc_msgSend 来执行。比如下面这两行代码是等价的。
          [array insertObject:foo atIndex:5];
      
          objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
  • 2、Objects, Classes, MetaClasses
    • 大多数面向对象的语言里有 classes 和 objects 的概念。Objects 通过 Classes 生成。但是在 Objective-C 中,classes 本身也是 objects,也可以处理消息,这也是为什么会有类方法和实例方法。具体来说,Objective-C 中的 Object 是一个结构体(struct),第一个成员是 isa,指向自己的 class。这是在 objc/objc.h 中定义的。

          typedef struct objc_object {
              Class isa;
          } *id;
    • object 的 class 保存了方法列表,还有指向父类的指针。但 classes 也是 objects,也会有 isa 变量,那么它又指向哪儿呢?这里就引出了第三个类型:metaclasses。一个 metaclass 被指向 class,class 被指向 object。它保存了所有实现的方法列表,以及父类的 metaclass。
  • 3、Methods, Selectors,IMPs
    • 我们知道了运行时会发消息给对象。我们也知道一个对象的 class 保存了方法列表。那么这些消息是如何映射到方法的,这些方法又是如何被执行的呢?
    • 第一个问题的答案很简单。class 的方法列表其实是一个字典,key 为 selectors,IMPs 为 value。一个 IMP 是指向方法在内存中的实现。很重要的一点是,selector 和 IMP 之间的关系是在运行时才决定的,而不是编译时。这样我们就能玩出些花样。
    • IMP 通常是指向方法的指针,第一个参数是 self,类型为 id,第二个参数是 _cmd,类型为 SEL,余下的是方法的参数。这也是 self 和 _cmd 被定义的地方。下面演示了 Method 和 IMP。
          - (id)doSomethingWithInt:(int)aInt{}
      
          id doSomethingWithInt(id self, SEL _cmd, int aInt){}

1.2 运行时作用

  • 运行时的作用:

    • 创建、修改、自省 classes 和 objects

      • 获得某个类的所有成员变量
      • 获得某个类的所有属性
      • 获得某个类的所有方法
      • 交换方法实现
      • 动态添加一个成员变量
      • 动态添加一个属性
      • 动态添加一个方法
    • 消息分发
  • 1、class
    • class 开头的方法是用来修改和自省 classes。方法如:
    • 拿到一个 class 的所有内容
          class_addIvar, class_addMethod, class_addProperty,class_addProtocol 允许重建 classes
          class_copyIvarList, class_copyMethodList, class_copyProtocolList,class_copyPropertyList
    • 返回单个内容
          class_getClassMethod, class_getClassVariable,
          class_getInstanceMethod, class_getInstanceVariable,
          class_getMethodImplementation,class_getProperty
    • 一些通用的自省方法
          class_conformsToProtocol, class_respondsToSelector, class_getSuperclass
    • 创建一个 object
          class_createInstance 来创建一个 object
  • 2、ivar
    • 这些方法能让你得到名字,内存地址和 Objective-C type encoding。
  • 3、method
    • 这些方法主要用来自省,比如:

          method_getName, method_getImplementation, method_getReturnType 等等
    • 也有一些修改的方法,包括:
          method_setImplementation 和 method_exchangeImplementations
  • 4、objc
    • 一旦拿到了 object,你就可以对它做一些自省和修改。你可以 get/set ivar, 使用 object_copyobject_dispose 来 copy 和 free object 的内存。不仅是拿到一个 class,而是可以使用 object_setClass 来改变一个 object 的 class。
  • 5、property
    • 属性保存了很大一部分信息。除了拿到名字,你还可以使用 property_getAttributes 来发现 property 的更多信息,如返回值、是否为 atomic、getter/setter 名字、是否为 dynamic、背后使用的 ivar 名字、是否为弱引用。
  • 6、protocol
    • Protocols 有点像 classes,但是精简版的,运行时的方法是一样的。你可以获取 method, property, protocol 列表, 检查是否实现了其他的 protocol。
  • 7、sel
    • 可以处理 selectors,比如获取名字,注册一个 selector 等等。

2、运行时的作用示例

2.1 Classes And Selectors From Strings

  • 比较基础的一个动态特性是通过 String 来生成 Classes 和 Selectors。Cocoa 提供了 NSClassFromString 和 NSSelectorFromString 方法,使用起来很简单:

        Class stringclass = NSClassFromString(@"NSString");
  • 于是我们就得到了一个 string class。接下来:
        NSString *myString = [stringclass stringWithString:@"Hello World"];
  • 为什么要这么做呢?直接使用 NSString Class 不是更方便?通常情况下是,但有些场景下这个方法会很有用。首先,可以得知是否存在某个 class,如果运行时不存在该 class 的话,NSClassFromString 会返回 nil。
  • 另一个使用场景是根据不同的输入返回不同的 class 或 method。比如你在解析一些数据,每个数据项都有要解析的字符串以及自身的类型(String,Number,Array)。你可以在一个方法里搞定这些,也可以使用多个方法。其中一个方法是获取 type,然后使用 if 来调用匹配的方法。另一种是根据 type 来生成一个 selector,然后调用之。以下是两种实现方式:
        - (void)parseObject:(id)object {
    
            for (id data in object) {
    
                if ([[data type] isEqualToString:@"String"]) {
                    [self parseString:[data value]];
                } else if ([[data type] isEqualToString:@"Number"]) {
                    [self parseNumber:[data value]];
                } else if ([[data type] isEqualToString:@"Array"]) {
                    [self parseArray:[data value]];
                }
            }
        }
    
        - (void)parseObjectDynamic:(id)object {
    
            for (id data in object) {
                [self performSelector:NSSelectorFromString([NSString stringWithFormat:@"parse%@:", [data type]]) withObject:[data value]];
            }
        }
    
        - (void)parseString:(NSString *)aString {}
        - (void)parseNumber:(NSString *)aNumber {}
        - (void)parseArray:(NSString *)aArray {}
  • 可以看到,你可以把 7 行带 if 的代码变成 1 行。将来如果有新的类型,只需增加实现方法即可,而不用再去添加新的 else if。

2.2 Method Swizzling

  • 之前我们讲过,方法由两个部分组成。Selector 相当于一个方法的 id;IMP 是方法的实现。这样分开的一个便利之处是 selector 和 IMP 之间的对应关系可以被改变。比如一个 IMP 可以有多个 selectors 指向它。
  • 而 Method Swizzling 可以交换两个方法的实现。或许你会问 “什么情况下会需要这个呢?”。我们先来看下 Objective-C 中,两种扩展 class 的途径。首先是 subclassing。你可以重写某个方法,调用父类的实现,这也意味着你必须使用这个 subclass 的实例,但如果继承了某个 Cocoa class,而 Cocoa 又返回了原先的 class(比如 NSArray)。这种情况下,你会想添加一个方法到 NSArray,也就是使用 Category。99% 的情况下这是 OK 的,但如果你重写了某个方法,就没有机会再调用原先的实现了。
  • Method Swizzling 可以搞定这个问题。你可以重写某个方法而不用继承,同时还可以调用原先的实现。通常的做法是在 category 中添加一个方法(当然也可以是一个全新的 class)。可以通过 method_exchangeImplementations 这个运行时方法来交换实现。来看一个 demo,这个 demo 演示了如何重写 addObject: 方法来纪录每一个新添加的对象。
        #import  <objc/runtime.h>
    
        @interface NSMutableArray (LoggingAddObject)
    
        - (void)logAddObject:(id)aObject;
    
        @end
    
        @implementation NSMutableArray (LoggingAddObject)
    
        + (void)load {
            Method addobject    = class_getInstanceMethod(self, @selector(addObject:));
            Method logAddobject = class_getInstanceMethod(self, @selector(logAddObject:));
    
            method_exchangeImplementations(addObject, logAddObject);
        }
    
        - (void)logAddObject:(id)aobject {
    
            [self logAddObject:aObject];
    
            NSLog(@"Added object %@ to array %@", aObject, self);
        }
    
        @end
  • 我们把方法交换放到了 load 中,这个方法只会被调用一次,而且是运行时载入。如果指向临时用一下,可以放到别的地方。注意到一个很明显的递归调用 logAddObject:。这也是 Method Swizzling 容易把我们搞混的地方,因为我们已经交换了方法的实现,所以其实调用的是 addObject:

2.3 动态继承、交换

  • 我们可以在运行时创建新的 class,这个特性用得不多,但其实它还是很强大的。你能通过它创建新的子类,并添加新的方法。
  • 但这样的一个子类有什么用呢?别忘了 Objective-C 的一个关键点:object 内部有一个叫做 isa 的变量指向它的 class。这个变量可以被改变,而不需要重新创建。然后就可以添加新的 ivar 和方法了。可以通过以下命令来修改一个 object 的 class.
        object_setClass(myObject, [MySubclass class]);
  • 这可以用在 Key Value Observing。当你开始 observing an object 时,Cocoa 会创建这个 object 的 class 的 subclass,然后将这个 object 的 isa 指向新创建的 subclass。

2.4 动态方法处理

  • 目前为止,我们讨论了方法交换,以及已有方法的处理。那么当你发送了一个 object 无法处理的消息时会发生什么呢?很明显,"it breaks"。大多数情况下确实如此,但 Cocoa 和 runtime 也提供了一些应对方法。
  • 首先是动态方法处理。通常来说,处理一个方法,运行时寻找匹配的 selector 然后执行之。有时,你只想在运行时才创建某个方法,比如有些信息只有在运行时才能得到。要实现这个效果,你需要重写 +resolveInstanceMethod: 和/或 +resolveClassMethod:。如果确实增加了一个方法,记得返回 YES。
        + (BOOL)resolveInstanceMethod:(SEL)aSelector {
    
            if (aSelector == @selector(myDynamicMethod)) {
                class_addMethod(self, aSelector, (IMP)myDynamicIMP, "v@:");
                return YES;
            }
            return [super resolveInstanceMethod:aSelector];
        }
  • 那 Cocoa 在什么场景下会使用这些方法呢?Core Data 用得很多。NSManagedObjects 有许多在运行时添加的属性用来处理 get/set 属性和关系。

2.5 消息转发

  • 如果 resolve method 返回 NO,运行时就进入下一步骤:消息转发。有两种常见用例。1) 将消息转发到另一个可以处理该消息的 object。2) 将多个消息转发到同一个方法。
  • 消息转发分两步。首先,运行时调用 -forwardingTargetForSelector:,如果只是想把消息发送到另一个 object,那么就使用这个方法,因为更高效。如果想要修改消息,那么就要使用 -forwardInvocation:,运行时将消息打包成 NSInvocation,然后返回给你处理。处理完之后,调用 invokeWithTarget:
  • Cocoa 有几处地方用到了消息转发,主要的两个地方是代理(Proxies)和响应链(Responder Chain)。NSProxy 是一个轻量级的 class,它的作用就是转发消息到另一个 object。如果想要惰性加载 object 的某个属性会很有用。NSUndoManager 也有用到,不过是截取消息,之后再执行,而不是转发到其他的地方。
  • 响应链是关于 Cocoa 如何处理与发送事件与行为到对应的对象。比如说,使用 Cmd+C 执行了 copy 命令,会发送 -copy: 到响应链。首先是 First Responder,通常是当前的 UI。如果没有处理该消息,则转发到下一个 -nextResponder。这么一直下去直到找到能够处理该消息的 object,或者没有找到,报错。

2.6 使用 Block 作为 Method IMP

  • iOS 4.3 带来了很多新的 runtime 方法。除了对 properties 和 protocols 的加强,还带来一组新的以 imp 开头的方法。通常一个 IMP 是一个指向方法实现的指针,头两个参数为 object(self) 和 selector(_cmd)。iOS 4.0 和 Mac OS X 10.6 带来了 block,imp_implementationWithBlock() 能让我们使用 block 作为 IMP,下面这个代码片段展示了如何使用 block 来添加新的方法。

        IMP myIMP = imp_implementationWithBlock(^(id _self, NSString *string) {
            NSLog(@"Hello %@", string);
        });
    
        class_addMethod([MYclass class], @selector(sayHello:), myIMP, "v@:@");
  • 可以看到,Objective-C 表面看起来挺简单,但还是很灵活的,可以带来很多可能性。动态语言的优势在于在不扩展语言本身的情况下做很多很灵巧的事情。比如 Key Value Observing,提供了优雅的 API 可以与已有的代码无缝结合,而不需要新增语言级别的特性。

3、运行时的使用

3.1 动态添加属性

  • 函数方法

        // 设值函数
        OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
    
        // 取值函数
        OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    
        参数:
            object  :属性的持有者,源对象,指定我们需要绑定的对象
            key     :属性的键值,设置一个静态常亮,也就是 Key 值,通过这个我们可以找到我们关联对象的那个数据值
            value   :属性的数值,这个是我们调用属性的时候会自动调用 set 方法进行传值
            policy  :属性的引用类型,关联策略
    
        关联策略:
            typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    
                OBJC_ASSOCIATION_ASSIGN = 0,           // 关联策略是基于基本类型的,也就是我们常用的 assign 属性
    
                OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 关联策略是基于对象类型的,如我们正常的是 retain nonatomic(非原子操作)类型 ,
                                                       // retain:保留一个引用指针,内存地址不修改,指向同一块内存地址
    
                OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   // 这个相当于属性中对一些对象或者字符串进行的 copy,这个是拷贝一个新对象,内存地址不在一起,
                                                       // 还是使用的非原子类型,非原子类型我们也称之为线程不安全的操作,但是对于 OC 里面的数据操作,
                                                       // 我们尽量避开原子操作,原子操作是线程安全的,会影响我们对数据的写入操作
    
                OBJC_ASSOCIATION_RETAIN = 01401,       // 简单的指针引用,retain 操作
    
                OBJC_ASSOCIATION_COPY = 01403          // 把简单的对象拷贝到一个新的内存地址
            };
  • 向分类中添加属性
    • 包含运行时头文件

          // 包含头文件
          #import <objc/runtime.h>                                                            
    • 1、添加 OC 对象类型属性
          // 添加属性
          @property (nonatomic, strong) NSString *address;
      
          // 设置属性的键值
          const void *addressKey = @"addressKey";
      
          // 属性的 setter 方法
          - (void)setAddress:(NSString *)address {
      
              // 用运行时方法设置属性的值
              objc_setAssociatedObject(self, addressKey, address, OBJC_ASSOCIATION_COPY_NONATOMIC);
          }
      
          // 属性的 getter 方法
          - (NSString *)address {
      
              // 用运行时方法获取属性的值
              return objc_getAssociatedObject(self, addressKey);
          }
    • 2、添加基本数据类型类属性
          // 添加属性
          @property (nonatomic, assign) NSUInteger telNo;
      
          // 设置属性的键值
          const void *telNoKey = @"telNoKey";
      
          // 属性的 setter 方法
          - (void)setTelNo:(NSUInteger)telNo {
      
              // 用运行时方法设置属性的值
              objc_setAssociatedObject(self, telNoKey, @(telNo), OBJC_ASSOCIATION_ASSIGN);
          }
      
          // 属性的 getter 方法
          - (NSUInteger)telNo {
      
              // 用运行时方法获取属性的值
              return [objc_getAssociatedObject(self, telNoKey) integerValue];
          }

3.2 获得类的所有属性

  • 动态获取对象属性

    • 1、objc_property_t 方式获取到的属性值包含分类中添加的属性,属性名不带下划线(_),无法获取到访问器方法添加的属性。

          // 包含运行时头文件
          #import <objc/runtime.h>
      
          // 属性的数量
          unsigned int count = 0;
      
          // 拷贝对象属性数组(数组名就是指向数组第一个元素的地址),Person 是要获取属性的类
          objc_property_t *properties = class_copyPropertyList(Person.class, &count);
      
          // 遍历所有的属性
          NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count];
          for (unsigned int i = 0; i < count; i++) {
      
              // 从数组中获取属性
              objc_property_t pty = properties[i];
      
              // 获取属性名称
              const char *cname = property_getName(pty);
      
              NSString *propertieName = [NSString stringWithUTF8String:cname];
              [arrayM addObject:propertieName];
          }
      
          // 释放数组
          free(properties);
      
          NSLog(@"%@", [arrayM copy]);

    • 2、Ivar 方式获取到的属性值不包含分类中添加的属性,属性名前带下划线(_),可以获取到访问器方法添加的属性。
          // 属性的数量
          unsigned int count = 0;
      
          // 获得所有的属性,ivars 是一个指向成员变量的指针,Person 是要获取属性的类
          Ivar *ivars = class_copyIvarList(Person.class, &count);
      
          // 遍历所有的属性
          NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count];
          for (unsigned int i = 0; i < count; i++) {
      
              // 从数组中获取属性
              Ivar ivar = ivars[i];
      
              // 获取属性名称
              const char *cname = ivar_getName(ivar);
      
              NSString *propertieName = [NSString stringWithUTF8String:cname];
              [arrayM addObject:propertieName];
          }
      
          // 释放数组
          free(ivars);
      
          NSLog(@"%@", [arrayM copy]);

时间: 2024-10-23 19:08:26

iOS - OC RunTime 运行时的相关文章

公共语言运行时CLR由CTS、CLS、JIT组成,请问C#编译器和VB.NET编译器属于CLR吗?

问题描述 公共语言运行时CLR由CTS.CLS.JIT组成,那么请问:C#编译器和VB.NET编译器属于CLR吗? 解决方案 解决方案二:这两者应该没关系吧.是两个东西,编译器是将C#VB代码编译成CLR可以执行的中间语言.解决方案三:CLR跟JVM的意义基本相等,就是一个运行时的环境.直接查一下定义就可以看到,"CLR的核心功能包括:内存管理.程序集加载.安全性.异常处理和线程同步".因此可以看到它跟编程语言无关.MFC跟编程语言其实是也无关,但是它显然几乎只是被特定语言接受,不如C

iOS运行时(Runtime)总结

声明 本博客中文章不会在此处再更新,只会在微信公众号中更新,请关注微信公众号,以获取最新的学习资源和更多学习资源.本博文末尾有微信公众号二维码,扫一扫添加关注. 原文出自:微信公众号iOSDevShares的文章 引言 相信很多同学都听过运行时,但是我相信还是有很多同学不了解什么是运行时,到底在项目开发中怎么用?什么时候适合使用?想想我们的项目中,到底在哪里使用过运行时呢?还能想起来吗?另外,在面试的时候,是否经常有笔试中要求运用运行时或者在面试时面试官会问是否使用过运行时,又是如何使用的? 回

总结iOS中runtime的使用_IOS

做iOS的朋友都知道或听说runtime,这个东西很像java的反射机制,但功能远胜于java的反射.通过runtime我们可以动态的向一个类中添加属性.成员变量.方法,以及对其进行读写访问. 一.runtime简介 RunTime简称运行时.OC就是运行时机制,也就是在运行时候 的一些机制,其中最主要的是消息机制. 对于C语言,函数的调用在编译的时候会决定调用哪个函数. 对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称 找到对应的

.NET基础知识-公共语言运行时

   .NET提供了一个运行环境,叫做公共语言运行时(CLR).CLR管理代码的执行并使开发过程变得更加简单.CLR是一种受控的执行环境,其功能通过编译器与其他工具共同展现.以"运行时"为目标的代码称为受控代码(Managed Code).受控代码指向的对象在执行过程中完全被CLR所控制.在执行过程中,CLR提供自动内存管理.调试支持.增强的安全性及与受控代码(如COM组件)的互操作性.凡是符合CLS(公共语言规范)的程序语言(如C#和Visual Basic.NET等)所开发的程序都

使用并发与协调运行时

介绍 并发与协调运行时(Concurrency and Coordination Runtime,CCR)是一个.NET平台 上的异步信息传递类库,提供了一套细小而强大的基础功能,能够使用不同的方式来组织 应用程序.应用程序通过有效使用CCR可以获得更好的响应能力,以及更好的伸缩性及容 错性.而它最神奇的地方则在于,开发人员获得这些便利的同时,还减少(甚至完全消除 )了对线程.锁.互斥体(mutex)或其他同步元素的直接操作(或捕获错误). 如果您的应用程序是单线程的,CCR可以使您的程序提高响

[译] 在你沉迷于包的海洋之前,还是了解一下运行时 Node.js 的本身

本文讲的是[译] 在你沉迷于包的海洋之前,还是了解一下运行时 Node.js 的本身, 原文地址:Before you bury yourself in packages, learn the Node.js runtime itself 原文作者:该文章已获得作者 Samer Buna 授权 译文出自:掘金翻译计划 译者:fghpdf 校对者:rccoder,reid3290 在你沉迷于包的海洋之前,还是了解一下运行时 Node.js 的本身 这篇文章将挑战你 Node.js 的知识极限. 我

iOS使用runtime修改文本框(TextField)的占位文字颜色_IOS

前言 在大家平时的开发中,有时候会遇到背景深色的界面上需要添加TextField,TextField默认的占位颜色也是深灰色,比较难看清,这时候就需要修改展位文字的颜色,可是系统没有提供相应的方法,那么就需要我们自己来自定义了 修改后的效果 话不多说,下面上代码: // .h文件 #import <UIKit/UIKit.h> @interface UITextField (Placeholder) @property UIColor *placeholderColor; @end // .m

Kubernetes提供了另一种容器运行时

传统上,Kubernetes容器运行时是绑定到Docker和rkt的.但是在过去数月中,这一情况发生了变化.Kubernetes发布了自己的容器运行时接口(CRI,Container Runtime Interface)API,同时正在完成一个称为CRI-O的实现,力图构建Kubernetes和OCI兼容运行时之间的桥梁.这为Kubernetes以标准方式使用任何OCI兼容容器运行时铺平了道路.Kubernetes依赖于底层的容器运行时实现生命周期控制,例如Pull.创建.删除等操作.运行时实现

一起谈.NET技术,VB.NET和C#的发展与动态语言运行时

在去年微软的专业开发者大会上(Professional Developers Conference PDC 2008),微软就开发者在.NET平台上的未来做了详细的展望,本文我们将概括地来看一下即将到来的.NET 4.0 Framework的一些特性,以及全新的Microsoft 系编程语言,讨论下微软对于.NET Platform在接下来几年的长远展望. VB.NET和C#的联合发展 当前来自软件的最首要的信息之一是针对Microsoft Visual Basic .NET开发者的.VB.NE