第18章:享元模式

flyweight模式(享元模式)是一种结构化的设计模式,可以最大限度地减少内存的使用和处理。

这种模式提供的对象都共享相同的底层数据,从而节省内存。它们通常是不可改变的,以使共享相同的基础数据变得简单。

享元模式有一些对象,叫做flyweights,还有一个静态方法来返回它们。

这听起来很熟悉吗?应该是的。享元模式是单例模式的一个变种。在享元模式中,你通常有多个相同类别的不同对象。一个例子是颜色的使用,你很快就会体会到这一点。你需要一种红色,一种绿色,等等。每种颜色都是一个单一的实例,共享相同的基础数据。

你什么时候应该使用它?

在你可以使用单例,但你需要多个具有不同配置的共享实例的地方使用flyweight。如果你有一个创建资源密集型的对象,而你又不能将创建过程的成本降到最低,那么最好的办法就是只创建一次对象,然后将其传递出去。

playground例子

在启动器目录中打开AdvancedDesignPatterns.xcworkspace,然后点击Flyweight链接来打开页面。在这里,你将使用UIKit。Flyweights在UIKit中非常常见。UIColor、UIFont和UITableViewCell都是具有飞行权重的类的例子。

import UIKit

let red = UIColor.red 
let red2 = UIColor.red 
print(red === red2)

这段代码证明了UIColor使用了flyweights。用===语句比较颜色显示,每个变量都有相同的内存地址,这意味着.red是一个flyweight,并且只被实例化一次。

当然,不是所有的UIColor对象都是flyweights。在下面添加以下内容。

let color = UIColor(red: 1, green: 0, blue: 0, alpha: 1) 
let color2 = UIColor(red: 1, green: 0, blue: 0, alpha: 1) 
print(color === color2)

这一次,你的控制台将记录false! 自定义UIColor对象不是flyweights。这个方法需要红色、绿色和蓝色,每次调用都会返回一个新的UIColor。

如果UIColor检查这些值,看看是否已经有了一个颜色,它可以返回flyweight实例来代替。你为什么不这样做呢?用下面的代码扩展UIColor类。

以下是你的做法。

  1. 你创建了一个叫做colorStore的字典来存储RGBA值。

  2. 你写了你自己的方法,像UIColor方法一样接收红绿蓝和alpha。你把RGB值存储在一个叫做key的字符串中。如果colorStore中已经存在一个带有该键的颜色,就使用该键而不是创建一个新的。

  3. 如果key不存在于colorStore中,则创建UIColor并将其与key一起存储。

最后,在playground的末尾添加以下代码。

let flyColor = UIColor.rgba(1, 0, 0, 1) 
let flyColor2 = UIColor.rgba(1, 0, 0, 1) 
print(flyColor === flyColor2)

这是对扩展方法的测试。你会看到控制台打印出true,这意味着你已经成功地实现了享元模式。

你应该注意什么?

在创建flyweights时,要注意你的flyweight内存的大小。如果你要存储几个flyweights,就像上面的colorStore一样,你要尽量减少内存

但你仍然可能在flyweight存储中使用过多的内存。

为了减轻这种情况,可以对你使用的内存数量设置限制,或者注册内存警告,并通过从内存中删除一些flyweights来应对。你可以使用LRU(最近使用最少的)缓存来处理这个问题。

还要注意的是,你的flyweight共享实例必须是一个类而不是一个结构。结构体使用复制语义,所以你不能获得引用类型所带来的共享底层数据的好处。

教程项目

在本节中,你将创建一个名为YetiJokes的教程应用。

这是一个阅读笑话的应用程序,使用自定义字体和雪花状的一些伟大的双关语。] 就本教程而言,大部分的设置已经完成了。

打开本章目录中的 starter\YetiJokes\YetiJokes.xcodeproj,查看flyweight模式。

建立并运行。在屏幕的底部,你会看到一个有以下选项的工具栏。

这个项目的目标是使用分段控制上的按钮来改变字体的大、中、小尺寸。这些字体将被动态加载为......你猜对了,是 "重量"!"重量 "是指字体的重量。

回到Finder,你会看到在Starter目录下有两个文件夹。YetiJokes和YetiTheme。YetiTheme是一个框架,里面有一个自定义字体。

打开Starter\YetiJokes\YetiTheme\YetiTheme.xcodeproj,在应用程序的左侧菜单中选择Fonts.swift。

将该文件的内容替换为以下内容。

以下是你所做的事情。

  1. 你创建了三个flyweights,每一个都是具有不同大小的字体。

  2. 为要使用的字体文件名创建一个私有常量。

  3. 你创建了一个方法,用来加载一个给定名称的、具有一定大小的字体。

  4. 在这个守护语句中,你将字体加载为CGFont,然后用CTFontManagerRegisterGraphicsFont将其注册到应用程序中。如果字体已经被注册了,就不会再被注册。

  5. 现在它已经被注册了,你可以按名称将你的自定义字体加载为UIFont。"为什么要这样加载字体?"你可能在想。"为什么不直接把字体包含在主捆绑包中?" 是的,在应用程序的主捆绑包里有一个更简单的方法。但是,如果YetiTheme是几个应用程序之间的共享库,你可能不希望每个应用程序都把这个字体添加到主捆绑包中。在一个现实世界的例子中,你可能有很多字体,但不希望每当有新的字体加入或现有的字体发生变化时,消费的应用程序都要麻烦地加入这些字体。

如果你的框架提供有商标的字体,你甚至可能被要求对字体数据进行加密。你在这里没有这样做,但如果你需要,你可以更容易地这样做,因为字体是在一个单独的包里。

现在你可以加载字体了,关闭YetiTheme并回到YetiJokes.xcodeproj。现在是时候将这个框架实际添加到应用程序中了。

右键点击导航树的顶部,选择添加文件到 "YetiJokes"......并添加YetiTheme.xcodeproj。

一旦你添加了YetiTheme.xcodeproj,你的项目结构将看起来类似于下面。

接下来,点击YetiJokes并选择General。滚动到底部,将YetiTheme.framework添加到框架、库和嵌入式内容中,如下所示。

好酷,你可以使用YetiTheme了吗?还不行。你需要先导入该框架。

打开ViewController.swift,在文件的顶部导入框架。

import YetiTheme

接下来,你要在视图控制器加载时使用新字体。在你的IBOutlet定义下面添加以下内容。

// MARK: - View Life Cycle 
public override func viewDidLoad() { 
  super.viewDidLoad()
  textLabel.font = Fonts.small
}

这段代码将在视图初始加载时将textLabel字体设置为小的自定义字体。最后,是时候设置分段式控件了。在现有的segmentedControlValueChanged(_:)中添加以下内容。

switch sender.selectedSegmentIndex { 
case 0:
  textLabel.font = Fonts.small 
case 1:
  textLabel.font = Fonts.medium 
case 2:
  textLabel.font = Fonts.large 
default:
  textLabel.font = Fonts.small }

构建并运行该应用程序。现在你可以在字体之间快速而轻松地切换了 每种字体只被加载一次,而且字体不会被多次注册。你已经成功地减少了你的应用程序的处理和加载时间。构建并运行应用程序以验证这一功能。

关键点

你在本章中了解了享元模式。以下是它的关键点。

请随意为YetiJokes添加功能,甚至改变笑话的内容;你只能用爸爸的双关语来获得这么多的笑声。


上一章 目录 下一章