Alexander Khitev
Alexander Khitev

Reputation: 6851

Kingfisher caches the data in RAM

I do like a news feed, I had the following problem, if for example the user uploads more than 300 news, then the application will already occupy more than 300 megabytes of memory. Once during the test, I did get didReceiveMemoryWarning and it helped only the full cleansing of the dataSource. I also use Kingfisher to cache images. What is the best way for this situation? Cache the first data and if the user will return to the top (to the newest data), then load them from the cache or if some better way? Thanks.

Update: this is news JSON model.

["peopleProperties": ["numberOfPeopleDescription": "Nobody here", "numberOfPeople": 0, "availableSeats": 0], "isPrivateStatus": false, "additionalInfo": ["note": ""], "ownerID": "", "ticketsInfo": ["tickets": []], "isTest": false, "isNewPendingRequest": false, "dateProperties": ["isEditable": true, "iso8601": "", "day": "", "endTimeStamp": 0.0, "isFlexDate": true, "isFlexTime": true, "timeStamp": 0.0], "boolProperties": ["isPartnerGeneratedCard": false, "isAutoGeneratedCard": true, "isUserCreatedCard": false, "isAdminCreatedCard": false], "location": ["formattedAddress": "692 N Robertson Blvd (at Santa Monica Blvd),West Hollywood, CA 90069,United States", "fullLocationName": "692 N Robertson Blvd", "coordinate": ["longitude": -118.38528500025966, "latitude": 34.083373986214625]], "id": "", "photoURLsProperties": ["placePhotoURLs": ["example"], "placeLogoURLs": []], "services": ["serviceURL": "", "serviceID": "41cf5080f964a520a61e1fe3", "index": 1], "version": 1, "title": "The Abbey Food & Bar", "ownerName": "", "phones": [:]]

UPDATE 1. Sometimes it comes to app crash. My test controller

import UIKit
import SVProgressHUD
class CardTestTableViewController: UITableViewController {

    // MARK: - Managers

    fileprivate let firCardDatabaseManager = FIRCardDatabaseManager()
    fileprivate let apiManager = ableCardsAPIManager()

    // MARK: - API Manager's properties

    fileprivate var firstCardsCount = 0
    fileprivate var isSecondTypeRequestLaunch = false

    /// Main cards array
    fileprivate var cardsModels = [CardModel]()
    fileprivate var firCardsModels = [CardModel]()
    fileprivate var backendCardsModels = [CardModel]()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        definesPresentationContext = true
        requestAllData()

        // table view
        registerCells()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(false, animated: false)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        debugPrint("didReceiveMemoryWarning")
        cardsModels.removeAll()
        tableView.reloadData()
        // Dispose of any resources that can be recreated.
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if cardsModels.count > 0 {
            debugPrint("cardsModels.first!.toJSON()", cardsModels.first!.toJSON(), "cardsModels.first!.toJSON()")

        }
        return cardsModels.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = cardTestTableViewCell(tableView, indexPath: indexPath)

        let lastElement = cardsModels.count - 15
        if indexPath.row == lastElement {
            secondRequest(indexPath.row)
        }

        return cell
    }

    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 298
    }


}

extension CardTestTableViewController {

    fileprivate func registerCells() {
        let nib = UINib(nibName: CardTestTableViewCell.defaultReuseIdentifier, bundle: Bundle.main)
        tableView.register(nib, forCellReuseIdentifier: CardTestTableViewCell.defaultReuseIdentifier)
    }


}

// MARK: - Cells 

extension CardTestTableViewController {

    fileprivate func cardTestTableViewCell(_ tableView: UITableView, indexPath: IndexPath) -> CardTestTableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CardTestTableViewCell.defaultReuseIdentifier, for: indexPath) as! CardTestTableViewCell

        let card = cardsModels[indexPath.row]

        cell.setupData(card)

        return cell
    }

}

// MARK: - Requests

extension CardTestTableViewController {

    @objc fileprivate func requestAllData() {
        let requestGroup = DispatchGroup()
        if let topViewController = UIApplication.topViewController() {
            if topViewController.isKind(of: CardViewController.self) {
                SVProgressHUD.show()
            }
        }
        firCardsModels.removeAll()
        backendCardsModels.removeAll()

        requestGroup.enter()
        firCardDatabaseManager.getCardModelsByUserLocation(success: { [weak self] (userCardsModels) in
            debugPrint("Finish +++ fir", userCardsModels.count)
            self?.firCardsModels = userCardsModels
            requestGroup.leave()
        }) { (error) in
            // TODO: - Think about it: Do not show an error, because we have cards with FourSquare
            debugPrint("FIRCardDatabaseManager error", error.localizedDescription)
            requestGroup.leave()
        }

        requestGroup.enter()
        apiManager.requestCards(10, secondRequestLimit: 50, isFirstRequest: true, success: { [weak self] (cards) in
            self?.backendCardsModels = cards
            requestGroup.leave()
        }) { (error) in
            requestGroup.leave()
        }

        requestGroup.notify(queue: .main) { [weak self] in
            guard let _self = self else { return }
            _self.cardsModels.removeAll()
            _self.cardsModels.append(contentsOf: _self.firCardsModels)
            _self.cardsModels.append(contentsOf: _self.backendCardsModels)

            self?.tableView.reloadData()
            // for api manager
            self?.firstCardsCount = _self.cardsModels.count

            SVProgressHUD.dismiss()
        }
    }


    fileprivate func secondRequest(_ index: Int) {
        // the second request
        debugPrint("swipe index", index, "firstCardsCount", firstCardsCount)

        // This is for how much to the end of the deck, we ask for more cards.
        let muchMoreIndex = 15
        let checkNumber = firstCardsCount-1 - index - muchMoreIndex
        debugPrint("checkNumber", checkNumber)

        if checkNumber == 0 || checkNumber < 0 {
            guard !isSecondTypeRequestLaunch else { return }
            isSecondTypeRequestLaunch = true
            apiManager.requestCards(0, secondRequestLimit: 50, isFirstRequest: false, success: { [weak self] (backendCards) in
                DispatchQueue.main.async {
                    guard let _self = self else { return }
                    _self.cardsModels.append(contentsOf: backendCards)
                    _self.firstCardsCount = _self.cardsModels.count
                    _self.isSecondTypeRequestLaunch = false

                    _self.tableView.reloadData()
                }

                }, fail: { [weak self] (error) in
                    self?.isSecondTypeRequestLaunch = false
            })
        }
    }

}

import UIKit
import Kingfisher

class CardTestTableViewCell: UITableViewCell {

    @IBOutlet private weak var titleLabel: UILabel!
    @IBOutlet private weak var cardImageView: UIImageView!
    @IBOutlet private weak var profileImageView: UIImageView!

    override func prepareForReuse() {
        cardImageView.image = nil
        profileImageView.image = nil
    }

    func setupData(_ card: CardModel) {
        downloadImages(card)
        setupLabelsData(card)
    }

    private func downloadImages(_ card: CardModel) {
        if let placeAvatarURLString = card.photoURLsProperties.placePhotoURLs.first {
            if let placeAvatarURL = URL(string: placeAvatarURLString) {
                cardImageView.kf.indicatorType = .activity
                cardImageView.kf.setImage(with: placeAvatarURL)
            } else {
                cardImageView.image = UIImage(named: "CardDefaultImage")
            }
        } else if let eventLogoURLPath = card.photoURLsProperties.placeLogoURLs.first {
            if let url = URL(string: eventLogoURLPath) {
                cardImageView.kf.indicatorType = .activity
                cardImageView.kf.setImage(with: url)
            } else {
                cardImageView.image = UIImage(named: "CardDefaultImage")
            }
        } else {
            cardImageView.image = UIImage(named: "CardDefaultImage")
        }

        guard card.boolProperties.isAutoGeneratedCard != true && card.boolProperties.isAdminCreatedCard != true else {
            profileImageView.image = #imageLiteral(resourceName: "ProfileDefaultIcon")
            return
        }

        let firImageDatabaseManager = FIRImageDatabaseManager()
        firImageDatabaseManager.downloadCardUserProfileImageBy(card.ownerID) { [weak self] (url, error) in
            DispatchQueue.main.async {
                guard error == nil else {
                    self?.profileImageView.image = #imageLiteral(resourceName: "ProfileDefaultIcon")
                    return
                }
                guard let _url = url else {
                    self?.profileImageView.image = #imageLiteral(resourceName: "ProfileDefaultIcon")
                    return
                }
                self?.profileImageView.kf.indicatorType = .activity
                self?.profileImageView.kf.setImage(with: _url)
            }
        }
    }

    private func setupLabelsData(_ card: CardModel) {
        titleLabel.text = card.title
    }

}

Update 2. When I commented out the code that is associated with the Kingfisher framework, then there is no memory leak and application crash.

Upvotes: 3

Views: 1995

Answers (1)

Alexander Khitev
Alexander Khitev

Reputation: 6851

I solved my problem, in fact Kingfisher initially stores all images in RAM, it has the property that if the application received a memory warning, then it should free up memory, but in my case this was not. So I set the limits for Kingfisher that you can only use 1 megabyte of RAM memory.

I placed this function in AppDelegate and call in function didFinishLaunchingWithOptions

fileprivate func setupKingfisherSettings() {
        ImageCache.default.maxMemoryCost = 1
    }

Upvotes: 4

Related Questions