当界面需要显示大批量数据的时候,在QT框架中,有一个叫做"视图-代理-模型"的设计模式,如果没有自定义绘制的界面需求,也可以叫做"视图-模型".
相比传统的直接在容器控件内添加子控件的方式,这种方法可以大大节省内存的消耗.
本文我们一起测试当数据量达到20万这样级别的时候,不同的设计方法有多大的区别.
首先是QListWidget,我们创建一个QWidget,在中间放上一个QListWidget,就这样,没有任何其他东西,然后在这个QListWidget中添加20万条文本信息,文本内容是"item:123",其中的"123"就是列表项的序号,从一到20万,实在是太简单的例子了,那么他所需要耗费的内存大概是63M
其次登场的是QListView,我们创建一个QWidget,在中间放上一个QListView,就这样,也没有任何其他东西,然后使用一个QStringListModel给这个QListView装载数据,代码类似:
QListView * v = new QListView;
QStringListModel * m = new QStringListModel;
QStringList strList;
qreal len = 0;
for(int i = 0;i < 200000;++i){
const auto str = "item:" + QString::number(i);
len += str.capacity() + sizeof(QString);
strList.push_back(str);
}
m->setStringList(strList);
v->setModel(m);
这样的方式耗费的内存大概是24M , 内存节省了62%
上图,第三个程序使用QListWidget , 第二个程序使用QListView
但是这么简单的程序如果耗费24M内存,仍然是不合理的.
在很多业务逻辑中,大批量数据的显示往往是通过分页的方式来节约内存的,用户选择页码,点击跳转后,界面加载对应的那一部分内容.
除了这个聪明的做法还有一种方式就是延迟加载,通过实现QAbstractListView的虚函数void fetchMore(const QModelIndex &parent) override来实现.
这个虚函数的常规实现类似这样的:
void fetchMore(const QModelIndex &parent) override {
const int total = mDataList.size();
if(total > TOTAL) {
return;
}
const int batch = 100;
emit layoutAboutToBeChanged();
beginInsertRows(QModelIndex(), total, total + batch -1);
for(int i = 0;i < batch;++i){
mDataList.push_back("item:" + QString::number(total + i));
}
endInsertRows();
emit layoutChanged();
}
当用户拖动滚动条到底部的时候,QListView会问模型索要更多数据,这时候这个虚函数就会被执行了,在这里面,判断当前已经显示的数量和总数量的大小关系,来给当前数据链表添加更多的数据,在实际业务中,上述代码的第十行可能是个查询数据库的操作,也可能是查询网络来加载更多数据的操作.
当使用这种方式的时候,我们发现,内存使用缩小到了5.7M ,但是这样的方式会随着用户的不断阅览而不断增加内存的消耗.在QWidget框架中还没有一个接口类似fetchLess那样可以告诉模型回收一些数据.
在QML中通过设置cacheBuffer,可以把大列表的内存消耗降低到用户想要的程度,并且保持这样.
Window {
visible: true
width: 640
height: 480
Component {
id: idTmpItemDelegate
Rectangle{
width: 200; height: 32
Column {
Text { text: "item:" + index }
}
}
}
ListView {
height: 480
width: 200;
cacheBuffer: 100;
id:idListView
model: 200000
delegate: idTmpItemDelegate
}
}
这看上去非常神奇,其实是它非常阴险地隐藏了滚动条,而且在快速滚动过程中总觉得有时候比较卡顿.
于是我们可以在QListView中实现类似的稳定低内存展示大批量数据的模式.首先隐藏滚动条,其次在滚动鼠标事件中捕获滚动的方向,根据滚动的偏移量滑动滚动条,当滚动条当前位置接近1的时候,照旧调用fetchMore,并且判断当前总数据是否大于一定的限值,是的话就删除一部分数据,这就保证了内存消耗不再永久增加了.而用户看不到滚动条的长短变化,根本毫无感知.
但是这样的实现并不高级 , 各位不需要对充满复杂的逻辑考虑的实现而灰心丧气. 实际大批量的业务数据的展示要么是用户根本不关心的,要么是用户非常关心的. 如果是不关心的 , 那么只显示一部分小数据已经仁至义尽了 , 如果是非常关心的 , 那么分页或者使用过滤排序模型显然更为合理. 这两种模式都涉及动态的加载, 这一操作带来的延时是说得过去的 ,因为用户知道他在查询很大的数据.