我們來做一個很簡單的小程序:在界面上隨機(jī)顯示10萬朵小花,這些小花只有6種樣式戳吝。如圖所示:
一看,這還不簡單痒留,直接創(chuàng)建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占用的內(nèi)存,如圖:
占用內(nèi)存153M舷蟀,這還不把人嚇?biāo)佬袅祝@才一個頁面,要是再多來兩個頁面野宜,那app還不直接把內(nèi)存撐爆啊扫步。
我們使用instrument工具分析下,到底是哪里占用了過多的內(nèi)存匈子。截圖如下:
可以看到內(nèi)存的消耗主要是調(diào)用方法[self flowerViewWithType:flowerType]
創(chuàng)建UIImageView導(dǎo)致的河胎,進(jìn)入這個方法再看看具體的內(nèi)存分配,如圖:
我們知道UIImageview的創(chuàng)建是很消耗內(nèi)存的虎敦,這一下子創(chuàng)建10w個游岳,內(nèi)存占用可想而知。
那怎么解決呢其徙?
分析知道胚迫,屏幕上的10W朵小花只有6種樣式,只是在屏幕顯示的位置不同唾那。那能不能只創(chuàng)建6個UIImageview顯示小花访锻,然后重復(fù)利用這些UIImageView呢?
答案是肯定的闹获,這就需要用到我們要講的設(shè)計模式:享元模式期犬。下面具體看看
定義
運用共享技術(shù)有效地支持大量細(xì)粒度的對象。
分析下上面的需求避诽,我們需要創(chuàng)建10w個uiimageview來顯示小花哭懈,其實這些小花樣式大多都是重復(fù)的,只是位置不同茎用,造成了內(nèi)存浪費遣总,解決方案就是緩存這些細(xì)粒度對象,讓他們之創(chuàng)建一次轨功,后續(xù)要使用直接從緩存中取就可以了旭斥。
但是要注意不是任何對象都可以緩存的,因為緩存的是對象的實例古涧,實例存放的是屬性垂券,如果這些屬性不斷改變,那么緩存中的數(shù)據(jù)也必須跟著改變,那緩存就沒有意義了菇爪。
所以我們需要把一個對象分為兩個部分:不變和改變的部分算芯。把不變的部分緩存起來,我們稱之為內(nèi)部狀態(tài)凳宙,把改變的部分作為外部狀態(tài)對外暴露熙揍,讓外界去改變。對應(yīng)到上面的程序氏涩,屏幕上顯示的小花届囚,圖片本身是固定不變的(只有6種樣式,其他都是重復(fù))是尖,我們可以把它作為內(nèi)部狀態(tài)分離出來共享意系,我們稱之為享元。而改變的是顯示的位置饺汹,我們可以把它作為外部狀態(tài)讓外界去改變蛔添,在需要的時候傳遞給享元使用。
UML結(jié)構(gòu)如及說明
為了方便讓外界獲取享元兜辞,一般采用享元工廠來管理享元對象作郭,今天我們只討論共享享元,不共享實用意義不大弦疮,暫不做討論夹攒。
要使用享元模式來實現(xiàn)上面的程序,關(guān)鍵之處就是分離出享元和外部狀態(tài)胁塞,享元就是6種UIImagview咏尝,外部狀態(tài)10W朵小花的位置。來看看具體的實現(xiàn)吧啸罢。
代碼實現(xiàn)
1编检、創(chuàng)建享元
我們需要分離出不變的部分作為享元,也就是6種UIImageview扰才,所以我們自定義一個flowerView
繼承自系統(tǒng)的UIImageview允懂,然后重寫UIImageview的-- (void) drawRect:(CGRect)rect
方法,把參數(shù)rect
作為外部狀態(tài)對外暴露衩匣,讓外界傳入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、 創(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)
我們通過享元工廠隨機(jī)取出一個享元生百,然后給它一個隨機(jī)位置,存入字典柄延。循環(huán)創(chuàng)建10w個對象蚀浆,存入數(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ù)利用對象
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,顯示享元對象
取出享元對象市俊,然后傳入外部狀態(tà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、測試
運行摆昧,再次查看app內(nèi)存占用
看撩满,只有44M,原來的三分之一都不到据忘,大家可以自己試試,如果小花的數(shù)目再增加一倍搞糕,使用享元模式增加的內(nèi)存才二十兆勇吊,但是如果使用我們文章開頭的方法,內(nèi)存幾乎是暴增2倍∏涎觯現(xiàn)在認(rèn)識到享元模式的威力了吧汉规。
我們再來看看此時的內(nèi)存分配
注意上圖中的UIImageview的flowerView內(nèi)存占用才457KB,我們進(jìn)入創(chuàng)建UIImageview的工廠方法看看具體的內(nèi)存分配
而且不管小花的數(shù)量增加多少驹吮,創(chuàng)建UIImageview的消耗內(nèi)存都是這么多针史,不會增加太多,因為我們只創(chuàng)建了6個UIImageview碟狞,而不是之前的幾十萬個啄枕。
對比此處的兩張截圖和文字開頭的兩種截圖,可以看到差別族沃。
問題
大家一看到這里频祝,享元模式太節(jié)省內(nèi)存了,以后只要是需要創(chuàng)建多個相似的對象脆淹,都可以使用享元模式了常空。其實不然,我們來看看盖溺,我們分別使用兩種方式創(chuàng)建100漓糙、1000、5000烘嘱、10000個小花昆禽,然后看看內(nèi)存消耗。你會發(fā)現(xiàn)只有當(dāng)創(chuàng)建的小花數(shù)目達(dá)到10000左右蝇庭,享元模式的內(nèi)存消耗才比普通模式的內(nèi)存消耗少为狸,其他三種情況,普通模式的內(nèi)存消耗竟然比享元模式的內(nèi)存消耗更低遗契。
這是為什么呢辐棒?
我們再把這段代碼拿出來看看
FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
//1、重復(fù)利用對象
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];
可以發(fā)現(xiàn)我們?yōu)榱舜鎯ν獠繝顟B(tài)泰涂,在2、3兩步我們一共創(chuàng)建了兩個新對象辐怕,這都是需要消耗內(nèi)存的逼蒙。
假設(shè)創(chuàng)建了1000個小花,使用享元模式寄疏,需要創(chuàng)建1000個NSValue和1000個NSDictonary對象以及6個UIImageview是牢,而使用普通模式需要創(chuàng)建1000個UIImageview。雖然NSValue和NSDictonary對象占用的內(nèi)存比UIImageview要小許多陕截,但是一旦數(shù)量多起來驳棱,也是需要占用大量內(nèi)存。
只有當(dāng)小花數(shù)量達(dá)到一定的數(shù)量农曲,這個時候創(chuàng)建NSValue和NSDictonary對象占用的內(nèi)存比普通方式創(chuàng)建的UIImageview占用的內(nèi)存小的時候社搅,享元模式才有優(yōu)勢。
分析到這里大家應(yīng)該知道乳规,享元模式把本來的對象拆成兩個部分:享元和外部狀態(tài)形葬。而每個享元都需要一個與之對應(yīng)的外部狀態(tài),而外部狀態(tài)也是需要創(chuàng)建對象去存儲的暮的。所以只有當(dāng)本來的對象占用的內(nèi)存比存儲外部狀態(tài)的對象的占用內(nèi)存大許多的時候笙以,享元模式才有優(yōu)勢。
而且享元模式把本來簡單的創(chuàng)建使用對象冻辩,拆分為幾個類合作完成源织,操作更加復(fù)雜,這也是需要消耗內(nèi)存和時間的微猖。
綜上所述谈息,只有滿足如下三個條件,才有必要考慮使用享元模式:
- 本來的對象占用的內(nèi)存比較大凛剥,比如UIImageView
- 數(shù)量非常多(以萬為單位)
- 每個對象都非常相似侠仇,才可以分離出享元
我翻閱大多數(shù)的書籍和網(wǎng)上文章,都只是給出了偽代碼犁珠,而沒有具體分析比較享元模式和普通模式在內(nèi)存消耗方面的優(yōu)劣逻炊,其實按照網(wǎng)上的那些代碼,享元模式消耗的內(nèi)存更多犁享。
要找到滿足上面要求余素,其實非常難,特別是移動端很少需要處理這么大量級的數(shù)據(jù)炊昆,畢竟設(shè)備能力有限桨吊。該模式在后端使用場景更加廣泛威根。