各位同仁、技术爱好者们,大家好!
今天,我们将深入探讨 Flutter 渲染引擎中一个核心但常常被忽视的机制:RenderBox 的 getMinIntrinsicWidth 算法及其背后 O(N) 复杂度的规避策略。理解这一机制,不仅能帮助我们写出更高性能的 Flutter 应用,更能揭示 Flutter 渲染系统设计的精妙之处。
引言:Flutter 渲染管线与布局的基础
在 Flutter 中,用户界面的绘制过程可以概括为三个主要阶段:布局 (Layout)、绘制 (Paint) 和 合成 (Compositing)。其中,布局阶段是确定每个 RenderObject 在屏幕上尺寸和位置的关键。RenderObject 是 Flutter 渲染树中的基本单元,而 RenderBox 则是最常见的 RenderObject 子类,它代表了一个具有矩形边界的渲染对象。
RenderBox 的布局过程遵循一套严格的约束-尺寸-位置协议:父级向下传递约束(BoxConstraints),子级向上返回尺寸(Size),父级最终确定子级的位置。这种单向数据流确保了布局过程的高效和可预测性。
然而,在某些场景下,仅仅依靠父级约束来决定子级尺寸是不够的。有时,我们需要知道一个 RenderBox 在给定特定条件下的“自然”尺寸。这就是固有尺寸 (Intrinsic Dimensions) 的概念。
固有尺寸 (Intrinsic Dimensions) 的概念
固有尺寸是 RenderBox 在不被父级约束完全限制时,其内容所期望的尺寸。Flutter 提供了四种主要的固有尺寸方法:
getMinIntrinsicWidth(double height): 在给定可用高度的情况下,此RenderBox能够占据的最小宽度,而不会导致内容溢出。getMaxIntrinsicWidth(double height): 在给定可用高度的情况下,此RenderBox能够占据的最大宽度,通常是其内容自然展开后的宽度。getMinIntrinsicHeight(double width): 在给定可用宽度的情况下,此RenderBox能够占据的最小高度,而不会导致内容溢出。getMaxIntrinsicHeight(double width): 在给定可用宽度的情况下,此RenderBox能够占据的最大高度,通常是其内容自然展开后的高度。
请注意,这些方法都接受一个参数(height 或 width),这意味着固有尺寸的计算通常是相互依赖的。例如,文本的最小宽度可能取决于它被允许的高度(单行还是多行)。对于 getMinIntrinsicWidth,通常我们会传入 double.infinity 作为高度参数,以模拟无限高的空间,从而计算出真正的“最小宽度”而无需考虑垂直方向的换行限制。
今天,我们的焦点是 getMinIntrinsicWidth,它是理解许多弹性布局(如 Row、Column 中的 flex 因子计算)和尺寸自适应控件(如 IntrinsicWidth)的关键。
问题所在:朴素实现带来的 O(N) 复杂度
想象一个简单的场景:我们有一个 RenderFlex (对应 Row 或 Column),它包含多个子 RenderBox。为了计算 RenderFlex 自身的 getMinIntrinsicWidth,它需要遍历其所有子级,并根据子级的 getMinIntrinsicWidth 来累加或计算。
一个朴素的递归实现可能会是这样的:
// 伪代码:一个简化的 RenderFlex 继承 RenderBox
class NaiveRenderFlex extends RenderBox {
List<RenderBox> _children;
// ... 构造函数和添加/移除子级的方法 ...
@override
double getMinIntrinsicWidth(double height) {
double minWidth = 0.0;
// 假设是水平方向的 Flex
for (RenderBox child in _children) {
// 递归调用子级的 getMinIntrinsicWidth
minWidth += child.getMinIntrinsicWidth(height);
}
return minWidth;
}
// ... 其他布局和绘制方法 ...
}
现在,考虑一个深层嵌套的 Widget 树:
RootWidget
-> Row (RenderFlex)
-> Column (RenderFlex)
-> Text (RenderParagraph)
-> Image (RenderImage)
-> Column (RenderFlex)
-> Text (RenderParagraph)
-> Row (RenderFlex)
-> Text (RenderParagraph)
-> Text (RenderParagraph)
当 RootWidget 的父级调用 RootWidget 的 getMinIntrinsicWidth 时,它会层层向下递归,直到叶子节点(RenderParagraph、RenderImage 等)。在最坏的情况下,如果每次布局更新都需要重新计算整个树的固有尺寸,并且树的深度为 D,节点数为 N,那么每次 getMinIntrinsicWidth 的调用都可能导致对整个子树的遍历。
在一个大型或复杂的用户界面中,渲染树可能包含成百上千甚至上万个 RenderBox。如果每次滚动、动画或状态更新都触发了对整个树的 O(N) 复杂度的固有尺寸计算,那么应用的性能将急剧下降,导致卡顿和不流畅的用户体验。这就是我们必须规避的 O(N) 复杂度问题。
规避策略的核心:缓存 (Caching) 与失效 (Invalidation)
解决 O(N) 复杂度的标准方法是缓存 (Caching)。如果一个计算的结果在短时间内不会改变,那么我们可以将其存储起来,当下次需要相同结果时,直接从缓存中读取,而不是重新计算。
然而,缓存的挑战在于缓存失效 (Cache Invalidation)。我们必须确保缓存中的数据始终是最新和准确的。如果缓存的数据已经过时,但我们仍然使用它,就会导致错误的布局。
Flutter 的 RenderBox 体系采用了一种精妙的缓存和失效机制来解决这个问题。
1. 缓存结构:_IntrinsicCache
在 RenderBox 内部,有一个名为 _IntrinsicCache 的私有类,用于存储固有尺寸的计算结果。它是一个简单的映射结构,将计算参数(例如,传入的高度)映射到计算结果。
更准确地说,_IntrinsicCache 存储的是 _IntrinsicCacheEntry 对象:
// 伪代码:_IntrinsicCacheEntry
class _IntrinsicCacheEntry {
final double value; // 缓存的固有尺寸值
final int generation; // 缓存数据所属的“世代”或“布局ID”
_IntrinsicCacheEntry(this.value, this.generation);
}
generation 是这里的关键。它是一个整数,用于标记缓存数据是基于哪一次布局计算的结果。
2. 全局布局世代计数器:_nextLayoutId
Flutter 渲染引擎维护一个全局的、递增的整数计数器,我们可以称之为 _nextLayoutId(在实际的 Flutter 源码中,类似的概念由 RenderObject._debugActiveLayout 或 PipelineOwner._nextGeneration 等实现)。每当渲染树中发生任何可能影响布局的更改时(例如,markNeedsLayout() 被调用),这个全局计数器就会递增。
这个 _nextLayoutId 可以被视为当前布局阶段的“世代编号”。
3. RenderObject 自身的布局世代:_lastLayoutId
每个 RenderObject 实例都有一个私有的 _lastLayoutId 字段,用于记录该 RenderObject 上一次成功执行布局计算时的全局 _nextLayoutId 值。
当一个 RenderObject 需要重新布局时,它会调用 markNeedsLayout()。这个方法会做两件事:
a. 将自身标记为需要布局。
b. 沿着父链向上遍历,直到根节点或遇到一个已经标记为需要布局的祖先,并调用它们的 markNeedsLayout()。这个过程会触发全局 _nextLayoutId 的递增。
4. 缓存失效逻辑:世代检查
当一个 RenderBox 尝试获取其固有尺寸时(例如,调用 getMinIntrinsicWidth),它首先会检查其 _IntrinsicCache:
-
查找缓存条目: 根据传入的参数(例如
height),在_IntrinsicCache中查找对应的_IntrinsicCacheEntry。 -
世代比较: 如果找到了条目,它会比较条目中的
generation值和当前RenderObject的_lastLayoutId。- 缓存命中 (有效): 如果
entry.generation == _lastLayoutId,这意味着缓存条目是在当前布局世代中计算出来的,因此它是有效的。直接返回entry.value。 - 缓存失效 (过期): 如果
entry.generation < _lastLayoutId,这意味着缓存条目是在更早的布局世代中计算出来的,而当前RenderObject已经经历了至少一次布局更新(_lastLayoutId已经更新),因此缓存条目可能已过期。需要重新计算。 - 缓存未命中: 如果没有找到条目,则需要重新计算。
- 缓存命中 (有效): 如果
-
重新计算与更新缓存: 如果缓存失效或未命中,
RenderBox会执行实际的固有尺寸计算逻辑(这可能涉及递归调用子级的固有尺寸方法)。计算完成后,它会将结果连同当前的_lastLayoutId一起存储到_IntrinsicCache中,形成一个新的_IntrinsicCacheEntry。
通过这种机制,只有当 RenderBox 或其子级的布局确实发生变化时,才会触发重新计算。否则,即使 getMinIntrinsicWidth 被频繁调用,只要缓存有效,它都是一个 O(1) 操作。整个树的固有尺寸计算的 O(N) 复杂度被平摊到整个布局生命周期中,每次全局布局更新最多发生一次 O(N) 的计算。
深入 Flutter 源码中的实现细节
让我们结合 Flutter 源码中的概念来进一步理解。
RenderBox 提供了以下方法来处理固有尺寸:
// 在 RenderBox 中,这些方法是抽象的,需要具体子类实现
double getMinIntrinsicWidth(double height);
double getMaxIntrinsicWidth(double height);
double getMinIntrinsicHeight(double width);
double getMaxIntrinsicHeight(double width);
// 辅助方法,通常在子类中调用来执行实际计算并利用缓存
@protected
double _computeMinIntrinsicWidth(double height);
@protected
double _computeMaxIntrinsicWidth(double height);
@protected
double _computeMinIntrinsicHeight(double width);
@protected
double _computeMaxIntrinsicHeight(double width);
_IntrinsicCache 的结构:
// 实际 Flutter 源码中可能更复杂,但核心概念类似
class _IntrinsicCache {
// Key: double (e.g., height for width calculations)
// Value: _IntrinsicCacheEntry
final Map<double, _IntrinsicCacheEntry> _map = <double, _IntrinsicCacheEntry>{};
_IntrinsicCacheEntry? get(double key) => _map[key];
void put(double key, double value, int generation) {
_map[key] = _IntrinsicCacheEntry(value, generation);
}
}
class _IntrinsicCacheEntry {
final double value;
final int generation; // 对应 RenderBox._cachedSizingParameters._lastLayoutId
_IntrinsicCacheEntry(this.value, this.generation);
}
RenderBox 内部的缓存管理:
每个 RenderBox 实例内部有一个 _cachedSizingParameters 字段(或者类似的概念),它包含了四个 _IntrinsicCache 实例,分别对应四种固有尺寸计算:
// 简化后的 RenderBox 内部结构
abstract class RenderBox extends RenderObject {
// ... 其他 RenderObject 字段 ...
// 缓存固有尺寸计算结果
final _IntrinsicCache _minWidthCache = _IntrinsicCache();
final _IntrinsicCache _maxWidthCache = _IntrinsicCache();
final _IntrinsicCache _minHeightCache = _IntrinsicCache();
final _IntrinsicCache _maxHeightCache = _IntrinsicCache();
// 上一次布局的世代ID,用于缓存失效
int? _lastLayoutId; // 实际由 PipelineOwner._nextGeneration 管理
// ... 构造函数 ...
@override
void performLayout() {
// 布局计算完成后,更新 _lastLayoutId
// 实际更新逻辑更复杂,由 PipelineOwner 协调
_lastLayoutId = RenderObject.debugActiveLayout; // 模拟获取当前全局布局ID
// ... 执行实际布局 ...
}
double getMinIntrinsicWidth(double height) {
// 1. 获取当前全局布局ID (模拟 PipelineOwner._nextGeneration)
final int currentLayoutId = RenderObject.debugActiveLayout;
// 2. 检查缓存
final _IntrinsicCacheEntry? entry = _minWidthCache.get(height);
if (entry != null && entry.generation == currentLayoutId) {
// 缓存命中且有效
return entry.value;
}
// 缓存失效或未命中,进行实际计算
final double result = _computeMinIntrinsicWidth(height);
// 3. 更新缓存
_minWidthCache.put(height, result, currentLayoutId);
return result;
}
// 类似的逻辑应用于 getMaxIntrinsicWidth, getMinIntrinsicHeight, getMaxIntrinsicHeight
// ...
// 子类需要实现这个方法来执行实际的计算逻辑
@protected
double _computeMinIntrinsicWidth(double height) {
throw FlutterError('RenderBox subclasses must implement _computeMinIntrinsicWidth');
}
@override
void markNeedsLayout() {
// 当自身或子级可能导致布局变化时调用
// 这会触发全局 _nextLayoutId (debugActiveLayout) 的递增
// 并且会通知父级,最终导致整个布局管线的重新执行
super.markNeedsLayout(); // 实际更复杂,会标记祖先为dirty
// 这里我们假设 debugActiveLayout 会在布局开始前递增
}
}
RenderObject.debugActiveLayout 的作用:
在 Flutter 的调试模式下,RenderObject.debugActiveLayout 提供了一个方便的全局布局 ID。在生产模式下,这个 ID 由 PipelineOwner 内部的 _nextGeneration 字段管理。每当 PipelineOwner 开始一个新的布局阶段时,_nextGeneration 就会递增。当一个 RenderBox 完成其布局后,它会将当前的 _nextGeneration 值记录下来,作为其 _lastLayoutId。
缓存键 (Cache Key) 的选择:
对于 getMinIntrinsicWidth(double height),height 参数就是缓存的键。这意味着即使对于同一个 RenderBox,如果以不同的 height 调用 getMinIntrinsicWidth,它们会被视为不同的缓存条目。这符合实际需求,因为文本的最小宽度在单行和多行情况下是不同的。
典型 RenderBox 子类的固有尺寸计算示例
理解了缓存机制,我们来看看几个典型 RenderBox 子类是如何实现 _computeMinIntrinsicWidth 的。
1. RenderParagraph (文本)
RenderParagraph 负责渲染文本。其 _computeMinIntrinsicWidth 相对复杂,因为它涉及到文本的排版和断行。
// 伪代码:RenderParagraph 的 _computeMinIntrinsicWidth 简化版
class RenderParagraph extends RenderBox {
TextPainter _textPainter; // Flutter 用于文本布局的核心工具
// ... 构造函数和文本设置 ...
@override
double _computeMinIntrinsicWidth(double height) {
if (_textPainter.text == null) {
return 0.0;
}
// 设置 TextPainter 的约束,以便计算最小宽度
// 传入的 height 参数用于模拟文本可以占据的最大高度
// TextPainter.layout() 会根据这些约束计算文本的实际尺寸
// 这里我们关注的是它在给定高度下能达到的最小宽度而不溢出
_textPainter.layout(minWidth: 0.0, maxWidth: double.infinity);
// 计算最小固有宽度,这通常是根据文本内容中最长的不可断行单词或字符序列来确定的。
// 如果文本是 "Hello World",最小宽度可能是 "World" 的宽度,因为它不能被拆开。
// 如果 height 足够小,导致只能容纳一个字符,则会尝试拆分单词。
// 这是一个复杂的文本布局算法,涉及到断字和行宽计算。
final double minWidth = _textPainter.minIntrinsicWidth;
return minWidth;
}
// ...
}
TextPainter 内部会执行复杂的算法,例如 Unicode 双向算法、断字规则等,来确定文本在给定约束下的最小宽度。这个计算本身可能是 O(文本长度) 的,但由于缓存的存在,它在每次布局周期中最多只会被计算一次(对于相同的 height 参数)。
2. RenderFlex (Row/Column)
RenderFlex 是 Row 和 Column 的底层渲染对象。其 _computeMinIntrinsicWidth 逻辑是理解弹性布局的关键。
// 伪代码:RenderFlex 的 _computeMinIntrinsicWidth 简化版
class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, FlexParentData> {
Axis direction; // 水平或垂直
// ... 其他 Flex 属性 (mainAxisAlignment, crossAxisAlignment, etc.) ...
@override
double _computeMinIntrinsicWidth(double height) {
if (direction == Axis.vertical) {
// 如果是垂直方向的 Flex (Column),其最小宽度取决于最宽的子级
// 假设每个子级都有无限的垂直空间 (height),我们找到它们各自的最小宽度
double maxChildMinWidth = 0.0;
RenderBox? child = firstChild;
while (child != null) {
final FlexParentData childParentData = child.parentData! as FlexParentData;
// 如果子级有 flex 因子,它的最小宽度可能会被忽略或以特殊方式处理
// 简化起见,这里假设没有 flex 因子或 flex 因子为 0
if (childParentData.flex == 0) { // 忽略 Expanded/Flexible 子级
maxChildMinWidth = math.max(maxChildMinWidth, child.getMinIntrinsicWidth(double.infinity));
}
child = childParentData.nextSibling;
}
return maxChildMinWidth;
} else { // direction == Axis.horizontal (Row)
// 如果是水平方向的 Flex (Row),其最小宽度是所有子级最小宽度的总和
double totalMinWidth = 0.0;
RenderBox? child = firstChild;
while (child != null) {
final FlexParentData childParentData = child.parentData! as FlexParentData;
// 同样,忽略 Expanded/Flexible 子级,因为它们的宽度是可伸缩的
if (childParentData.flex == 0) {
totalMinWidth += child.getMinIntrinsicWidth(height);
}
child = childParentData.nextSibling;
}
return totalMinWidth;
}
}
// ...
}
注意:RenderFlex 的实际实现要复杂得多,它需要处理 flex 因子、mainAxisAlignment、crossAxisAlignment、textDirection 等等。但核心思想是:对于非 flex 子级,它会递归调用子级的固有尺寸方法。
3. RenderImage (图片)
RenderImage 的 _computeMinIntrinsicWidth 取决于图片的原始分辨率和 fit 属性。
// 伪代码:RenderImage 的 _computeMinIntrinsicWidth 简化版
class RenderImage extends RenderBox {
ImageStream? _imageStream;
ImageInfo? _imageInfo; // 包含图片尺寸信息
// ...
@override
double _computeMinIntrinsicWidth(double height) {
if (_imageInfo == null || _imageInfo!.image == null) {
return 0.0; // 图片未加载或无效
}
final double imageWidth = _imageInfo!.image.width.toDouble();
final double imageHeight = _imageInfo!.image.height.toDouble();
// 假设 fit 属性为 BoxFit.scaleDown 或 BoxFit.none
// 最小宽度通常就是图片的原始宽度,除非被其他约束强制缩小
// 如果有 fit 属性,逻辑会更复杂,例如 BoxFit.fitWidth 会根据给定 height 按比例缩放宽度
if (imageHeight > 0 && height.isFinite && height < imageHeight) {
// 如果给定高度小于图片原始高度,那么图片宽度需要按比例缩小
return imageWidth * (height / imageHeight);
}
return imageWidth; // 默认情况下,最小宽度就是原始宽度
}
// ...
}
4. RenderConstrainedBox (通过 ConstrainedBox 设置尺寸限制)
RenderConstrainedBox 只是简单地将约束传递给其子级,并根据子级的尺寸来确定自己的尺寸。其固有尺寸计算通常会传递给子级。
// 伪代码:RenderConstrainedBox 的 _computeMinIntrinsicWidth 简化版
class RenderConstrainedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
BoxConstraints additionalConstraints;
// ...
@override
double _computeMinIntrinsicWidth(double height) {
if (child == null) {
return additionalConstraints.minWidth;
}
// 将 additionalConstraints 应用于子级的固有尺寸计算
// 这是一个简化的处理,实际情况会考虑 min/max width/height
final double childMinWidth = child!.getMinIntrinsicWidth(height);
return math.max(childMinWidth, additionalConstraints.minWidth);
}
// ...
}
何时需要固有尺寸?
理解固有尺寸的计算方式,更重要的是理解何时以及为何我们需要它们。
-
IntrinsicWidth和IntrinsicHeightWidgets:
这些 Widget 显式地要求其子级计算其固有尺寸。例如,IntrinsicWidth会强制其子级在布局时,将宽度设置为其getMaxIntrinsicWidth。这在需要一个 Widget 根据其内容的自然宽度来填充父级空间时非常有用,例如在一个Row中,你希望一个Column的宽度能够包裹其内容而不会溢出。Row( children: [ IntrinsicWidth( // 这个 Column 的宽度会根据其内容的最大宽度来确定 child: Column( children: [ Text('很长很长的一段文本'), Text('短文本'), ], ), ), Expanded( child: Container(color: Colors.blue), ), ], )在这种情况下,
IntrinsicWidth会调用其子RenderColumn的getMaxIntrinsicWidth,而RenderColumn又会调用其RenderParagraph子级的getMaxIntrinsicWidth。 -
Flex布局中的flex因子计算:
当Row或Column中包含Expanded或FlexibleWidget 时,RenderFlex需要在分配剩余空间之前,知道其非弹性子级的固有尺寸。它会首先计算所有非弹性子级的总固有尺寸,然后将剩余空间按flex因子分配给弹性子级。 -
FittedBox:
FittedBox会尝试调整其子级的大小以适应自身可用空间。在决定如何缩放子级时,它可能会查询子级的固有尺寸。 -
自定义布局:
如果你正在编写一个自定义的RenderBox,并且需要根据其子级的内容来决定自己的尺寸,那么你很可能会用到getMinIntrinsicWidth等方法。
复杂度分析与性能考量
朴素递归方案:
- 复杂度: O(N),其中 N 是子树中的
RenderBox数量。每次调用都可能遍历整个子树。 - 问题: 性能瓶颈,尤其是在频繁布局更新的场景。
Flutter 缓存方案:
- 单次缓存命中: O(1)。这是最常见的情况,因为大部分时间缓存都是有效的。
- 单次缓存未命中或失效: O(D) 或 O(N),其中 D 是子树深度,N 是子树节点数。当一个
RenderBox的固有尺寸缓存失效时,它需要重新计算,这会递归地调用其子级的固有尺寸方法。在最坏的情况下,这会遍历整个受影响的子树。 - 摊销复杂度 (Amortized Complexity): 每次全局布局周期中,每个
RenderBox的固有尺寸计算(对于相同的参数)最多只会被执行一次。因此,对于整个渲染树,一个完整的固有尺寸计算周期是 O(N) 的。但是,由于 Flutter 的markNeedsLayout机制通常只影响树中需要更新的部分,因此实际的重新计算范围通常远小于整个树。
性能优势:
通过缓存,Flutter 极大地减少了重复计算。在稳定的 UI 中,固有尺寸的计算几乎是免费的。只有当 Widget 树的结构或内容发生变化,导致 markNeedsLayout 被触发时,相关的固有尺寸缓存才会被标记为失效并重新计算。
潜在问题:
- 缓存键的选择: 如果
getMinIntrinsicWidth被频繁地以大量不同的height值调用,那么缓存可能会变得庞大,并且缓存命中率会降低。但这在实践中并不常见,因为通常我们会传入double.infinity或有限的几个固定高度值。 - 缓存内存开销: 缓存需要占用内存。对于非常大的树,这可能会累积。然而,Flutter 的设计通常会平衡性能和内存。
规避 O(N) 的更广泛思考:Flutter 的布局策略
Flutter 在布局阶段规避 O(N) 复杂度的策略远不止固有尺寸的缓存。它是一个综合性的系统设计:
- 单向数据流: 父级向下传递约束,子级向上返回尺寸。这种模式避免了子级与父级之间的循环依赖,确保了布局过程的确定性和高效性。
- 严格的约束模型:
BoxConstraints始终定义了一个最小和最大尺寸范围。这使得子级在布局时拥有明确的边界,无需反复协商。 - 增量更新:
markNeedsLayout机制确保只有受影响的RenderObject及其祖先才会被标记为“脏”,从而避免了不必要的全局布局计算。 - 布局隔离:
RenderObject之间的布局是相对独立的。一个RenderBox在布局其子级时,通常不会依赖于其兄弟节点的布局结果,这有助于并行化和局部化更新。 - 缓存 (Intrinsic Dimensions): 正如我们今天深入探讨的,固有尺寸的缓存是解决特定 O(N) 问题的关键。
通过这些机制,Flutter 构建了一个既灵活又高效的渲染系统,能够处理复杂的用户界面,同时保持流畅的 60fps 甚至 120fps 性能。
总结与展望
RenderBox 的 getMinIntrinsicWidth 算法及其背后的 O(N) 复杂度规避策略是 Flutter 渲染引擎设计中的一个亮点。通过引入基于世代的缓存机制,Flutter 成功地将潜在的 O(N) 递归计算转化为在大多数情况下接近 O(1) 的查找,从而确保了即使在大型和动态的 UI 中也能保持卓越的性能。
理解这一机制,不仅能帮助我们更好地调试布局问题,还能指导我们设计更高效的自定义 Widget。下次当你看到 IntrinsicWidth 或 Flex Widget 的神奇表现时,希望你能想起背后那套精妙的缓存与失效策略,以及 Flutter 工程师们为性能优化所做的努力。