【iOS】享元模式

运用享元思想写了一个百花池demo,需要源码请看文末!!!😶‍🌫️



以程序设计围棋为例

在这里插入图片描述

围棋棋盘上有19 X 19个空位可以放置棋子,所以在理论上每局棋可能有361个♟棋子对象产生

如果有一网络围棋游戏,要支持多玩家同时在线,随着玩家数量逐渐庞大,就需要很强大的服务器支持,显然这很耗费内存,这时就应该换种思路来实现游戏,毕竟内存空间是有限的

分析

  • 我们发现每一个棋子其实本质上是一样的,颜色(只有黑、白)也是不常变化的
  • 而每一个棋子的位置是不固定的

一个完整的棋子包括: 棋子本身(黑/白) + 棋盘坐标

结论

  • 我们只要2个棋子对象就够了,在外部保存一个数组存储坐标信息
  • 使用时,我们只需根据key(黑、白)取到对应的棋子,然后把坐标信息传递给棋子,这样我们就得到对应完整棋子对象

模式定义

享元模式:运用共享技术有效地支持大量细粒度的对象

享元模式(Flyweight Pattern)是一种结构设计模式,专门用于设计可共享对象,尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象,目的是减少创建对象的数量,以减少内存占用和提高性能

简而言之,就是多对象的多复用,在面向对象程序设计中,利用公共对象不仅能节省资源还能提高性能

结构关系图

实现享元模式需要两个关键组件,通常是可共享的享元对象保存它们的池

某种中央对象维护这个池中的享元对象,并根据父类型返回各种类型的具体享元对象

某些(或多数)对象的独特状态(外在状态)可以拿到外部(围棋坐标),在别处管理,其余部分(棋子颜色 黑/白)被共享

先来看看它们的静态关系类图:

请添加图片描述

Flyweight是两个具体享元类ConcreteFlyweight1ConcreteFlyweight2的父接口(协议),前者声明、后者实现operation:extrinsicState方法

每个具体享元类维护不能用于识别对象的内在状态intrinsicState,即享元对象可被共享的部分

客户端Clientoperation:消息提供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 的值

在每次遍历中,代码提取了flowerViewarea属性值,调用了flowerViewdrawRect:方法,传入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
- (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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值