[iOS]多代理模式的设计与实现

原文地址:https://www.stephenw.cc/post/5RhkyFSozS

背景

电商类应用中,购物车的本地数据结构是相对比较复杂的,并且购物车是一个共享的组件,在其他页面的数据更新比较难及时同步到所有引用它的 instance页面,如果不注意设计,这个共享的组件很容易造成非常高的耦合度
多代理模式是日常开发中很少被提及的一个概念,但多代理在这些独特的场景下有着独特的功用。
使用代理可以通过中间层降低数据层和视图的耦合度,代理可以做到实时通知

避免使用NSNotification这种模式通知数据更新

alias

本文假设 购物车数据结构这个组件叫做 DB
需要接收数据更新的视图组件以 view_count的形式命名,如view_1
编程语言: Objective-C

设计

多代理意味着 DB需要持有 view_1..view_n的 instance,在数据更新的时候依次把结果通知到代理方法,我们需要一个数组存储这些 instance。

注意 代理的 instance需要被 weak reference,否则 view的释放会造成 DB持有野指针

为了解决数组的 weak reference,我选用了 NSPointerArray,Apple 文档如是介绍:

The NSPointerArray class represents a mutable collection modeled after NSArray, but can also hold nil values. nil values may be inserted or removed and contribute to the object’s count. An NSPointerArray object can also increase and decrease its count directly.

这表明 NSPointerArray是可以跟踪集合中的对象内存的,并且它是 mutable的。

为了耦合度更低,这个方法更通用,我编写了一个独立的类来表示这种多代理模式

NVMMultidelegate

我的 namespace是 NVM,可随意更改,无需太在意

定义 interface

#import 

NS_ASSUME_NONNULL_BEGIN

@interface NVMMultiDelegate : NSObject

@property (nonatomic, readonly) NSPointerArray *delegates;

- (void)addDelegate:(id)delegate;
- (void)removeDelegate:(id)delegate;

@end

NS_ASSUME_NONNULL_END

对外仅暴露 readonly的 delegates,和增删 delegate的方法。

Init

- (instancetype)init {
  if (self = [super init]) {
    _delegates = [NSPointerArray weakObjectsPointerArray];
  }
  return self;
}

在初始化 delegates的 Ivars的时候,指定其按照弱引用的内存管理方式来跟踪集合内的指针

inherited interface

添加对象指针

- (void)addDelegate:(id)delegate {
  [_delegates addPointer:(__bridge void*)delegate];
}  

NSArray在概念上不一样的地方是,NSPointerArray需要添加的是对象的指针地址,尽管他俩都是在操作指针。所以在添加对象时,需要将其转换为指针类型,__bridge转换十分具有 CoreFoundation特色

移除对象指针


- (void)removeDelegate:(id)delegate {
  NSUInteger index = [self indexOfDelegate:delegate];
  if (index != NSNotFound) {
    [_delegates removePointerAtIndex:index];
  }
  [_delegates compact];
}

- (NSUInteger)indexOfDelegate:(id)delegate {
  for (NSUInteger i = 0; i < _delegates.count; i += 1) {
    if ([_delegates pointerAtIndex:i] == (__bridge void*)delegate) {
      return i;
    }
  }
  return NSNotFound;
}
  

移除就稍显麻烦了,因为 NSPointerArray并不像 NSArray那样具有非常便捷的 API,这也是它自己的功用造成的结果。所以我们需要自己写一下 indexOf API,考虑到多代理模式的对象并不会非常多,我们就用普通的快速遍历好了。

核心:事件转发

在 Objc的响应链中,判断一个对象是否可以执行 Selector可以使用 respondsToSelector方法,首先我们需要复写这个方法,让调用者知道我们的多代理对象可以响应它存的delegates数组里对象的方法

- (BOOL)respondsToSelector:(SEL)aSelector {
  if ([super respondsToSelector:aSelector]) {
    return YES;
  }
  for (id delegate in _delegates) {
    if (delegate && [delegate respondsToSelector:aSelector]) {
      return YES;
    }
  }
  return NO;
}  

调用者在得知可以响应开始调用后,Objc在 Runtime时期会去取方法签名,通过复写这个方法,我们替换掉默认的方法签名,让调用者可以获得他想要在 delegates对象数组里的方法签名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
  if (signature) {
    return signature;
  }
  [_delegates compact];
  for (id delegate in _delegates) {
    if (!delegate) {
      continue;
    }
    signature = [delegate methodSignatureForSelector:aSelector];
    if (signature) {
      break;
    }
  }
  return signature;
}

注意 [_delegates compact],这个方法可以帮助你去掉数组里面的野指针,避免你在快速遍历的时候拿到一个指向不存在对象的地址

在拿到方法签名后,接下来会去调用,同样地,我们需要复写这个方法

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  SEL selector = [anInvocation selector];
  BOOL responded = NO;
  for (id delegate in _delegates) {
    if (delegate && [delegate respondsToSelector:selector]) {
      [anInvocation invokeWithTarget:delegate];
      responded = YES;
    }
  }
  if (!responded) {
    [self doesNotRecognizeSelector:selector];
  }
}

这样可以保证添加在 delegates数组里面所有能响应这次调用得代理对象都获得调用,如果没有任何对象能响应这次调用,那肯定是会造成一次未识别 selector的调用,这个调用一般会产生 exception,造成应用闪退,如果你不想在这个未得到灰度验证的类中发生 crash,最好能覆盖一下 doesNotRecognizeSelector方法

使用

在使用这个类时,只需要声明一个 property:@property (nonatomic, strong) id multidelegate

声明其为 id类型只是希望在 build time能通过方法检查,因为多代理方法并没有显式声明在这个类上

比如你可以把 tableview的 delegate和 datasouce 通过[multidelegate addDelegate:tableViewDelegate]的方式添加到 multidelegate中,然后指定

tableview.delegate = multidelegate; 
tableview.dataSource = multidelegate

就可以了,当然这只是演示其用法,最好的方法是 DB设置多个页面到 multidelegate,DB在更新数据时只需调用一次 [multidelegate didUpdateData:data]这种模式就可以了

注意, NSPointerArray因为有跟踪内存的作用,所以它的性能并不是非常好,并不推荐把应用程序声明周期里的代理添加到同一个 multidelegate中,这数量可能到达上千个,正好是性能瓶颈体现比较明显的数量

你可能感兴趣的:([iOS]多代理模式的设计与实现)