這篇文章呢,能夠說是對最近flutter的listView研究作個小小的彙總;git
其實呢,我一直對flutter的設計沒太有什麼好感,尤爲是完成那個支持仿真翻頁的小說閱讀器的那段時期,不止一次的想:md這作的啥玩意,看看隔壁Android 的xxx,就你這還好意思對標Android原生?github
PS:其實如今關於文字繪製這塊我仍是這麼想的;算法
不過最近看了閒魚的 Flutter 高性能、多功能的全場景滾動容器,必定要看!,有了些想法改進重寫那個小說閱讀器,研究了下,發現flutter的listView設計,好像仍是蠻不錯的,思路很清晰很輕量,相較之下,Android 就比較沉重了;(可能也是如今的listView太初期的緣故,不過設計思路卻是蠻好讀的)windows
PPS:話說是否是閒魚的自定義engine版本過低了,因此纔沒那些功能?像曝光、複用這塊都已經有現成的部分了啊……爲啥要費勁自定義呢?緩存
首先呢,默認你們都對flutter的一些基礎知識、好比說三棵樹及其做用啊什麼的都已經瞭解;固然沒了解的也沒太大關係,百度谷歌下,這玩意的講解已經爛大街了,看個5分鐘瞭解下大概就夠了;markdown
PS:若是懶得看,直接翻後面總結部分,一步到胃;ide
由於widget樹和element樹並不參與繪製過程,因此相對輕量,因此在我看來,複用renderObject,便可提升不少性能,因此問題來了:oop
怎麼去複用renderObject?post
要解決這個問題,咱們就要開始追蹤下RenderObject跟element的愛恨情仇;性能
固然關於他倆的雞毛蒜皮或陳穀子爛芝麻的事就不在這裏提了,直接看相關的部分;
widget樹?那玩意就是個舔狗,召之即來揮之即去的傢伙,不用管;
衆所周知,一個View要想展現,必需要走三步:measure、layout、draw;flutter中一樣道理,只不過這事是renderObject來作的;在listView中找下相關方法就能找到相關部分:
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
/// Creates a sliver that places multiple box children in a linear array along
/// the main axis.
///
/// The [childManager] argument must not be null.
RenderSliverList({
required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
@override
void performLayout() {
…………一堆不相關的
/// 好哥哥看過來
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!);
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 = childScrollOffset(child!)! + paintExtentOf(child!);
return true;
}
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
// 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;
}
}
……
collectGarbage(leadingGarbage, trailingGarbage);
……
}
}
複製代碼
注意其中的advance(),其中有個insertAndLayoutChild 方法,從字面上來看,就是它負責插入子RenderObject的
@protected
RenderBox? insertAndLayoutChild(
BoxConstraints childConstraints, {
required RenderBox? after,
bool parentUsesSize = false,
}) {
assert(_debugAssertChildListLocked());
assert(after != null);
final int index = indexOf(after!) + 1;
_createOrObtainChild(index, after: after);
final RenderBox? child = childAfter(after);
if (child != null && indexOf(child) == index) {
child.layout(childConstraints, parentUsesSize: parentUsesSize);
return child;
}
childManager.setDidUnderflow(true);
return null;
}
複製代碼
唉,其中有個_createOrObtainChild 方法,從字面上翻譯,意思是,建立或獲取child?獲取child?
再點進去看看
void _createOrObtainChild(int index, { required RenderBox? after }) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
if (_keepAliveBucket.containsKey(index)) {
final RenderBox child = _keepAliveBucket.remove(index)!;
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
assert(childParentData._keptAlive);
dropChild(child);
child.parentData = childParentData;
insert(child, after: after);
childParentData._keptAlive = false;
} else {
_childManager.createChild(index, after: after);
}
});
}
複製代碼
下面那個createChild確定不是緩存相關的了,因此這個_keepAliveBucket 嫌疑很大唉,看下他的方法內容,應該就是它作複用了;可是被複用的對象是誰加進來的呢?
點一下看下都是誰在用這個_keepAliveBucket
em,好像還很多人用,可是沒有關西;反正我就想知道是誰往裏面塞數據,這麼再一看,只剩下兩個方法:
一個是move方法,看名字就不像; 一個是_destroyOrCacheChild 方法,看上去就是它,點進去看看;
void _destroyOrCacheChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
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);
}
}
複製代碼
因此又冒出一個新東西:SliverMultiBoxAdaptorParentData,那麼是否是隻要讓這玩意的 keepAlive爲true 就好了
先看下是誰在調用這個_destroyOrCacheChild;
唉,就一個方法 collectGarbage ,而這個方法只在performLayout 調用,跟那個_createOrObtainChild 方法同樣,都是直接或間接在performLayout方法中調用,看來跟它同樣,屬於比較直接的生命週期類方法;
那麼直接重寫這個collectGarbage 方法,講要回收的對象的keepAlive改成true,不就能夠了麼?
就這?????
最後試了下,好像還真是這樣,建立過的RenderObject,不會再建立第二次,createChild 方法每一個index只調用一次;應該是複用成功了;
複用問題就這麼解決了?就這 兩個字真的是我當時的想法,甚至感受好像不會這麼簡單,是否是哪裏有坑啊……
不過我所以產生了一些猜測:
若是這樣的話,可否像RecyclerView那樣完全複用RenderObject?
如今是根據index來複用的,很簡單;若是滑動到新的item,並且緩存中不存在,仍是會走create;
可是若是像Android的RecyclerView那樣,直接拿緩存區的RenderObject,而後從新塞回去替代建立,並經過更新機制更新展現數據,這樣是否可行呢?記得看過某篇文章中說過,flutter內部更新使用一個diff算法來作的,挺高效的,這樣用高效的diff算法應該比單純建立高效吧,應該吧~~~
複用這塊直接用flutter官方提供的就完事了,固然純屬試驗性質,有沒有坑還真沒試出來,也沒作全面測試
這是個人flutter doctor -v
關於flutter部分的信息,若是你那代碼有差異的話,能夠對比參考下:
[√] Flutter (Channel stable, 1.22.5, on Microsoft Windows [Version 10.0.17763.1577], locale zh-CN)
• Flutter version 1.22.5 at D:\Program File\sdk\flutter\flutter_windows_v1.9.1+hotfix.2-stable
• Framework revision 7891006299 (9 weeks ago), 2020-12-10 11:54:40 -0800
• Engine revision ae90085a84
• Dart version 2.10.4
複製代碼
下面上關鍵部分代碼,默認自定義的 RenderSliverList 已經經過繼承和重寫引入進去
class RecyclerRenderSliverList extends RenderSliverList {
RecyclerRenderSliverList({
@required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
@override
void collectGarbage(int leadingGarbage, int trailingGarbage) {
/// 若是從頭開始要回收的垃圾數量+從尾開始要回收的垃圾數量 不等於 0(也就是大於0)
if (leadingGarbage + trailingGarbage != 0) {
print("collectGarbage : " +
" leadingGarbage : " +
leadingGarbage.toString() +
", trailingGarbage : " +
trailingGarbage.toString());
if (childCount >= leadingGarbage + trailingGarbage) {
int tempLeadingGarbage = leadingGarbage;
int tempTrailingGarbage = trailingGarbage;
RenderObject tempFirstChild = firstChild;
RenderObject tempLastChild = lastChild;
while (tempLeadingGarbage > 0) {
/// 標記keepAlive爲true
(tempFirstChild.parentData as SliverMultiBoxAdaptorParentData)
.keepAlive = true;
tempFirstChild = childAfter(tempFirstChild);
tempLeadingGarbage -= 1;
}
while (tempTrailingGarbage > 0) {
/// 標記keepAlive爲true
(tempLastChild.parentData as SliverMultiBoxAdaptorParentData)
.keepAlive = true;
tempLastChild = childBefore(tempLastChild);
tempTrailingGarbage -= 1;
}
}
}
/// 剩下的flutter都作好了
super.collectGarbage(leadingGarbage, trailingGarbage);
}
}
複製代碼
就這麼簡單…………
固然這塊是徹底複用,貌似沒上限的那種,就是那種你要有一萬個item,他就給你緩存一萬個,因此理論上會很是吃內存,正確的作法應該要結合本身定義的緩存規則來作,不過那塊還沒搞~
因此還須要進一步測試研究~
另外補充一小點:
曝光這塊其實也蠻簡單的,這幫renderObject的parentData都是SliverMultiBoxAdaptorParentData ,裏面都帶上了index……
因此其實只要遍歷下子child,看下保存的offset的數值,就能夠得知當前第一個可見項什麼的……而後直接從parentData中拿index就完事了
這個我是真的感受沒啥問題的
若是還有須要,能夠拉下 flutter_novel 的dev分支,裏面的reader2文件夾就是相關部分的,不過須要本身找,目前那塊都是試驗性質的,搞的其實有點亂;
在這裏先給你們拜個年,若是不出意外的話,接下來的12天我就要蹲在提瓦特大陸了~因此嘛,失個聯,很正常嘛,除非你們能在提瓦特大陸相遇,而後問候一句:
原來你也玩原神?
咳咳,真tmd尬