Image and QQuickAsyncImageProvider caching doesn't work?



  • I have a QML ListView in my QtQuick application, with an Image item as a delegate for the view. Images are loaded from Web via a custom-created QQuickAsyncImageProvider. Image elements have "cache" as well as "asynchronous" properties set to true; nevertheless it seems that the caching doesn't work as expected(when I scroll my view up and down, QQuickAsyncImageProvider is always called to source the images, even those that were already downloaded just a second ago). It also doesn't look particularly good on the client side(images that were loaded just moments ago disappear and begin to load again).

    In all of this, the "source" property of the Images doesn't change(the address strings always stay the same), so the caching should work, but it certainly doesn't for me?

    Here are some code snippets:

    delegate for the View:

                delegate: Rectangle {
                    id: delegItem
    
                    height: Math.max(delegImage.height, 300)
                    width: Math.max(delegImage.width, 300)
    
                    border.width: 1
                    color: "lightBlue"
    
                    Image {
                        id: delegImage
                        cache: true
                        asynchronous: true
                        source: "image://gloader/" + index.toString()
                    }
                }
    

    ImageProvider:

    class AsyncImageResponse : public QQuickImageResponse
    {
        Q_OBJECT
    public:
        // QQuickImageResponse interface
    public:
        explicit AsyncImageResponse(QNetworkRequest req, QSize const& requestedSize, ImageListModel* imageList, int index);
        QQuickTextureFactory *textureFactory() const override;
    
    public slots:
        void onResponseFinished();
    
    protected:
        QNetworkAccessManager m_imageLoader;
        QNetworkReply* m_reply;
        QSize m_requestedSize;
        ImageListModel* m_imageList;
        QImage m_resultImage;
        int m_index;
    };
    
    class GoogleImageProvider : public QQuickAsyncImageProvider
    {
    public:
        explicit GoogleImageProvider(ImageListModel* imageList);
    
    protected:
        ImageListModel* m_imageList;
    
        // QQuickAsyncImageProvider interface
    public:
        QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
    };
    
    AsyncImageResponse::AsyncImageResponse(QNetworkRequest req, QSize const& reqSize, ImageListModel* imageList, int index)
    {
        m_reply = m_imageLoader.get(req);
        m_requestedSize = reqSize;
        m_imageList = imageList;
        m_index = index;
    
        qDebug() << "Waiting for image request";
    
        connect(m_reply, &QNetworkReply::finished, this, &AsyncImageResponse::onResponseFinished);
    }
    
    void AsyncImageResponse::onResponseFinished()
    {
        QByteArray myImageData = m_reply->readAll();
    
        m_resultImage = QImage::fromData(myImageData);
        //*size = resultImage.size();
    
        qDebug() << "Image loaded";
        m_imageList->setImageAtIndex(m_index, m_resultImage);
        if (m_requestedSize.isValid())
        {
            m_resultImage = m_resultImage.scaled(m_requestedSize);
        }
        //return resultImage;
        emit finished();
    }
    
    QQuickTextureFactory *AsyncImageResponse::textureFactory() const
    {
        return QQuickTextureFactory::textureFactoryForImage(m_resultImage);
    }
    
    GoogleImageProvider::GoogleImageProvider(ImageListModel* imageList):
        m_imageList(imageList)
    {
    }
    
    QQuickImageResponse* GoogleImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
    {
        QNetworkRequest myRequest(QUrl(m_imageList->getImageAddress(id.toInt())));
    
        return new AsyncImageResponse(myRequest, requestedSize, m_imageList, id.toInt());
    }
    


  • Without looking into qt source i think the problem is the creation and destruction of the listviews delegates. After going out of visible scope the delegate itemes are destructed and the creation of the delegates triggesr new QAsyncImageRequest. So you have to cache the images in your AsyncImageProvider by yourselfe.

    But maybe setting cacheBuffer (http://doc.qt.io/qt-5/qml-qtquick-listview.html#cacheBuffer-prop) to some big value will have an effect, because of the delegates not beeing destructed.



  • I was thinking about this quote from the Qt documentation:

    Images returned by a QQuickImageProvider are automatically cached, similar to any image loaded by the QML engine. When an image with a "image://" prefix is loaded from cache, requestImage() and requestPixmap() will not be called for the relevant image provider. 
    

    There aren't many details on the mechanism used for caching. I assumed that there is some global cache, and if there were a scene with say 2 Image elements in it with exactly the same source, then the ImageProvider would only be called once. In my case, having delegates destroyed and recreated wouldn't cause any new picture requests to be made. This doesn't seem to be what is happening however.

    So it seems like every Image has its own local image cache, isn't it? What would be the point in this "cache" though? And why then mention this in the documentation at all, this should be a hidden implementation detail, because it seems to be as good as useless(=invisible) for the library user.

    And it presents me with a bit of challenge. Currently my ImageProvider fetches pictures from I-net asynchronously(of course), I also save them in my data model, which should make the caching implementation almost trivial. Following QQuickAsyncImageProvider's interface while doing so isn't trivial. Ideally i would like to call "finished" signal already in the constructor then, but it seems to be way too early.



  • You are right. In src/qtdeclarative/src/quick/util/qquickpixmapcache.cpp in QQuickPixmap::load there´s the caching mechanism.

    Do you actually get multiple calls to you´r requestImageResponse function, while you scroll your view?



  • Hi Jagh,

    In the V-Play demo app we created for Qt World Summit, we used a different approach for caching. We created a subclass of NetworkManager, which caches up to 10 MB of all data we load via network - images, texts, etc.

    You can find the full source code of the app on github, maybe it's helpful for you. The relevant code parts are in main.cpp and the cpp folder.

    It should be easy to use as well, just copy the C++ files to your project and add the

      engine.setNetworkAccessManagerFactory(new DiskCacheFactory(1024 * 1024 * 10));
    

    line and you should be good to go.

    Cheers,
    Lorenz



  • Thanks all for your replies. I "stared" your project on github, Lorenz, will look it up later. This sounds like somewhat clever solution, but i don't think i will need it in this application(I generally don't like changing behaviour of library classes by subclassing them - too invasive for my taste - , if there are alternatives), and i already came up with a solution that should work.

    to sneubert: each time a delegate becomes visible(or is recreated by ListView), there is a new requestImageResponse() call. That what makes me think that current "caching" in Image elements is nearly useless for the user.



  • maybe report a bug, because this should do


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.