设计模式13--享元模式

image

我们来做一个很简单的小程序:在界面上随机显示10万朵小花,这些小花只有6种样式。如图所示:

image

一看,这还不简单,直接创建10w个imageview显示不就是了,代码如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
//使用普通模式
    for (int i = 0; i < 100000; i++) {
        @autoreleasepool {
            CGRect screenBounds = [[UIScreen mainScreen] bounds];
            CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
            CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
            NSInteger minSize = 10;
            NSInteger maxSize = 50;
            CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;
            CGRect area = CGRectMake(x, y, size, size);
            
            FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
            //新建对象
            UIImageView *imageview = [self flowerViewWithType:flowerType];
            imageview.frame = area;
            [self.view addSubview:imageview];
        }
    }
}

- (UIImageView *)flowerViewWithType:(FlowerType)type
{
    UIImageView *flowerView = nil;
    UIImage *flowerImage;
    
    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 = [[UIImageView alloc]initWithImage:flowerImage];
    
    return flowerView;
}


觉得很好对吧?来,看看app占用的内存,如图:

image

占用内存153M,这还不把人吓死,这才一个页面,要是再多来两个页面,那app还不直接把内存撑爆啊。

我们使用instrument工具分析下,到底是哪里占用了过多的内存。截图如下:

image

可以看到内存的消耗主要是调用方法[self flowerViewWithType:flowerType]创建UIImageView导致的,进入这个方法再看看具体的内存分配,如图:

image

我们知道UIImageview的创建是很消耗内存的,这一下子创建10w个,内存占用可想而知。

那怎么解决呢?

分析知道,屏幕上的10W朵小花只有6种样式,只是在屏幕显示的位置不同。那能不能只创建6个UIImageview显示小花,然后重复利用这些UIImageView呢?

答案是肯定的,这就需要用到我们要讲的设计模式:享元模式。下面具体看看


定义

运用共享技术有效地支持大量细粒度的对象。

分析下上面的需求,我们需要创建10w个uiimageview来显示小花,其实这些小花样式大多都是重复的,只是位置不同,造成了内存浪费,解决方案就是缓存这些细粒度对象,让他们之创建一次,后续要使用直接从缓存中取就可以了。

但是要注意不是任何对象都可以缓存的,因为缓存的是对象的实例,实例存放的是属性,如果这些属性不断改变,那么缓存中的数据也必须跟着改变,那缓存就没有意义了。

所以我们需要把一个对象分为两个部分:不变和改变的部分。把不变的部分缓存起来,我们称之为内部状态,把改变的部分作为外部状态对外暴露,让外界去改变。对应到上面的程序,屏幕上显示的小花,图片本身是固定不变的(只有6种样式,其他都是重复),我们可以把它作为内部状态分离出来共享,我们称之为享元。而改变的是显示的位置,我们可以把它作为外部状态让外界去改变,在需要的时候传递给享元使用。


UML结构如及说明

image

为了方便让外界获取享元,一般采用享元工厂来管理享元对象,今天我们只讨论共享享元,不共享实用意义不大,暂不做讨论。

要使用享元模式来实现上面的程序,关键之处就是分离出享元和外部状态,享元就是6种UIImagview,外部状态10W朵小花的位置。来看看具体的实现吧。


代码实现

1、创建享元

我们需要分离出不变的部分作为享元,也就是6种UIImageview,所以我们自定义一个flowerView继承自系统的UIImageview,然后重写UIImageview的-- (void) drawRect:(CGRect)rect方法,把参数rect作为外部状态对外暴露,让外界传入uiimageviwe的frame来绘制图像。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>


@interface FlowerView : UIImageView
{
  
}

- (void) drawRect:(CGRect)rect;

@end

==================

#import "FlowerView.h"
#import <UIKit/UIKit.h>

@implementation FlowerView

- (void) drawRect:(CGRect)rect
{
  [self.image drawInRect:rect];
}

@end

2、 创建享元工厂

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

typedef enum
{
  kAnemone,
  kCosmos,
  kGerberas,
  kHollyhock,
  kJasmine,
  kZinnia,
  kTotalNumberOfFlowerTypes
} FlowerType;

@interface FlowerFactory : NSObject 
{
  @private
  NSMutableDictionary *flowerPool_;
}

- (UIImageView *) flowerViewWithType:(FlowerType)type;

@end

======================

#import "FlowerFactory.h"
#import "FlowerView.h"

@implementation FlowerFactory


- (UIImageView *)flowerViewWithType:(FlowerType)type
{
  if (flowerPool_ == nil)
  {
    flowerPool_ = [[NSMutableDictionary alloc] 
                   initWithCapacity:kTotalNumberOfFlowerTypes];
  }

  UIImageView *flowerView = [flowerPool_ objectForKey:[NSNumber
                                                  numberWithInt:type]];

  if (flowerView == nil)
  {
    UIImage *flowerImage;
    
    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];
    [flowerPool_ setObject:flowerView 
                    forKey:[NSNumber numberWithInt:type]];
  }
  
  return flowerView;
}


@end


3、分离享元和外部状态

我们通过享元工厂随机取出一个享元,然后给它一个随机位置,存入字典。循环创建10w个对象,存入数组

#import "ViewController.h"
#import "FlowerFactory.h"
#import "FlyweightView.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
// 使用享元模式
    FlowerFactory *factory = [[FlowerFactory alloc] init];
    NSMutableArray *flowerList = [[NSMutableArray alloc]
                                   initWithCapacity:500];
    for (int i = 0; i < 10000; ++i)
    {
        @autoreleasepool {
            FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
            //重复利用对象
            UIImageView *flowerView = [factory flowerViewWithType:flowerType];

            CGRect screenBounds = [[UIScreen mainScreen] bounds];
            CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
            CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
            NSInteger minSize = 10;
            NSInteger maxSize = 50;
            CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;
            
            CGRect area = CGRectMake(x, y, size, size);
            //新建对象
            NSValue *key = [NSValue valueWithCGRect:area];
            //新建对象
            NSDictionary *dic =   [NSDictionary dictionaryWithObject:flowerView forKey:key];
            [flowerList addObject:dic];

        }
        
    }
    
    FlyweightView *view = [[FlyweightView alloc]initWithFrame:self.view.bounds];
    view.flowerList = flowerList;
    self.view = view;


}

@end

4、自定义UIView,显示享元对象

取出享元对象,然后传入外部状态:位置,开始绘制UIImageview

#import <UIKit/UIKit.h>

@interface FlyweightView : UIView 

@property (nonatomic, retain) NSArray *flowerList;

@end

==================

#import "FlyweightView.h"
#import "FlowerView.h"

@implementation FlyweightView

extern NSString *FlowerObjectKey, *FlowerLocationKey;


- (void)drawRect:(CGRect)rect 
{
  for (NSDictionary *dic in self.flowerList)
  {
    
      NSValue *key = (NSValue *)[dic allKeys][0];
      FlowerView *flowerView = (FlowerView *)[dic allValues][0];
      CGRect area = [key CGRectValue];
      [flowerView drawRect:area];
  }

}

@end

5、测试

运行,再次查看app内存占用

image

看,只有44M,原来的三分之一都不到,大家可以自己试试,如果小花的数目再增加一倍,使用享元模式增加的内存才二十兆,但是如果使用我们文章开头的方法,内存几乎是暴增2倍。现在认识到享元模式的威力了吧。

我们再来看看此时的内存分配

image

注意上图中的UIImageview的flowerView内存占用才457KB,我们进入创建UIImageview的工厂方法看看具体的内存分配

image

而且不管小花的数量增加多少,创建UIImageview的消耗内存都是这么多,不会增加太多,因为我们只创建了6个UIImageview,而不是之前的几十万个。

对比此处的两张截图和文字开头的两种截图,可以看到差别。


问题

大家一看到这里,享元模式太节省内存了,以后只要是需要创建多个相似的对象,都可以使用享元模式了。其实不然,我们来看看,我们分别使用两种方式创建100、1000、5000、10000个小花,然后看看内存消耗。你会发现只有当创建的小花数目达到10000左右,享元模式的内存消耗才比普通模式的内存消耗少,其他三种情况,普通模式的内存消耗竟然比享元模式的内存消耗更低。

这是为什么呢?

我们再把这段代码拿出来看看

            FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
            //1、重复利用对象
            UIImageView *flowerView = [factory flowerViewWithType:flowerType];

            CGRect screenBounds = [[UIScreen mainScreen] bounds];
            CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
            CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
            NSInteger minSize = 10;
            NSInteger maxSize = 50;
            CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;
            CGRect area = CGRectMake(x, y, size, size);
            //2、新建对象
            NSValue *key = [NSValue valueWithCGRect:area];
            //3、新建对象
            NSDictionary *dic =   [NSDictionary dictionaryWithObject:flowerView forKey:key];
            [flowerList addObject:dic];


可以发现我们为了存储外部状态,在2、3两步我们一共创建了两个新对象,这都是需要消耗内存的。

假设创建了1000个小花,使用享元模式,需要创建1000个NSValue和1000个NSDictonary对象以及6个UIImageview,而使用普通模式需要创建1000个UIImageview。虽然NSValue和NSDictonary对象占用的内存比UIImageview要小许多,但是一旦数量多起来,也是需要占用大量内存。

只有当小花数量达到一定的数量,这个时候创建NSValue和NSDictonary对象占用的内存比普通方式创建的UIImageview占用的内存小的时候,享元模式才有优势。

分析到这里大家应该知道,享元模式把本来的对象拆成两个部分:享元和外部状态。而每个享元都需要一个与之对应的外部状态,而外部状态也是需要创建对象去存储的。所以只有当本来的对象占用的内存比存储外部状态的对象的占用内存大许多的时候,享元模式才有优势。

而且享元模式把本来简单的创建使用对象,拆分为几个类合作完成,操作更加复杂,这也是需要消耗内存和时间的。

综上所述,只有满足如下三个条件,才有必要考虑使用享元模式:

  • 本来的对象占用的内存比较大,比如UIImageView
  • 数量非常多(以万为单位)
  • 每个对象都非常相似,才可以分离出享元

我翻阅大多数的书籍和网上文章,都只是给出了伪代码,而没有具体分析比较享元模式和普通模式在内存消耗方面的优劣,其实按照网上的那些代码,享元模式消耗的内存更多。

要找到满足上面要求,其实非常难,特别是移动端很少需要处理这么大量级的数据,毕竟设备能力有限。该模式在后端使用场景更加广泛。


使用时机

image

Demo下载

享元模式demo

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

推荐阅读更多精彩内容