Objective-C runtime常见用法

jopen 8年前

runtime是Objective-C上一个非常强大的屠龙刀,提供了很多奇幻的魔法,当然,如果过度滥用的话,维护上的代价也是显而易见的。

我们这里只讨论一下我们平常工作中常用的特性,当然,它有大量功能,只是我们并不一定用的到,类似objc_msgSend这种的我们也不作介绍。

Objective-C runtime已经开源了,有阅读源码习惯的程序员可以前往官网下载阅读。

下面是下载地址: http://www.opensource.apple.com/tarballs/objc4/

添加、获取属性

以开源库 SVPullToRefresh (SVPullToRefresh是一个提供上下拉刷新的库)举例。

在 UIScrollView+SVPullToRefresh 这个Category上, SVPullToRefresh  给UIScrollView动态添加了一个属性,我们以 SVPullToRefreshView *pullToRefreshView 这个属性举例。

在 UIScrollView+SVPullToRefresh.h 上先申明了这个属性

@property (nonatomic, strong, readonly) SVPullToRefreshView *pullToRefreshView;

之后,在 UIScrollView+SVPullToRefresh.m 中重写了它的Setter和Getter方法,分别如下:

Setter:

- (void)setPullToRefreshView:(SVPullToRefreshView *)pullToRefreshView {      //[self willChangeValueForKey:@"SVPullToRefreshView"];      objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,                               pullToRefreshView,                               OBJC_ASSOCIATION_ASSIGN);      //[self didChangeValueForKey:@"SVPullToRefreshView"];  }

我们将注释掉的两行忽略,只看中间的一行:

objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,                                 pullToRefreshView,                                 OBJC_ASSOCIATION_ASSIGN);

语法结构如下:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

这个方法用于使用一个给定的Key和policy,将对象和值(Value)相关联。

object:需要关联的对象

key:用于关联的Key

value:使用Key来关联在对象上的值,如果设置为nil,则清除已绑定的值。

policy:关联的政策,提供assign、retain、copy等政策。

Getter:

- (SVPullToRefreshView *)pullToRefreshView {      return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView);  }

和上面类似,这里使用的是runtime里面的这个方法:

id objc_getAssociatedObject(id object, const void *key)

这个方法用于获取对象相关联的值,与上面的方法相呼应。 objc_getAssociatedObject 这种方式只能获取已知的属性,如果一个类有很多属性,但是我们可能并不知道它的具体名称和类型,那怎么办呢?

我们以 JsonModel 举例, JsonModel 是一个 Json 转 Model 的一个库,一般用于将从服务端或者某处获得的 Json 字符串映射到对象上,并完成填充工作。

我们以官方文档上的例子举例:

#import "JSONModel.h"    @interface CountryModel : JSONModel    @property (strong, nonatomic) NSString* country;    @end

我们写了一个类,名叫 CountryModel ,继承于 JSONModel ,我们在 CountryModel 中声明了名叫 country 的一个 NSString 类型的属性。

之后,我们将获取到的 Json 字符串进行填充的时候,它就自动填充好了。

#import "CountryModel.h"  ...    NSString* json = (fetch here JSON from Internet) ...  NSError* err = nil;  CountryModel* country = [[CountryModel alloc] initWithString:json error:&err];

如果不考虑一些其他东西的话,这要比我们手写赋值要简单,起码可以省一些代码。那,它是怎么实现的呢,我们看一下它的源代码:

JsonModel.m

-(void)__inspectProperties  {      ...          while (class != [JSONModel class]) {          //JMLog(@"inspecting: %@", NSStringFromClass(class));            unsigned int propertyCount;          objc_property_t *properties = class_copyPropertyList(class, ∝ertyCount);              for (unsigned int i = 0; i < propertyCount; i++) {                JSONModelClassProperty* p = [[JSONModelClassProperty alloc] init];                //get property name              objc_property_t property = properties[i];              const char *propertyName = property_getName(property);              p.name = @(propertyName);      ...  }

这个是 JsonModel 中最主要的方法之一,比较长,我只摘取其中的一部分,忽略了很多其他特性,比如它使用protocol的方式去给属性添加option等描述,或者从protocol上取类名,再给数组赋值。

JsonModel 会遍历Model,通过 class_copyPropertyList 方法来获取类上所有的属性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

通过 property_getName 方法来获取属性名称: const char *property getName(objc property_t property)

之后会依据此名称,动态创建setter和getter方法,这里不提。

objc_getAssociatedObject 、 class_copyPropertyList 、 property_getName 和 objc_setAssociatedObject 方法让我们在开发中,可以很方便的对对象进行属性获取和属性添加的操作。

添加、获取、替换方法

苹果提供了Category等方式,使得我们可以很简单的给已知类添加方法。但是在很多时候,我们并不能明确的知道我们要添加的方法叫什么,这个时候我们可以使用runtime提供的一些方法。

以开源库 Aspects 为例,Aspects是一个拦截器,它可以在某个方法运行前、运行后进行拦截,来加载其他的一些操作,或者替换整个方法。

使用方式类似于下面这种:

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {      NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);  } error:NULL];

如果现在有一个需求,要给所有VC的 viewWillAppear: 方法上加一个log的方法,我们可能会使用继承的办法,在基类上面重写 viewWillAppear: 方法,并添加打印的办法。除此之外,使用Aspects拦截 viewWillAppear: ,然后在执行之前或者执行之后添加log方法,也是一种办法,这里并不探讨孰优孰劣。

我们来看一下它的实现原理:

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {      NSCParameterAssert(selector);      Class klass = aspect_hookClass(self, error);      Method targetMethod = class_getInstanceMethod(klass, selector);      IMP targetMethodIMP = method_getImplementation(targetMethod);      if (!aspect_isMsgForwardIMP(targetMethodIMP)) {          // Make a method alias for the existing method implementation, it not already copied.          const char *typeEncoding = method_getTypeEncoding(targetMethod);          SEL aliasSelector = aspect_aliasForSelector(selector);          if (![klass instancesRespondToSelector:aliasSelector]) {              __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);              NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);          }            // We use forwardInvocation to hook in.          class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);          AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));      }  }

这是hook类并添加或者替换方法的方法。 这里入参上面的selector即我们的@selector(viewWillAppear:) 。

Method targetMethod = class_getInstanceMethod(klass, selector);  IMP targetMethodIMP = method_getImplementation(targetMethod);

class_getInstanceMethod 方法可以获取到指定类的指定方法,返回的是一个Method,而通过 method_getImplementation 方法获取到Method结构体的IMP指针。 (这个Method是一个结构体,这里不多说明,大家可以自行查看一下runtime的源码中对Method结构体的定义,在 objc-private.h 中可以找到,或者你可以参考我的另一篇博客 Objective-C中为什么不支持泛型方法 。)

SEL aliasSelector = aspect_aliasForSelector(selector);

之后,使用 aspect_aliasForSelector 方法,给selector添加了一个前缀,新的方法名为 aspects__viewWillAppear: 。

__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);

class_addMethod 方法会给类动态添加方法,这里就是给 UIViewController 添加了一个 aspects__viewWillAppear 方法,而这个方法的实现,依然是原来 viewWillAppear: 方法的实现。

到这一步,Aspects只是给类添加了一个 aspects__viewWillAppear: 方法,并没有hook进去。

class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);

class_replaceMethod 方法,顾名思义,用来替换方法的。这个方法使用 aspects__viewWillAppear: 替换了 viewWillAppear: 方法,但是类里面并没有实现 aspects__viewWillAppear: 方法,这个意思就是说,当执行 viewWillAppear: 方法的时候,就会去执行 aspects__viewWillAppear: ,而 aspects__viewWillAppear: 这个方法我们并没有实现。那会不会挂呢。。。

Aspects这里使用了一个特殊的办法来hook。

- (void)forwardInvocation:(NSInvocation *)anInvocation

NSObject中提供了 forwardInvocation: 方法,这个方法会在对象收到一个无法响应的selector之后,给 forwardInvocation: 方法发一条消息,或者说调用一下NSObject的这个方法。

我们回到最上面的一条方法:

Class klass = aspect_hookClass(self, error);

这个方法会调用这么几个方法,其中有一个是:

static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";  static void aspect_swizzleForwardInvocation(Class klass) {      NSCParameterAssert(klass);      // If there is no method, replace will act like class_addMethod.      IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");      if (originalImplementation) {          class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");      }      AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));  }

上面的这个方法会用 __aspects_forwardInvocation: 方法来替换 forwardInvocation: 方法。Aspects创建了AspectInfo这个类,里面包含类实例和一个 NSInvocation ,这个 NSInvocation 就来自于 forwardInvocation: 方法的传参。Aspects再将AspectInfo实例关联到对象上。

当程序执行 viewWillAppear: 时,就会执行替换过的 aspects__viewWillAppear: 方法,由于并没有这个方法的实现,那么就会走 forwardInvocation: 方法,这个方法也已经被替换为 __aspects_forwardInvocation: 方法,那么最终走的就会是 __aspects_forwardInvocation: 这个方法。

OK,我们理清了。

我们继续:

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {      NSCParameterAssert(self);      NSCParameterAssert(invocation);      SEL originalSelector = invocation.selector;      SEL aliasSelector = aspect_aliasForSelector(invocation.selector);      invocation.selector = aliasSelector;      AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);      AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);      AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];      NSArray *aspectsToRemove = nil;        // Before hooks.      aspect_invoke(classContainer.beforeAspects, info);      aspect_invoke(objectContainer.beforeAspects, info);        // Instead hooks.      BOOL respondsToAlias = YES;      if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {          aspect_invoke(classContainer.insteadAspects, info);          aspect_invoke(objectContainer.insteadAspects, info);      }else {          Class klass = object_getClass(invocation.target);          do {              if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {                  [invocation invoke];                  break;              }          }while (!respondsToAlias && (klass = class_getSuperclass(klass)));      }        // After hooks.      aspect_invoke(classContainer.afterAspects, info);      aspect_invoke(objectContainer.afterAspects, info);        // If no hooks are installed, call original implementation (usually to throw an exception)      if (!respondsToAlias) {          invocation.selector = originalSelector;          SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);          if ([self respondsToSelector:originalForwardInvocationSEL]) {              ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);          }else {              [self doesNotRecognizeSelector:invocation.selector];          }      }        // Remove any hooks that are queued for deregistration.      [aspectsToRemove makeObjectsPerformSelector:@selector(remove)];  }

这里有个宏定义, aspect_invoke ,这个方法是这样的:

#define aspect_invoke(aspects, info) \  for (AspectIdentifier *aspect in aspects) {\      [aspect invokeWithInfo:info];\      if (aspect.options & AspectOptionAutomaticRemoval) { \          aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \      } \  }

aspect_invoke 方法会遍历 AspectsContainer 上绑定的 AspectIdentifier ,然后去执行 AspectIdentifier 上的 - (BOOL)invokeWithInfo:(id<AspectInfo>)info 方法,这个方法中会执行 [blockInvocation invokeWithTarget:self.block]; 方法,而这个self.block就是

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector                        withOptions:(AspectOptions)options                         usingBlock:(id)block                              error:(NSError **)error;

这个方法上的block。

所以 __ASPECTS_ARE_BEING_CALLED__ 方法在执行上,会先去执行 beforeAspects 数组中的 AspectIdentifier ,之后执行 insteadAspects 数组中的 AspectIdentifier ,最后到 afterAspects 。这就实现了在方法前和方法后的hook。

class_getInstanceMethod 、 class_addMethod 、 class_replaceMethod 方法,让我们在开发中可以很方便的获取、添加、替换对象的方法。当然,如果你想获取对象所有的方法的话,你可以使用 Method class_getInstanceMethod(Class cls, SEL name) 这个方法。

交换方法

UITextView+Placeholder 是一个用来给 UITextView 添加Placeholder的category,在它的源码里面有这么一个方法:

- (void)swizzledDealloc {      [[NSNotificationCenter defaultCenter] removeObserver:self];      UILabel *label = objc_getAssociatedObject(self, @selector(placeholderLabel));      if (label) {          for (NSString *key in self.class.observingKeys) {              @try {                  [self removeObserver:self forKeyPath:key];              }              @catch (NSException *exception) {                  // Do nothing              }          }      }      [self swizzledDealloc];  }

我们在 swizzledDealloc 方法最后看到了这么一行:

[self swizzledDealloc];

这不死循环了嘛!其实不是。

在这个类的load方法上,已经将 dealloc 方法和 swizzledDealloc 方法进行交换了。我们看下面的代码:

+ (void)load {      [super load]; method_exchangeImplementations(class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")),                                     class_getInstanceMethod(self.class, @selector(swizzledDealloc)));  }

我们在调用 dealloc 的时候,实际上会去走 swizzledDealloc 方法,而在 swizzledDealloc 方法中调用 swizzledDealloc 方法,会去走真正的 dealloc 方法。

这种方法可以实现一定的hook,通过这种方式我们依然可以在某个方法执行前、执行后添加代码,或者直接替换方法。

获取、添加类

这是一个很有意思的东西。

现在,某个实习生接到了这么一个任务:目前项目中都是使用UIAlertView,现在需要在iOS8上使用UIAlertController来代替UIAlertView,而在iOS8下,依然保持原样。

项目现在比较大,那怎么办呢,他想到了一个办法,做了一个XXAlertView,所有人使用AlertView的时候,都使用XXAlertView, XXAlertView接口和UIAlertView接口比较一致,修改的量倒不是很大。但是如果以后某个实习生不小心忘记使用XXAlertView,而直接使用了UIAlertController,导致程序挂了怎么办呢?那是否要使用另一个脚本或工具来保证这种事情不会发生呢?

当然,这是一种办法,有没有其他更简单的办法呢?有! (当然,我不是说上面的办法不好,也不是说下面的办法更好,这只是一种方式。)

我们可以对iOS8以下的系统添加一个类,名叫UIAlertController,API与系统UIAlertController保持一致。开发人员在以后的程序开发中,都写UIAlertController,他不用时刻提醒自己,要使用XXAlertView,完全可以按照自己的习惯写。哪怕他没写UIAlertController,而写了UIAlertView也没事,好歹可以正常工作的。

FDStackView 是一个ForkingDog组织开发和维护的一个开源项目。

UIStackView 是iOS9上新增加的一种很方便进行流式布局的工具,很好,很强,但是只在iOS9及其以上。所以,他们做了一个 FDStackView 。

只需要将代码添加到工程中,不需要import。什么也不用做,就和平时写 UIStackView 一样。

在iOS9及其以上,自然会调用系统的 UIStackView ,而在iOS9以下,会使用 FDStackView 替换 UIStackView 。

怎么实现的呢,我们看源码:

// ----------------------------------------------------  // Runtime injection start.  // Assemble codes below are based on:  // https://github.com/0xced/NSUUID/blob/master/NSUUID.m  // ----------------------------------------------------    #pragma mark - Runtime Injection    __asm(        ".section        __DATA,__objc_classrefs,regular,no_dead_strip\n"  #if TARGET_RT_64_BIT        ".align          3\n"        "L_OBJC_CLASS_UIStackView:\n"        ".quad           _OBJC_CLASS_$_UIStackView\n"  #else        ".align          2\n"        "_OBJC_CLASS_UIStackView:\n"        ".long           _OBJC_CLASS_$_UIStackView\n"  #endif        ".weak_reference _OBJC_CLASS_$_UIStackView\n"        );    // Constructors are called after all classes have been loaded.  __attribute__((constructor)) static void FDStackViewPatchEntry(void) {      static dispatch_once_t onceToken;      dispatch_once(&onceToken, ^{          @autoreleasepool {                // >= iOS9.              if (objc_getClass("UIStackView")) {                  return;              }                Class *stackViewClassLocation = NULL;    #if TARGET_CPU_ARM              __asm("movw %0, :lower16:(_OBJC_CLASS_UIStackView-(LPC0+4))\n"                    "movt %0, :upper16:(_OBJC_CLASS_UIStackView-(LPC0+4))\n"                    "LPC0: add %0, pc" : "=r"(stackViewClassLocation));  #elif TARGET_CPU_ARM64              __asm("adrp %0, L_OBJC_CLASS_UIStackView@PAGE\n"                    "add  %0, %0, L_OBJC_CLASS_UIStackView@PAGEOFF" : "=r"(stackViewClassLocation));  #elif TARGET_CPU_X86_64              __asm("leaq L_OBJC_CLASS_UIStackView(%%rip), %0" : "=r"(stackViewClassLocation));  #elif TARGET_CPU_X86              void *pc = NULL;              __asm("calll L0\n"                    "L0: popl %0\n"                    "leal _OBJC_CLASS_UIStackView-L0(%0), %1" : "=r"(pc), "=r"(stackViewClassLocation));  #else  #error Unsupported CPU  #endif                if (stackViewClassLocation && !*stackViewClassLocation) {                  Class class = objc_allocateClassPair(FDStackView.class, "UIStackView", 0);                  if (class) {                      objc_registerClassPair(class);                      *stackViewClassLocation = class;                  }              }          }      });  }

FDStackView 使用了一段来自 NSUUID 的代码,有一些汇编,不好懂(其实一定意义上来说,这里面一部分已经不是runtime的特性了,不过也确实是在运行时去做的,我这里也就一并搬上来了)。大家有兴趣可以看一下 yaqing 对这段汇编的解释。

那么,上面代码中的 objc_getClass 就是获取类的一种方式,当然,还有 objc_lookUpClass 、 NSClassFromString 等方式。

添加类的话,可以使用 objc_addClass 方法。这与我们上面的举例不一样,主要是因为 FDStackView 类创建的类是需要去替换 UIStackView 类的,在iOS9以下, UIStackView 类即 FDStackView 类。

FDStackView 的这种做法很方便,但是,也有个问题,在低版本上,对 UIStackView 添加的category会失效,因为低版本上已经使用的是 FDStackView ,而不是UIStackView了,而category是添加在 UIStackView 上的,所以是不起作用的。 大家可以参考这篇文档: http://hailoong.sinaapp.com/?p=125

其他

runtime还有其他大量功能,这些都是黑魔法,用好了,省时省力,用不好,说不定哪天就是灾难。

</div>

来自: http://ifujun.com/objective-c-runtimechang-jian-yong-fa/