对Runtime的理解

什么是Runtime? Runtime又叫运行时,是一套底层C语言的API,其为iOS内部的核心之一,我们平时编写的OC代码底层都是基于它来实现的。比如:[target doSomething];底层运行时会被编译器转化成objc_msgSend(target,@selector(doSomething)); 带参数的[target doSomething:arg1...]; 会被底层运行时会被编译器转化成objc_msgSend(target, @selector(doSomething), arg1, arg2, ...); 以上你可能看不出它的价值,但是我们需要了解的是 Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。Runtime 基本是用 C 和汇编写的,由此可见苹果为了动态系统的高效而做出的努力。苹果和 GNU 各自维护一个开源的 Runtime 版本,这两个版本之间都在努力保持一致。 点击这里下载苹果维护的开源代码。 Runtime 的作用 Objc 在三种层面上与 Runtime 系统进行交互: 1 通过 Objective-C 源代码 2 通过 Foundation 框架的 NSObject 类定义的方法 3 通过对 Runtime 库函数的直接调用 Objective-C 源代码 多数情况我们只需要编写 OC 代码即可,Runtime 系统自动在幕后搞定一切,还记得简介中如果我们调用方法,编译器会将 OC 代码转换成运行时代码,在运行时确定数据结构和函数。 通过 Foundation 框架的 NSObject 类定义的方法 Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy 类时个例外,它是个抽象超类) 一些情况下,NSObject 类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如 -description 方法,该方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject 类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject 的子类可以重新实现。 还有一些 NSObject 的方法可以从 Runtime 系统中获取信息,允许对象进行自我检查。例如: • -class方法返回对象的类; • -isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量); • -respondsToSelector: 检查对象能否响应指定的消息; • -conformsToProtocol:检查对象是否实现了指定协议类的方法; • -methodForSelector: 返回指定方法实现的地址。 通过对 Runtime 库函数的直接调用 Runtime 系统是具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下,这意味着我们使用时只需要引入objc/Runtime.h头文件即可。 许多函数可以让你使用纯 C 代码来实现 Objc 中同样的功能。除非是写一些 Objc 与其他语言的桥接或是底层的 debug 工作,你在写 Objc 代码时一般不会用到这些 C 语言函数。对于公共接口都有哪些,后面会讲到。我将会参考苹果官方的 API 文档。 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%@", [NSString stringWithUTF8String:propertyName]); }

//获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i%@", NSStringFromSelector(method_getName(method)));
}

//获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i%@", [NSString stringWithUTF8String:ivarName]);
}

//获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i%@", [NSString stringWithUTF8String:protocolName]);
}
复制代码

在Xcode上跑一下看看输出吧,需要给你当前的类写几个属性,成员变量,方法和协议,不然获取的列表是没有东西的。 注意,调用这些获取列表的方法别忘记导入头文件#import 。 方法调用 让我们看一下方法调用在运行时的过程(参照前文类在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的四个参数:
  5. id object给谁设置关联对象。
  6. const void *key关联对象唯一的key,获取时会用到。
  7. id value关联对象。
  8. 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的两个参数。
  9. id object获取谁的关联对象。
  10. 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

@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面向切面编程。 • 既然是切面,就一定不要忘记,交换完再调回自己。 • 一定要保证只交换一次,否则就会很乱。 • 最后,据说这个技术很危险,谨慎使用

转载于:https://juejin.im/post/5a308a075188252ae93aed12

你可能感兴趣的:(对Runtime的理解)