設計模式系列13--享元模式

西木柚子發表於2016-12-23

設計模式系列13--享元模式
image

我們來做一個很簡單的小程式:在介面上隨機顯示10萬朵小花,這些小花只有6種樣式。如圖所示:

設計模式系列13--享元模式
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佔用的記憶體,如圖:

設計模式系列13--享元模式
image

佔用記憶體153M,這還不把人嚇死,這才一個頁面,要是再多來兩個頁面,那app還不直接把記憶體撐爆啊。

我們使用instrument工具分析下,到底是哪裡佔用了過多的記憶體。截圖如下:

設計模式系列13--享元模式
image

可以看到記憶體的消耗主要是呼叫方法[self flowerViewWithType:flowerType]建立UIImageView導致的,進入這個方法再看看具體的記憶體分配,如圖:

設計模式系列13--享元模式
image

我們知道UIImageview的建立是很消耗記憶體的,這一下子建立10w個,記憶體佔用可想而知。

那怎麼解決呢?

分析知道,螢幕上的10W朵小花只有6種樣式,只是在螢幕顯示的位置不同。那能不能只建立6個UIImageview顯示小花,然後重複利用這些UIImageView呢?

答案是肯定的,這就需要用到我們要講的設計模式:享元模式。下面具體看看


定義

運用共享技術有效地支援大量細粒度的物件。

分析下上面的需求,我們需要建立10w個uiimageview來顯示小花,其實這些小花樣式大多都是重複的,只是位置不同,造成了記憶體浪費,解決方案就是快取這些細粒度物件,讓他們之建立一次,後續要使用直接從快取中取就可以了。

但是要注意不是任何物件都可以快取的,因為快取的是物件的例項,例項存放的是屬性,如果這些屬性不斷改變,那麼快取中的資料也必須跟著改變,那快取就沒有意義了。

所以我們需要把一個物件分為兩個部分:不變和改變的部分。把不變的部分快取起來,我們稱之為內部狀態,把改變的部分作為外部狀態對外暴露,讓外界去改變。對應到上面的程式,螢幕上顯示的小花,圖片本身是固定不變的(只有6種樣式,其他都是重複),我們可以把它作為內部狀態分離出來共享,我們稱之為享元。而改變的是顯示的位置,我們可以把它作為外部狀態讓外界去改變,在需要的時候傳遞給享元使用。


UML結構如及說明

設計模式系列13--享元模式
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記憶體佔用

設計模式系列13--享元模式
image

看,只有44M,原來的三分之一都不到,大家可以自己試試,如果小花的數目再增加一倍,使用享元模式增加的記憶體才二十兆,但是如果使用我們文章開頭的方法,記憶體幾乎是暴增2倍。現在認識到享元模式的威力了吧。

我們再來看看此時的記憶體分配

設計模式系列13--享元模式
image

注意上圖中的UIImageview的flowerView記憶體佔用才457KB,我們進入建立UIImageview的工廠方法看看具體的記憶體分配

設計模式系列13--享元模式
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
  • 數量非常多(以萬為單位)
  • 每個物件都非常相似,才可以分離出享元

我翻閱大多數的書籍和網上文章,都只是給出了虛擬碼,而沒有具體分析比較享元模式和普通模式在記憶體消耗方面的優劣,其實按照網上的那些程式碼,享元模式消耗的記憶體更多。

要找到滿足上面要求,其實非常難,特別是移動端很少需要處理這麼大量級的資料,畢竟裝置能力有限。該模式在後端使用場景更加廣泛。


使用時機

設計模式系列13--享元模式
image


Demo下載

享元模式demo

相關文章