設(shè)計(jì)模式13--享元模式

image

我們來(lái)做一個(gè)很簡(jiǎn)單的小程序:在界面上隨機(jī)顯示10萬(wàn)朵小花,這些小花只有6種樣式。如圖所示:

image

一看,這還不簡(jiǎn)單,直接創(chuàng)建10w個(gè)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;
            //新建對(duì)象
            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;
}


覺(jué)得很好對(duì)吧?來(lái),看看app占用的內(nèi)存,如圖:

image

占用內(nèi)存153M,這還不把人嚇?biāo)溃@才一個(gè)頁(yè)面,要是再多來(lái)兩個(gè)頁(yè)面,那app還不直接把內(nèi)存撐爆啊。

我們使用instrument工具分析下,到底是哪里占用了過(guò)多的內(nèi)存。截圖如下:

image

可以看到內(nèi)存的消耗主要是調(diào)用方法[self flowerViewWithType:flowerType]創(chuàng)建UIImageView導(dǎo)致的,進(jìn)入這個(gè)方法再看看具體的內(nèi)存分配,如圖:

image

我們知道UIImageview的創(chuàng)建是很消耗內(nèi)存的,這一下子創(chuàng)建10w個(gè),內(nèi)存占用可想而知。

那怎么解決呢?

分析知道,屏幕上的10W朵小花只有6種樣式,只是在屏幕顯示的位置不同。那能不能只創(chuàng)建6個(gè)UIImageview顯示小花,然后重復(fù)利用這些UIImageView呢?

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


定義

運(yùn)用共享技術(shù)有效地支持大量細(xì)粒度的對(duì)象。

分析下上面的需求,我們需要?jiǎng)?chuàng)建10w個(gè)uiimageview來(lái)顯示小花,其實(shí)這些小花樣式大多都是重復(fù)的,只是位置不同,造成了內(nèi)存浪費(fèi),解決方案就是緩存這些細(xì)粒度對(duì)象,讓他們之創(chuàng)建一次,后續(xù)要使用直接從緩存中取就可以了。

但是要注意不是任何對(duì)象都可以緩存的,因?yàn)榫彺娴氖菍?duì)象的實(shí)例,實(shí)例存放的是屬性,如果這些屬性不斷改變,那么緩存中的數(shù)據(jù)也必須跟著改變,那緩存就沒(méi)有意義了。

所以我們需要把一個(gè)對(duì)象分為兩個(gè)部分:不變和改變的部分。把不變的部分緩存起來(lái),我們稱之為內(nèi)部狀態(tài),把改變的部分作為外部狀態(tài)對(duì)外暴露,讓外界去改變。對(duì)應(yīng)到上面的程序,屏幕上顯示的小花,圖片本身是固定不變的(只有6種樣式,其他都是重復(fù)),我們可以把它作為內(nèi)部狀態(tài)分離出來(lái)共享,我們稱之為享元。而改變的是顯示的位置,我們可以把它作為外部狀態(tài)讓外界去改變,在需要的時(shí)候傳遞給享元使用。


UML結(jié)構(gòu)如及說(shuō)明

image

為了方便讓外界獲取享元,一般采用享元工廠來(lái)管理享元對(duì)象,今天我們只討論共享享元,不共享實(shí)用意義不大,暫不做討論。

要使用享元模式來(lái)實(shí)現(xiàn)上面的程序,關(guān)鍵之處就是分離出享元和外部狀態(tài),享元就是6種UIImagview,外部狀態(tài)10W朵小花的位置。來(lái)看看具體的實(shí)現(xiàn)吧。


代碼實(shí)現(xiàn)

1、創(chuàng)建享元

我們需要分離出不變的部分作為享元,也就是6種UIImageview,所以我們自定義一個(gè)flowerView繼承自系統(tǒng)的UIImageview,然后重寫UIImageview的-- (void) drawRect:(CGRect)rect方法,把參數(shù)rect作為外部狀態(tài)對(duì)外暴露,讓外界傳入uiimageviwe的frame來(lái)繪制圖像。

#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、 創(chuàng)建享元工廠

#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、分離享元和外部狀態(tài)

我們通過(guò)享元工廠隨機(jī)取出一個(gè)享元,然后給它一個(gè)隨機(jī)位置,存入字典。循環(huán)創(chuàng)建10w個(gè)對(duì)象,存入數(shù)組

#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;
            //重復(fù)利用對(duì)象
            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);
            //新建對(duì)象
            NSValue *key = [NSValue valueWithCGRect:area];
            //新建對(duì)象
            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,顯示享元對(duì)象

取出享元對(duì)象,然后傳入外部狀態(tài):位置,開(kāi)始繪制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、測(cè)試

運(yùn)行,再次查看app內(nèi)存占用

image

看,只有44M,原來(lái)的三分之一都不到,大家可以自己試試,如果小花的數(shù)目再增加一倍,使用享元模式增加的內(nèi)存才二十兆,但是如果使用我們文章開(kāi)頭的方法,內(nèi)存幾乎是暴增2倍。現(xiàn)在認(rèn)識(shí)到享元模式的威力了吧。

我們?cè)賮?lái)看看此時(shí)的內(nèi)存分配

image

注意上圖中的UIImageview的flowerView內(nèi)存占用才457KB,我們進(jìn)入創(chuàng)建UIImageview的工廠方法看看具體的內(nèi)存分配

image

而且不管小花的數(shù)量增加多少,創(chuàng)建UIImageview的消耗內(nèi)存都是這么多,不會(huì)增加太多,因?yàn)槲覀冎粍?chuàng)建了6個(gè)UIImageview,而不是之前的幾十萬(wàn)個(gè)。

對(duì)比此處的兩張截圖和文字開(kāi)頭的兩種截圖,可以看到差別。


問(wèn)題

大家一看到這里,享元模式太節(jié)省內(nèi)存了,以后只要是需要?jiǎng)?chuàng)建多個(gè)相似的對(duì)象,都可以使用享元模式了。其實(shí)不然,我們來(lái)看看,我們分別使用兩種方式創(chuàng)建100、1000、5000、10000個(gè)小花,然后看看內(nèi)存消耗。你會(huì)發(fā)現(xiàn)只有當(dāng)創(chuàng)建的小花數(shù)目達(dá)到10000左右,享元模式的內(nèi)存消耗才比普通模式的內(nèi)存消耗少,其他三種情況,普通模式的內(nèi)存消耗竟然比享元模式的內(nèi)存消耗更低。

這是為什么呢?

我們?cè)侔堰@段代碼拿出來(lái)看看

            FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
            //1、重復(fù)利用對(duì)象
            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、新建對(duì)象
            NSValue *key = [NSValue valueWithCGRect:area];
            //3、新建對(duì)象
            NSDictionary *dic =   [NSDictionary dictionaryWithObject:flowerView forKey:key];
            [flowerList addObject:dic];


可以發(fā)現(xiàn)我們?yōu)榱舜鎯?chǔ)外部狀態(tài),在2、3兩步我們一共創(chuàng)建了兩個(gè)新對(duì)象,這都是需要消耗內(nèi)存的。

假設(shè)創(chuàng)建了1000個(gè)小花,使用享元模式,需要?jiǎng)?chuàng)建1000個(gè)NSValue和1000個(gè)NSDictonary對(duì)象以及6個(gè)UIImageview,而使用普通模式需要?jiǎng)?chuàng)建1000個(gè)UIImageview。雖然NSValue和NSDictonary對(duì)象占用的內(nèi)存比UIImageview要小許多,但是一旦數(shù)量多起來(lái),也是需要占用大量?jī)?nèi)存。

只有當(dāng)小花數(shù)量達(dá)到一定的數(shù)量,這個(gè)時(shí)候創(chuàng)建NSValue和NSDictonary對(duì)象占用的內(nèi)存比普通方式創(chuàng)建的UIImageview占用的內(nèi)存小的時(shí)候,享元模式才有優(yōu)勢(shì)。

分析到這里大家應(yīng)該知道,享元模式把本來(lái)的對(duì)象拆成兩個(gè)部分:享元和外部狀態(tài)。而每個(gè)享元都需要一個(gè)與之對(duì)應(yīng)的外部狀態(tài),而外部狀態(tài)也是需要?jiǎng)?chuàng)建對(duì)象去存儲(chǔ)的。所以只有當(dāng)本來(lái)的對(duì)象占用的內(nèi)存比存儲(chǔ)外部狀態(tài)的對(duì)象的占用內(nèi)存大許多的時(shí)候,享元模式才有優(yōu)勢(shì)。

而且享元模式把本來(lái)簡(jiǎn)單的創(chuàng)建使用對(duì)象,拆分為幾個(gè)類合作完成,操作更加復(fù)雜,這也是需要消耗內(nèi)存和時(shí)間的。

綜上所述,只有滿足如下三個(gè)條件,才有必要考慮使用享元模式:

  • 本來(lái)的對(duì)象占用的內(nèi)存比較大,比如UIImageView
  • 數(shù)量非常多(以萬(wàn)為單位)
  • 每個(gè)對(duì)象都非常相似,才可以分離出享元

我翻閱大多數(shù)的書籍和網(wǎng)上文章,都只是給出了偽代碼,而沒(méi)有具體分析比較享元模式和普通模式在內(nèi)存消耗方面的優(yōu)劣,其實(shí)按照網(wǎng)上的那些代碼,享元模式消耗的內(nèi)存更多。

要找到滿足上面要求,其實(shí)非常難,特別是移動(dòng)端很少需要處理這么大量級(jí)的數(shù)據(jù),畢竟設(shè)備能力有限。該模式在后端使用場(chǎng)景更加廣泛。


使用時(shí)機(jī)

image

Demo下載

享元模式demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,011評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 175,263評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,543評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,323評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 54,874評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,095評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,605評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,551評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,720評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,961評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,358評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,612評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,330評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,690評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容