[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 <Foundation/Foundation.h>

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中,这数量可能到达上千个,正好是性能瓶颈体现比较明显的数量

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

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,928评论 6 13
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,131评论 30 470
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,291评论 0 6