内存受限下的设计模式(3)——局部毁损
背景
A、B两公司主营相同的业务。A公司耗费两个月时间,开发出一个基于flash的RIA站点用于宣传。B公司耗费一周,开发了一个简单的CMS用于宣传。顾客Z的浏览器不支持flash或禁用了flash。Z先访问了A公司的站点,然后再访问B公司的站点。请问哪家公司的胜算更大些?或者说,哪家公司在竞争中占得了先机?
C公司也耗费两个月时间开发了一个基于JavaScript的RIA站点用于宣传,C公司还多花费了半个月的时间为不支持或禁用了JavaScript的浏览器开发了一个降级版本的站点。
现在,C至少在网络宣传方面将立于不败之地。
Web开发领域中将这种模式称为“优雅降级”或“渐进增强”。嵌入式领域将其称为“局部毁损”。
上面这则故事告诉我们至少三个道理:
1、采用局部毁损模式的产品更可靠;
2、采用局部毁损模式的产品要求更有经验的开发人员和更长的开发周期;
3、搞前端的人可能是和美术接触的比较多,起个名字都这么浪漫——优雅降级;而搞嵌入式显然要更死板更学究气一点——局部毁损。
模式
局部毁损模式要求我们,即便在系统内存耗尽的时候,也要让系统处于安全的状态。
更深入的探讨
局部毁损蕴含着三处值得思考的问题:
Q1:如何确定毁损的时机?
答:当内存耗尽时。一般来说,内存耗尽的消息是直接通过编程语言获知的。C语言通常返回一个错误码来暗示内存已耗尽;Java和C++一般通过异常的方式来通知内存已经耗尽。
Q2:毁损哪一个或多个组件?
答:毁损的一定不是关键的部件。比如我现在在使用javaeye的文本编辑器输入我的博文,则至少应当保证我能输入基本的文字,诸如表情、字体等功能,可以酌情毁损。
Q3:毁损的方式是什么?
答:拒绝申请内存的操作。
Q4:组件如何响应毁损动作?
答:一旦组件侦测到内存分配失败,它必须有能力回复到上一个安全状态,消除因为分配失败所导致的矛盾。有两种策略,适用的对象不同:
1、撤回,即简单地忽略这次内存请求。适用于后台运行的组件。(见下图)
2、降级,即删减不是那么重要又很耗费内存的功能,为关键功能腾出内存空间。适用于用户可见的组件(如UI)。(见下图)
实现
第一步:侦测内存耗尽。
如果从堆中分配内存,则编程语言本身应当会提供某种提醒分配失败的机制。如果使用固定结构分配,那么程序员自己就要保证该组件有能力检查并确定所使用的固定结构已满。
第二步:考虑受影响的组件。
一旦发现内存耗尽,必须确定一个系统中究竟有多少组件受到了这个错误的影响。OO的特性能够减少错误传播的趋势,但是就像我在《内存受限下的设计模式(2)——小型接口》中叙述的一样,OO在嵌入式领域只是一种选择,但绝非最佳选择。
第三步:释放资源。
在“撤回”(第一幅图)中,为了保证不留下任何副作用,凡是受到错误影响的组件,都要释放已经分配但无法使用的内存,并将其状态恢复至错误发生以前的状态。
C++的异常在try和catch语句之间通常要进行stack unwind。此时C++ run time会调用所有在try块中构造的对象的析构函数。调用的顺序和它们在try中构造的顺序相反。让我们来看下面这个例子(XL C/C++ V8.0 for AIX):
输出的结果是:
class Image{ // 创建一个中等画质和小尺寸的图片作为默认图片 static Picture defaultPic = new Picture(Picture.MEDIUM_QUALITY, Picture.SMALL_SIZE); // 返回默认图片 public static defaultPicture(){ return defaultPic; } // 创建图片 public static Picture createPic(int quality, int size){ Picture pic; try{ pic = new Picture(quality, size); } catch(OutOfMemoryException e){ // 内存满,返回默认图片 return defaultPicture(); } }}