运用享元思想写了一个百花池demo,需要源码请看文末!!!😶🌫️
以程序设计围棋为例
围棋棋盘上有19 X 19个空位可以放置棋子,所以在理论上每局棋可能有361个♟棋子对象产生
如果有一网络围棋游戏,要支持多玩家同时在线,随着玩家数量逐渐庞大,就需要很强大的服务器支持,显然这很耗费内存,这时就应该换种思路来实现游戏,毕竟内存空间是有限的
分析
- 我们发现每一个棋子其实本质上是一样的,颜色(只有黑、白)也是不常变化的
- 而每一个棋子的位置是不固定的
一个完整的棋子包括: 棋子本身(黑/白) + 棋盘坐标
结论
- 我们只要2个棋子对象就够了,在外部保存一个数组存储坐标信息
- 使用时,我们只需根据key(黑、白)取到对应的棋子,然后把坐标信息传递给棋子,这样我们就得到对应完整棋子对象
模式定义
享元模式:运用共享技术有效地支持大量细粒度的对象
享元模式(Flyweight Pattern)是一种结构设计模式,专门用于设计可共享对象,尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象,目的是减少创建对象的数量,以减少内存占用和提高性能
简而言之,就是多对象的多复用,在面向对象程序设计中,利用公共对象不仅能节省资源还能提高性能
结构关系图
实现享元模式需要两个关键组件,通常是可共享的享元对象和保存它们的池
某种中央对象维护这个池中的享元对象,并根据父类型返回各种类型的具体享元对象
某些(或多数)对象的独特状态(外在状态)可以拿到外部(围棋坐标),在别处管理,其余部分(棋子颜色 黑/白)被共享
先来看看它们的静态关系类图:
Flyweight
是两个具体享元类ConcreteFlyweight1
和ConcreteFlyweight2
的父接口(协议),前者声明、后者实现operation:extrinsicState
方法
每个具体享元类维护不能用于识别对象的内在状态intrinsicState
,即享元对象可被共享的部分
客户端Client
向operation:
消息提供extrinsicState
,补充缺少的信息,让享元对象唯一
这里intrinsicState
就是棋子颜色,extrinsicState
就是棋子坐标
在iOS中,享元模式通过以下两种方式来实现:
- 内部状态(Intrinsic State):这是享元对象共享的数据,它是不可变的,可以在不同对象之间共享。通常存储在享元对象的属性中
- 外部状态(Extrinsic State):这是与享元对象特定实例相关的数据,它可以变化,并传递给享元对象的方法。外部状态通常作为参数传递给享元对象的方法
通过将内部状态和外部状态分离,可以在多个对象之间共享内部状态,而不需要为每个对象都存储一份相同的数据。这样可以大大减少内存消耗。
下图显示了运行时FlyweightFactory
的实例如何管理池中的享元对象:
何时使用享元模式
当满足以下所有条件时,自然会考虑使用这个模式:
- 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式
创建百花池示例demo
在实际的iOS开发中,享元模式可以应用于许多场景,例如:
- UITableView和UICollectionView中的重用机制,通过重用单元格来避免创建大量相同的视图对象
- 字符串池(String Pool)的实现,避免重复创建相同的字符串对象
- 图片缓存,通过共享相同图片的实例来避免重复加载和内存占用
目标
下面我们要开发一个小demo,该程序在屏幕上随机显示上百个花朵🌺图案,但只使用了6个实例
现我们有如图所示的六种花朵图案:银莲花、 大波斯菊、非洲菊、蜀葵、茉莉和百日菊
每种图案由一个唯一的FlowerView
的实例(总共6个)来维护,与所需花朵种数无关,随机生成200个花朵图案的效果如图:
分析
FlowerView
FlowerView是UIImageView的子类,用它绘制一朵花朵图案。FlowerFactory是享元工厂类,它管理一个FlowerView实例的池
尽管工厂池中对象是FlowerView,但要求FlowerFactory返回UIView的实例。与让工厂直接返回UIImage类型的最终产品相比,这样的设计更灵活。因为有时候我们可能需要自定义绘制其它的图案,而不只是显示固定的图像
UIView是任何需要在屏幕上绘图的最高抽象。FlowerFactory可以返回任何UIView子类对象,而不会破坏系统。这就是针对接口编程,而不是针对实现编程
的好处
FlowerFactory
FlowerFactory用flowerPool聚合了一个花朵池的引用。flowerPool是一个保存FlowerView的所有实例的数据结构
FlowerFactory通过flowerViewWithType: 方法返回FlowerView实例
如果池子中没有所请求花朵的类型,就会创建一个新的FlowView实例返回,并保存到池子中
代码
FlowerView.h和FlowerView.m
@interface FlowerView : UIImageView
@end
@implementation FlowerView
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
- (void)drawRect:(CGRect)rect {
[self.image drawInRect: rect];
}
@end
这里重载了UIImageView的drawRect:rect
方法,重新声明仅为了说明此类实现了哪些方法
FlowerFactory.h
typedef enum {
kAnemone = 0, kCosmos, kGerberas, kHollyhock, kJasmine,
kZinnia, kTotalNumberOfFlowerTypes
} FlowerType;
@interface FlowerFactory : NSObject
//私有变量
@property (nonatomic, strong)NSMutableDictionary* flowerPool;
- (UIView *)flowerViewWithType: (FlowerType)type;
@end
可以用一组枚举值enum
来表示花朵的类型。显然,kTotalNumberOfFlowerTypes
是所支持花朵类型的总数
FlowerFactory有一个私有的可变字典flowerPool
,表示一个保存整个可供返回花朵的池
flowerViewWithType:(FlowerType)type
根据枚举参数type
返回特定UIView实例,也就是用来决定来返回哪种花
FlowerFactory.m
@implementation FlowerFactory
- (UIView *)flowerViewWithType:(FlowerType)type {
//懒加载花朵池
if (!self.flowerPool) {
self.flowerPool = [[NSMutableDictionary alloc] initWithCapacity: kTotalNumberOfFlowerTypes];
}
//尝试从池中取出某种类型的一朵花
FlowerView* flowerView = [self.flowerPool objectForKey: [NSNumber numberWithInt: type]];
//若请求的类型池中不存在,就创建并加到池子里
if (!flowerView) {
UIImage* flowerImage = nil;
switch (type) {
case kAnemone:
flowerImage = [UIImage imageNamed: @"anemone.png"];
break;
case kCosmos:
flowerImage = [UIImage imageNamed: @"cosmos.png"];
break;
case kGerberas:
flowerImage = [UIImage imageNamed: @"gerberas.png"];
break;
case kHollyhock:
flowerImage = [UIImage imageNamed: @"hollyhock.png"];
break;
case kJasmine:
flowerImage = [UIImage imageNamed: @"jasmine.png"];
break;
case kZinnia:
flowerImage = [UIImage imageNamed: @"zinnia.png"];
break;
default:
break;
}
flowerView = [[FlowerView alloc] initWithImage: flowerImage];
[self.flowerPool setObject: flowerView forKey: [NSNumber numberWithInt: type]];
}
return flowerView;
}
@end
ExtrinsicFlowerState
享元对象总是和某种可共享的内在状态联系在一起,尽管并不完全如此,但是我们的FlowerView享元对象确实共享了作为其内在状态的内部花朵图案。不管享元对象是否有可供共享的内在状态,任然需要定义某种外部的数据结构,保存享元对象的外在状态
每朵花都有各自的显示区域,所以这需要作为外在状态来处理
这里特此定义了一个C结构体ExtrinsicFlowerState
typedef struct ExtrinsicFlowerState {
__unsafe_unretained UIView* flowerView;
CGRect area;
} ExtrinsicFlowerState;
FlowerContainerView.h和FlowerContainerView.m
创建一视图用于显示花朵,定制视图
@interface FlowerContainerView : UIView
@property (nonatomic, copy)NSArray* flowerList;
@end
@implementation FlowerContainerView
//把生成的所有花朵实例都填充到FlowerContainerView视图上
- (void)drawRect:(CGRect)rect {
for (NSValue* stateValue in self.flowerList) {
ExtrinsicFlowerState flowerExState;
[stateValue getValue: &flowerExState];
UIView* flowerView = flowerExState.flowerView;
CGRect frame = flowerExState.area;
[flowerView drawRect: frame];
}
}
@end
重载UIView的drawRect:
方法(定义定制的绘图操作),在屏幕上绘制花朵
遍历了一个包含 NSValue 对象的self.flowerList
数组,并从 NSValue 对象中提取出自定义结构体 ExtrinsicFlowerState 的值
在每次遍历中,代码提取了flowerView
和area
属性值,调用了flowerView
的drawRect:
方法,传入frame
作为参数。
通过遍历self.flowerList
中的花朵状态数据,获取花朵的视图和位置,并在相应的位置上进行绘制。
客端户代码的实现
现所有部分都到位了,我们来看看客户端如何使用这个享元基础设施绘制大量花朵
ViewController.h
@interface ViewController : UIViewController
@property (nonatomic, strong)FlowerFactory* flowerFactory;
@end
在根视图的viewDidLoad
方法中调用
#define kFlowerListCount 200
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//创建好显示花朵的视图
FlowerContainerView* flowerContainer = [[FlowerContainerView alloc] initWithFrame: self.view.bounds];
flowerContainer.backgroundColor = [UIColor whiteColor];
[self.view addSubview: flowerContainer];
//创建花朵池子(工厂)
FlowerFactory* factory = [[FlowerFactory alloc] init];
self.flowerFactory = factory;
//构建花朵列表
NSMutableArray* flowerList = [NSMutableArray arrayWithCapacity: kFlowerListCount];;
for (NSInteger i = 0; i < kFlowerListCount; ++i) {
//从花朵工厂随机取得一个共享的花朵享元对象实例
FlowerType type = arc4random() % kTotalNumberOfFlowerTypes;
//整个遍历过程中,这段代码中的方法只会创建6个对象实例,然后赋给UIView实例
UIView* flowerView = [factory flowerViewWithType: type];
//设置花朵的显示位置和区域
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGFloat x = arc4random() % (NSInteger)screenBounds.size.width;
CGFloat y = arc4random() % (NSInteger)screenBounds.size.height;
NSInteger minSize = 10;
NSInteger maxSize = 60;
CGFloat size = (arc4random() % (maxSize - minSize)) + minSize;
//把花朵参数存入一个外在对象
ExtrinsicFlowerState extrinsicState;
extrinsicState.flowerView = flowerView;
extrinsicState.area = CGRectMake(x, y, size, size);
//花朵外在状态添加到花朵列表中
//ExtrinsicFlowerState不是OC对象,需要用@encode把它编码成NSValue对象才能安全地添加到数组
//将extrinsicState的值打包,并将其作为对象添加到flowerList数组中
[flowerList addObject: [NSValue value: &extrinsicState withObjCType: @encode(ExtrinsicFlowerState)]];
}
[flowerContainer setFlowerList: flowerList];
}
@end
每次执行[flowerContainer setFlowerList: flowerList];
这段代码,flowerList
更新一次,FlowerContainerView
就会自动执行重载的drawRect:
方法
也就是将构造好的花朵列表赋给定制视图,执行绘图操作,最终显示列表中的每朵花
优缺点&总结
优缺点
优点
- 享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享
缺点
- 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化
- 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长
模式分析
分享是人类的美德。分享相同的资源以执行任务,可能比使用个人的资源完成相同的事情更加高效
在本文中,我们设计了一个可以在屏幕上显示上百个花朵的程序,而只用了6个不同花朵图案的实例。
这些不同的花朵实例,把一些与众不同的可被标识的信息(位置和大小)去掉,只剩下显示花朵图案的基本操作。
在请求特定的花朵时,客户端需要像花朵实例提供某些与众不同的信息(外在状态),让ta使用这些信息就只一朵与众不同的花。
不用享元模式的话,要在屏幕上画多少话,程序就要实例化多少个UIImageView。经过精心的设计,享元模式可以通过共享一部分必需的对象,来节省大量内存。
UITableViewCell复用机制
通过分析,享元模式的思想不正是之前学习UI时所接触到的cell的复用问题吗?
现在来回顾一下:
简单的测试代码
- 在 vc中, 创建了一个 taleview,并加到 vc.view 上
- 为了方便统计cell创建个数,以及复用个数。 自定义了 MineCell
- 查看官方文档后,发现:
-
- tableViewCell创建时, 会执行 initWithStyle:reuseIdentifier: 方法
-
- tableviewCell复用时,会执行 prepareForReuse 方法
所以我们在 MineCell 中覆写以上两个方法来统计 createCnt 和 reuseCnt
- tableviewCell复用时,会执行 prepareForReuse 方法
- (void)viewDidLoad {
[super viewDidLoad];
UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
tableView.delegate = self;
tableView.dataSource = self;
tableView.rowHeight = 200;
[tableView registerClass:[MineCell class] forCellReuseIdentifier:@"cell"];
[self.view addSubview:tableView];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 10000;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
MineCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
return cell;
}
MineCell.m
#import "MineCell.h"
@implementation MineCell
int createCnt = 0;
int reuseCnt = 0;
- (void)prepareForReuse{
NSLog(@"复用了 %d 次",++reuseCnt);
[super prepareForReuse];
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
NSLog(@"创建了 %d 次",++createCnt);
return [super initWithStyle:style reuseIdentifier:reuseIdentifier];
}
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
@end
结果如下:
【Github】百花池demo