предисловие
Продолжить предыдущую главуВраг жизни Флаттера Щепки (ScrollView), в этой главе мы будем следовать строке ListView/GridView => SliverList/SliverGrid => RenderSliverList/RenderSliverGrid, отсортируем окончательный код одного километра для расчета списка и обратные N.
Добро пожаловать во флаттер-конфетыГруппа QQ: 181398081
- Враг жизни Флаттера Щепки (ScrollView)
- Враг жизни Флаттера Щепки (расширенный список)
- Флаттер Щепка Водопад
- Flutter Sliver запирает вашу красоту
Щепка макета ввода и вывода
Прежде чем объяснять код макета, сначала разберитесь с вводом и выводом макета Sliver.
SliverConstraints
Входными данными для макета Sliver являются ограничения, которые сообщает нам Viewport.
class SliverConstraints extends Constraints {
/// Creates sliver constraints with the given information.
///
/// All of the argument must not be null.
const SliverConstraints({
//滚动的方向
@required this.axisDirection,
//这个是给center使用的,center之前的sliver是颠倒的
@required this.growthDirection,
//用户手势的方向
@required this.userScrollDirection,
//滚动的偏移量,注意这里是针对这个Sliver的,而且非整个Slivers的总滚动偏移量
@required this.scrollOffset,
//前面Slivers的总的大小
@required this.precedingScrollExtent,
//为pinned和floating设计的,如果前一个Sliver绘制大小为100,但是布局大小只有50,那么这个Sliver的overlap为50.
@required this.overlap,
//还有多少内容可以绘制,参考viewport以及cache。比如多Slivers的时候,前一个占了100,那么后面能绘制的区域就要减掉前面绘制的区域大小,得到剩余的绘制区域大小
@required this.remainingPaintExtent,
//纵轴的大小
@required this.crossAxisExtent,
//纵轴的方向,这里会影响GridView同一行元素的摆放顺序,是0~x,还是x~0
@required this.crossAxisDirection,
//viewport中还有多少内容可以绘制
@required this.viewportMainAxisExtent,
//剩余的缓存区域大小
@required this.remainingCacheExtent,
//相对于scrollOffset缓存区域大小
@required this.cacheOrigin,
})
SliverGeometry
Вывод макета Sliver будет возвращен в окно просмотра.
@immutable
class SliverGeometry extends Diagnosticable {
/// Creates an object that describes the amount of space occupied by a sliver.
///
/// If the [layoutExtent] argument is null, [layoutExtent] defaults to the
/// [paintExtent]. If the [hitTestExtent] argument is null, [hitTestExtent]
/// defaults to the [paintExtent]. If [visible] is null, [visible] defaults to
/// whether [paintExtent] is greater than zero.
///
/// The other arguments must not be null.
const SliverGeometry({
//预估的Sliver能够滚动大小
this.scrollExtent = 0.0,
//对后一个的overlap属性有影响,它小于[SliverConstraints.remainingPaintExtent],为Sliver在viewport范围(包含cache)内第一个元素到最后一个元素的大小
this.paintExtent = 0.0,
//相对Sliver位置的绘制起点
this.paintOrigin = 0.0,
//这个sliver在viewport的第一个显示位置到下一个sliver的第一个显示位置的大小
double layoutExtent,
//最大能绘制的总大小,这个参数是用于[SliverConstraints.remainingPaintExtent] 是无穷大的,就是使用在shrink-wrapping viewport中
this.maxPaintExtent = 0.0,
//如果sliver被pinned在边界的时候,这个大小为Sliver的自身的高度。其他情况为0
this.maxScrollObstructionExtent = 0.0,
//点击有效区域的大小,默认为paintExtent
double hitTestExtent,
//可见,paintExtent为0不可见。
bool visible,
//是否需要做clip,免得chidren溢出
this.hasVisualOverflow = false,
//viewport layout sliver的时候,如果sliver出现了一些问题,那么这个值将不等于0,通过这个值来修正整个滚动的ScrollOffset
this.scrollOffsetCorrection,
//该Sliver使用了多少[SliverConstraints.remainingCacheExtent],针对多Slivers的情况
double cacheExtent,
})
Я примерно объяснил смысл этих параметров, но может я еще не понял, и объясню по сцене в более поздних исходниках.
BoxScrollView
Widget | Extends |
---|---|
ListView/GridView | BoxScrollView => ScrollView |
И ListView, и GirdView наследуются от BoxScrollView. Давайте сначала посмотрим, в чем разница между BoxScrollView и ScrollView.
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry padding;
@override
List<Widget> buildSlivers(BuildContext context) {
/// 这个方法被ListView/GirdView 实现
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry effectivePadding = padding;
if (padding == null) {
final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
if (mediaQuery != null) {
// Automatically pad sliver with padding from MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
if (effectivePadding != null)
sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
return <Widget>[ sliver ];
}
/// Subclasses should override this method to build the layout model.
@protected
/// 这个方法被ListView/GirdView 实现
Widget buildChildLayout(BuildContext context);
Видно, что есть только еще один слой SliverPadding, а возвращенный [ sliver ] также показывает, что по сравнению с CustomScrollView ListView и GridView на самом деле являются одним Sliver, а последний может быть несколькими Sliver.
ListView
buildChildLayout вызывается в методе buildSlivers класса BoxScrollView, а ниже приведена реализация в ListView. Видно, что два Sliver, SliverList и SliverFixedExtentList, возвращаются в соответствии с itemExtent.
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
return SliverList(delegate: childrenDelegate);
}
SliverList
class SliverList extends SliverMultiBoxAdaptorWidget {
/// Creates a sliver that places box children in a linear array.
const SliverList({
Key key,
@required SliverChildDelegate delegate,
}) : super(key: key, delegate: delegate);
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context;
return RenderSliverList(childManager: element);
}
}
RenderSliverList
Серебряный макет
PerformLayout в RenderSliverList (GitHub.com/flutter/Appendix…
Зеленая часть на рисунке — это часть, которую мы видим, желтая часть — это область кэша, а серая часть — это часть, которую следует переработать.
//指示开始
childManager.didStartLayout();
//指示是否可以添加新的child
childManager.setDidUnderflow(false);
//constraints就是viewport给我们的布局限制,也就是布局输入
//滚动位置包含cache,布局区域开始位置
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
assert(scrollOffset >= 0.0);
//绘制整个区域大小包含缓存区域,就是图中黄色和绿色部分
final double remainingExtent = constraints.remainingCacheExtent;
assert(remainingExtent >= 0.0);
//布局区域结束位置
final double targetEndScrollOffset = scrollOffset + remainingExtent;
//获取到child的限制,如果是垂直滚动的列表,高度应该是无限大double.infinity
final BoxConstraints childConstraints = constraints.asBoxConstraints();
//从第一个child开始向后需要回收的孩子个数,图中灰色部分
int leadingGarbage = 0;
//从最后一个child开始向前需要回收的孩子个数,图中灰色部分
int trailingGarbage = 0;
//是否滚动到最后
bool reachedEnd = false;
//如果列表里面没有一个child,我们将尝试加入一个,如果加入失败,那么整个Sliver无内容
if (firstChild == null) {
if (!addInitialChild()) {
// There are no children.
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
- Случай форвардного расчета, (список с вертикальной прокруткой) — это список, который нужно прокручивать раньше. Поскольку дочерний элемент в серой части будет удален, при прокрутке вперед нам нужно посмотреть, нужно ли нам вставить дочерний элемент впереди в соответствии с текущей позицией прокрутки.
// Find the last child that is at or before the scrollOffset.
RenderBox earliestUsefulChild = firstChild;
//当第一个child的layoutOffset小于我们的滚动位置的时候,说明前面是空的,如果在第一个child的签名插入一个新的child来填充
for (double earliestScrollOffset =
childScrollOffset(earliestUsefulChild);
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
// We have to add children before the earliestUsefulChild.
// 这里就是在插入新的child
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
//处理当前面已经没有child的时候
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
//已经到0.0的位置了,所以不需要再向前找了,break
if (scrollOffset == 0.0) {
// insertAndLayoutLeadingChild only lays out the children before
// firstChild. In this case, nothing has been laid out. We have
// to lay out firstChild manually.
firstChild.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = firstChild;
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
break;
} else {
// We ran out of children before reaching the scroll offset.
// We must inform our parent that this sliver cannot fulfill
// its contract and that we need a scroll offset correction.
// 这里就是我们上一章讲的,出现出错了。将scrollOffsetCorrection设置为不为0,传递给viewport,这样它会整体重新移除掉这个差值,重新进行layout布局。
geometry = SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
);
return;
}
}
/// 滚动的位置减掉firstChild的大小,用来继续计算是否还需要插入更多child来补足前面。
final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild);
// firstChildScrollOffset may contain double precision error
// 同样的道理,如果发现最终减掉之后,数值小于0.0(precisionErrorTolerance这是一个接近0.0的极小数)的话,肯定是不对的,所以又告诉viewport移除掉差值,重新布局
if (firstChildScrollOffset < -precisionErrorTolerance) {
// The first child doesn't fit within the viewport (underflow) and
// there may be additional children above it. Find the real first child
// and then correct the scroll position so that there's room for all and
// so that the trailing edge of the original firstChild appears where it
// was before the scroll offset correction.
// TODO(hansmuller): do this work incrementally, instead of all at once,
// i.e. find a way to avoid visiting ALL of the children whose offset
// is < 0 before returning for the scroll correction.
double correction = 0.0;
while (earliestUsefulChild != null) {
assert(firstChild == earliestUsefulChild);
correction += paintExtentOf(firstChild);
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
}
geometry = SliverGeometry(
scrollOffsetCorrection: correction - earliestScrollOffset,
);
final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}
// ok,这里就是正常的情况
final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData as SliverMultiBoxAdaptorParentData;
// 设置child绘制的开始点
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
- предварительный метод (GitHub.com/flutter/Appendix…)
Переместите дочерний элемент назад, если его нет, верните false
bool inLayoutRange = true;
RenderBox child = earliestUsefulChild;
int index = indexOf(child);
double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
bool advance() { // returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication.
assert(child != null);
if (child == trailingChildWithLayout)
inLayoutRange = false;
child = childAfter(child);
///不在render tree里面
if (child == null)
inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child) != index) {
// We are missing a child. Insert it (and lay it out) if possible.
//不在树里面,尝试新增进去
child = insertAndLayoutChild(childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
if (child == null) {
// We have run out of children.
return false;
}
} else {
// Lay out the child.
child.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
//设置绘制位置
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
//设置endScrollOffset为child的绘制结束位置
endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
return true;
}
При прокрутке назад первый дочерний элемент может быть не самым близким к scrollOffset, поэтому нам нужно выполнить поиск в обратном направлении, чтобы найти ближайший.
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
//如果是小于,说明需要被回收,这里+1记录一下。
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
//找到最后都没有满足的话,将以最后一个child为准
// we want to make sure we keep the last child around so we know the end scroll offset
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent = childScrollOffset(lastChild) + paintExtentOf(lastChild);
geometry = SliverGeometry(
scrollExtent: extent,
paintExtent: 0.0,
maxPaintExtent: extent,
);
return;
}
}
- Обработать дочерний элемент в обратном порядкедо конца области макета.
// Now find the first child that ends after our end.
// 直到布局区域的结束位置
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
// Finally count up all the remaining children and label them as garbage.
//到上面位置是需要布局的最后一个child,所以在它之后的child就是需要被回收的
if (child != null) {
child = childAfter(child);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child);
}
}
// At this point everything should be good to go, we just have to clean up
// the garbage and report the geometry.
// 使用之前计算出来的回收参数
collectGarbage(leadingGarbage, trailingGarbage);
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
assert(_debugAssertChildListLocked());
assert(childCount >= leadingGarbage + trailingGarbage);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
//从第一个向后删除
while (leadingGarbage > 0) {
_destroyOrCacheChild(firstChild);
leadingGarbage -= 1;
}
//从最后一个向前删除
while (trailingGarbage > 0) {
_destroyOrCacheChild(lastChild);
trailingGarbage -= 1;
}
// Ask the child manager to remove the children that are no longer being
// kept alive. (This should cause _keepAliveBucket to change, so we have
// to prepare our list ahead of time.)
_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
return !childParentData.keepAlive;
}).toList().forEach(_childManager.removeChild);
assert(_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
return !childParentData.keepAlive;
}).isEmpty);
});
}
void _destroyOrCacheChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
//如果child被标记为缓存的话,从tree中移除并且放入缓存中
if (childParentData.keepAlive) {
assert(!childParentData._keptAlive);
remove(child);
_keepAliveBucket[childParentData.index] = child;
child.parentData = childParentData;
super.adoptChild(child);
childParentData._keptAlive = true;
} else {
assert(child.parent == this);
//直接移除
_childManager.removeChild(child);
assert(child.parent == null);
}
}
assert(debugAssertChildListIsNonEmptyAndContiguous());
double estimatedMaxScrollOffset;
//以及到底了,直接使用最后一个child的绘制结束位置
if (reachedEnd) {
estimatedMaxScrollOffset = endScrollOffset;
} else {
// 计算出估计最大值
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild),
lastIndex: indexOf(lastChild),
leadingScrollOffset: childScrollOffset(firstChild),
trailingScrollOffset: endScrollOffset,
);
assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
}
//根据remainingPaintExtent算出当前消耗了的绘制区域大小
final double paintExtent = calculatePaintOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
//根据remainingCacheExtent算出当前消耗了的缓存绘制区域大小
final double cacheExtent = calculateCacheOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
//布局区域结束位置
final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
//将输出反馈给Viewport,viewport根据sliver的输出,如果这个sliver已经没有内容了,再布局下一个
geometry = SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
//是否需要clip
hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
);
// We may have started the layout while scrolled to the end, which would not
// expose a new child.
// 2者相等说明已经这个sliver的底部了
if (estimatedMaxScrollOffset == endScrollOffset)
childManager.setDidUnderflow(true);
//通知完成layout
//这里会通过[SliverChildDelegate.didFinishLayout] 将第一个index和最后一个index传递出去,可以用追踪
childManager.didFinishLayout();
Расчетная максимальная реализация по умолчанию
static double _extrapolateMaxScrollOffset(
int firstIndex,
int lastIndex,
double leadingScrollOffset,
double trailingScrollOffset,
int childCount,
) {
if (lastIndex == childCount - 1)
return trailingScrollOffset;
final int reifiedCount = lastIndex - firstIndex + 1;
//算出平均值
final double averageExtent = (trailingScrollOffset - leadingScrollOffset) / reifiedCount;
//加上剩余估计值
final int remainingCount = childCount - lastIndex - 1;
return trailingScrollOffset + averageExtent * remainingCount;
}
Серебряный розыгрыш
@override
void paint(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
// offset is to the top-left corner, regardless of our axis direction.
// originOffset gives us the delta from the real origin to the origin in the axis direction.
Offset mainAxisUnit, crossAxisUnit, originOffset;
bool addExtent;
// 根据滚动的方向,来获取主轴和横轴的系数
switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
case AxisDirection.up:
mainAxisUnit = const Offset(0.0, -1.0);
crossAxisUnit = const Offset(1.0, 0.0);
originOffset = offset + Offset(0.0, geometry.paintExtent);
addExtent = true;
break;
case AxisDirection.right:
mainAxisUnit = const Offset(1.0, 0.0);
crossAxisUnit = const Offset(0.0, 1.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.down:
mainAxisUnit = const Offset(0.0, 1.0);
crossAxisUnit = const Offset(1.0, 0.0);
originOffset = offset;
addExtent = false;
break;
case AxisDirection.left:
mainAxisUnit = const Offset(-1.0, 0.0);
crossAxisUnit = const Offset(0.0, 1.0);
originOffset = offset + Offset(geometry.paintExtent, 0.0);
addExtent = true;
break;
}
assert(mainAxisUnit != null);
assert(addExtent != null);
RenderBox child = firstChild;
while (child != null) {
//获取child主轴的位置,为child的layoutOffset减去滚动位移scrollOffset
final double mainAxisDelta = childMainAxisPosition(child);
//获取child横轴的位置,ListView为0.0, GridView为计算出来的crossAxisOffset
final double crossAxisDelta = childCrossAxisPosition(child);
Offset childOffset = Offset(
originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta,
originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta,
);
if (addExtent)
childOffset += mainAxisUnit * paintExtentOf(child);
// If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child))
// does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden.
// 这里可以看到因为有cache的原因,有一些child其实是不需要绘制在我们可以看到的可视区域的
if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0)
context.paintChild(child, childOffset);
child = childAfter(child);
}
}
RenderSliverFixedExtentList
Когда itemExtent ListView не равен null, используется RenderSliverFixedExtentList. Мы лишь кратко расскажем об этом.Поскольку мы знаем высоту главной оси ребенка, это еще проще в различных расчетах. Мы можем напрямую вычислить первый дочерний элемент и последний дочерний элемент на основе scrollOffset и области просмотра.
GridView
RenderSliverGrid
Наконец, наш GridView, поскольку дизайн GridView таков, что размер основной оси дочернего элемента равен размеру горизонтальной оси/количеству дочерних элементов на горизонтальной оси (конечно, это также связано с childAspectRatio (по умолчанию 1.0) соотношение сторон), поэтому на самом деле размер главной оси дочернего элемента также известен, иПоложение чертежа по горизонтальной осиЕго также очень легко определить, основной принцип расчета аналогичен тому, что используется в ListView.
учиться по аналогии
Я говорил о большом количестве исходного кода, я не знаю, сколько людей могут его здесь увидеть. Анализируя исходный код, мы получаем некоторые знания о расчетах и рисовании списка осколков. Далее мы внесем несколько дополнений в официальный список Sliver, чтобы удовлетворить эффект застенчивости.
Оптимизация памяти списка изображений
Я часто слышу, как некоторые друзья говорят, что список изображений мигает после нескольких прокруток. Эта ситуация особенно очевидна на ios, но на Android память быстро растет. Причина в том, что Flutter по умолчанию создает кеш памяти для изображений. То есть, если вы прокрутите список и загрузите 300 изображений, в памяти будет кэш памяти на 300 изображений, а официальный лимит кеша составляет 1000.
Список тестов памяти
Во-первых, давайте посмотрим на память списка изображений без какой-либо обработки. Я сделал здесь список картинок, обычный 9-сеточный список картинок, общее количество инкрементно загружаемых дочерних элементов равно 300, то есть после загрузки может быть (19)*300=(3002700) кеш памяти изображения, конечно, поскольку официальный кеш равен 1000, окончательный кеш памяти изображения должен быть между 300 и 1000 (если общий размер изображения не превышает официального предела).
инструмент обнаружения памяти
- Сначала выполните
flutter packages pub global activate devtools
активировать дартс devtools
- После успешной активации выполнить
flutter --no-color packages pub global run devtools --machine --port=0
Введите адрес 127.0.0.1:9540 с изображения выше в адресную строку браузера.
- Далее нам нужно выполнить
flutter run --profile
Запуск нашего тестового приложения
После выполнения будет адрес, этот адрес мы скопируем в Connect в devtools
- После нажатия «Подключиться» переключитесь на «Память» в верхней части, и мы сможем увидеть мониторинг изменения памяти приложения в реальном времени.
Тест без обработки
- Андроид, открываю список и тяну вниз пока не загрузится 300 пунктов, память меняется на следующую картинку, видно что память вылетает и взрывается
- ios, я проделал те же шаги, но, к сожалению, это не дотянуло до конца, и оно вернулось примерно на 600 м (связано с ограничением памяти приложения ios)
В приведенном выше примере ясно видно огромное потребление памяти списками с несколькими изображениями Мы узнали обо всем процессе рисования списка во Flutter, так что можем ли мы как-то улучшить память? Ответ заключается в том, что мы можем попытаться активно очищать кеш памяти, содержащий изображения в этом дочернем элементе, когда дочерние элементы списка перерабатываются. Таким образом, в нашем списке есть только небольшой объем памяти изображений.С другой стороны, поскольку наши изображения кэшируются на жестком диске, даже если мы очистим кэш памяти, изображения не будут загружены снова, когда они будут перезагружается, что незаметно для пользователя.
Оптимизация памяти изображений
С последним обновлением вы можете удалить больше памяти изображений, установив ее напрямую
ExtendedImage(
clearMemoryCacheWhenDispose: true,
)
Ранее мы упоминали, что официальныйcollectGarbageметод, когда этот метод вызывается, он очищает ненужные дочерние элементы. Затем мы можем получить индексы очищенных дочерних элементов и уведомить пользователя в этот момент.
Код ключа выглядит следующим образом. Поскольку я не хочу переписывать базовые классы Sliver, я здесьExtendedListDelegateОбратный вызов передает индексы наружу.
void callCollectGarbage({
CollectGarbage collectGarbage,
int leadingGarbage,
int trailingGarbage,
int firstIndex,
int targetLastIndex,
}) {
if (collectGarbage == null) return;
List<int> garbages = [];
firstIndex ??= indexOf(firstChild);
targetLastIndex ??= indexOf(lastChild);
for (var i = leadingGarbage; i > 0; i--) {
garbages.add(firstIndex - i);
}
for (var i = 0; i < trailingGarbage; i++) {
garbages.add(targetLastIndex + i);
}
if (garbages.length != 0) {
//call collectGarbage
collectGarbage.call(garbages);
}
}
Когда уведомление детей будет очищено, перейдитеImageProvider.evictспособ удаления кэша изображений из памяти.
SliverListConfig<TuChongItem>(
collectGarbage: (List<int> indexes) {
///collectGarbage
indexes.forEach((index) {
final item = listSourceRepository[index];
if (item.hasImage) {
item.images.forEach((image) {
final provider = ExtendedNetworkImageProvider(
image.imageUrl,
);
provider.evict();
});
}
});
},
После оптимизации выполните те же действия, и память Android изменится на следующую
ios аналогична, а производительность следующая
Недостаточно лимита?
Из приведенного выше теста мы видим, что после оптимизации память списка изображений была значительно оптимизирована, что в основном соответствует нашим потребностям. Но того, что мы сделали, недостаточно, потому что для изображения списка обычно качество нашего изображения не такое высокое (я думаю о старшем брате, у которого снова есть изображение списка 8 м)
- Используйте официальный ResizeImage, который был недавно добавлен официальным, чтобы уменьшить кеш памяти изображения. Вы можете уменьшить изображение, установив ширину/высоту, по сути, официальное сжатие сделано за вас. Использование заключается в следующем
Конечно, предпосылка такого использования заключается в том, что вы уже знаете размер изображения заранее, так что вы можете пропорционально сжать изображение. Например, в следующем коде я уменьшил ширину и высоту в 5 раз.Учтите, что после этого качество изображения ухудшится, а если оно будет слишком маленьким, оно будет размытым. Пожалуйста, установите в соответствии с вашей собственной ситуацией. Еще одна проблема заключается в том, что изображение списка и изображение, предварительно просмотренное при нажатии на изображение, не являются одним и тем же ImageProvider (предварительные изображения, как правило, должны иметь высокое разрешение), поэтому они будут загружаться повторно, пожалуйста, выберите в соответствии с вашей собственной ситуацией.
ImageProvider createResizeImage() {
return ResizeImage(ExtendedNetworkImageProvider(imageUrl),
width: width ~/ 5, height: height ~/ 5);
}
- в наследствоExtendedNetworkImageProvider(Конечно, другие расширенные поставщики также используют этот метод для сжатия изображений), переопределите метод instanceiateImageCodec, в котором изображения сжимаются.
///override this method, so that you can handle raw image data,
///for example, compress
Future<ui.Codec> instantiateImageCodec(
Uint8List data, DecoderCallback decode) async {
_rawImageData = data;
return await decode(data);
}
- После выполнения этих оптимизаций мы снова проверили и проверили изменения в памяти ниже, и расход памяти снова уменьшился.
поддержите мой пиар
Если решение вам пригодится, поддержите мой пиар на collectGarbage.
add collectGarbage method for SliverChildDelegate to track which children can be garbage collected
Это позволяет большему количеству людей решить проблему памяти списка изображений. Конечно, вы также можете напрямую использоватьExtendedList WaterfallFlowа такжеLoadingMoreListВсе они поддерживают этот API. Все полное решение, которое я представилExtendedImageизdemoСреди них удобно просматривать весь процесс.
Получение списка отслеживания экспозиции
Проще говоря, как нам удобно узнать детей в видимой области? Из процесса вычисления и рисования списка мы можем легко получить индексы дочерних элементов в видимой области. Здесь я предоставляю обратные вызовы ViewportBuilder, чтобы получить первый и последний индекс в видимой области.расположение кода
также черезExtendedListDelegate, обратный вызов в viewportBuilder.
использовать демо
ExtendedListView.builder(
extendedListDelegate: ExtendedListDelegate(
viewportBuilder: (int firstIndex, int lastIndex) {
print("viewport : [$firstIndex,$lastIndex]");
}),
Специализируйте макет последнего дочернего элемента
Когда мы только начинали работать с Flutter, когда мы делали списки пошаговой загрузки, мы видели пример с использованием последнего дочернего элемента как loadmore/no more. Нет проблем, если ListView заполнен на весь экран, но необходимо решить следующую ситуацию.
- Когда ListView не заполнен, последний дочерний элемент отображает «больше нет». Обычно ожидается, что внизу будет отображаться «больше нет», но, поскольку это последний дочерний элемент, он будет предпоследним.
- Когда последний дочерний элемент GridView используется как loadmore/no more. Продукты не хотят, чтобы они размещались как обычные элементы GridView.
Чтобы решить эту проблему, я разработал lastChildLayoutTypeBuilder. Последний дочерний элемент размещается в соответствии с типом последнего дочернего элемента, указанным пользователем.В следующем примере используется RenderSliverList.
if (reachedEnd) {
///zmt
final layoutType = extendedListDelegate?.lastChildLayoutTypeBuilder
?.call(indexOf(lastChild)) ??
LastChildLayoutType.none;
// 最后一个child的大小
final size = paintExtentOf(lastChild);
// 最后一个child 绘制的结束位置
final trailingLayoutOffset = childScrollOffset(lastChild) + size;
//如果最后一个child绘制的结束位置小于了剩余绘制大小,那么我们将最后一个child的位置改为constraints.remainingPaintExtent - size
if (layoutType == LastChildLayoutType.foot &&
trailingLayoutOffset < constraints.remainingPaintExtent) {
final SliverMultiBoxAdaptorParentData childParentData =
lastChild.parentData;
childParentData.layoutOffset = constraints.remainingPaintExtent - size;
endScrollOffset = constraints.remainingPaintExtent;
}
estimatedMaxScrollOffset = endScrollOffset;
}
Наконец, давайте посмотрим, как его использовать.
enum LastChildLayoutType {
/// 普通的
none,
/// 将最后一个元素绘制在最大主轴Item之后,并且使用横轴大小最为layout size
/// 主要使用在[ExtendedGridView] and [WaterfallFlow]中,最后一个元素作为loadmore/no more元素的时候。
fullCrossAxisExtend,
/// 将最后一个child绘制在trailing of viewport,并且使用横轴大小最为layout size
/// 这种常用于最后一个元素作为loadmore/no more元素,并且列表元素没有充满整个viewport的时候
/// 如果列表元素充满viewport,那么效果跟fullCrossAxisExtend一样
foot,
}
ExtendedListView.builder(
extendedListDelegate: ExtendedListDelegate(
// 列表的总长度应该是 length + 1
lastChildLayoutTypeBuilder: (index) => index == length
? LastChildLayoutType.foot
: LastChildLayoutType.none,
),
Простой список чатов
Когда мы составляем список чата, поскольку макет расположен сверху вниз, наша первая реакция должна быть Для реверса ListView установлено значение true, когда новая сессия будет вставлена в позицию 0, это самая простая настройка, но когда сессия не заполнена областью просмотра, потому что макет перевернут, поэтому макет будет выглядеть как последующий.
trailing
-----------------
| |
| |
| item2 |
| item1 |
| item0 |
-----------------
leading
Чтобы решить эту проблему, вы можете установить для closeToTrailing значение true, макет станет следующим Это свойство также поддерживает [ExtendedGridView], [ExtendedList], [WaterfallFlow]. Конечно, если reverse не верен, вы все равно установите это свойство, чтобы оно действовало.Когда окно просмотра не заполнено, макет будет близок к замыкающему.
trailing
-----------------
| item2 |
| item1 |
| item0 |
| |
| |
-----------------
leading
Как это реально? Для этого я добавил 2 метода расширения
Если конечная позиция отрисовки последнего дочернего элемента не так велика, как оставшаяся область отрисовки (то есть дочерние элементы не заполнены областью просмотра), то мы добавляем расстояние Constraints.remainingPaintExtent - endScrollOffset к начальной точке отрисовки каждого дочерним, то явление будет заключаться в том, что все дочерние элементы тесно связаны с замыкающим макетом. Этот метод вызывается после расчета общего макета.
/// handle closeToTrailing at end
double handleCloseToTrailingEnd(
bool closeToTrailing, double endScrollOffset) {
if (closeToTrailing && endScrollOffset < constraints.remainingPaintExtent) {
RenderBox child = firstChild;
final distance = constraints.remainingPaintExtent - endScrollOffset;
while (child != null) {
final SliverMultiBoxAdaptorParentData childParentData =
child.parentData;
childParentData.layoutOffset += distance;
child = childAfter(child);
}
return constraints.remainingPaintExtent;
}
return endScrollOffset;
}
Потому что мы добавляем ограничения.remainingPaintExtent - endScrollOffset в начальную точку каждого дочернего рисунка. В следующий раз, когда мы будем выполнять Layout, мы должны сначала удалить эту часть расстояния. Когда индекс первого дочернего элемента равен 0, а layoutOffset не равен 0, нам нужно удалить layoutOffset всех дочерних элементов.
/// handle closeToTrailing at begin
void handleCloseToTrailingBegin(bool closeToTrailing) {
if (closeToTrailing) {
RenderBox child = firstChild;
SliverMultiBoxAdaptorParentData childParentData = child.parentData;
// 全部移除掉前一次performLayout增加的距离
if (childParentData.index == 0 && childParentData.layoutOffset != 0) {
var distance = childParentData.layoutOffset;
while (child != null) {
childParentData = child.parentData;
childParentData.layoutOffset -= distance;
child = childAfter(child);
}
}
}
}
Наконец, давайте посмотрим, как его использовать.
ExtendedListView.builder(
reverse: true,
extendedListDelegate: ExtendedListDelegate(closeToTrailing: true),
Эпилог
В этой главе мы решили некоторые проблемы реальной разработки, проанализировав исходный код sliver list. В следующей главе мы создадим свои собственные каскадные макеты, но у вас также есть возможность создавать списки произвольных ленточных макетов.
Добро пожаловатьFlutter Candies, чтобы вместе производить милые конфеты FlutterГруппа QQ: 181398081
надевать в последнюю очередьFlutter CandiesВся семейная бочка, действительно ароматная.