Лучший способ кэшировать изображения в приложении ios?

Вскоре у меня есть NSDictionary с URL-адресами изображений, которые мне нужно показать в моем UITableView. У каждой ячейки есть заголовок и изображение. Я успешно добился этого, хотя прокрутка задерживалась, так как казалось, что клетки загружали свое изображение каждый раз, когда они появлялись на экране. Я поискал немного и нашел SDWebImage на github. Это заставило пропустить задержку прокрутки. Я не совсем уверен, что он сделал, но я полагал, что это было кеширование. Но! Каждый раз, когда я открываю приложение в первый раз, я не вижу изображений, и мне нужно прокрутить вниз и вернуться назад, чтобы они появились. И если я выхожу из приложения с помощью кнопки «Домой» и снова открываюсь, то кажется, что кеширование работает, потому что изображения на экране видны, однако, если я прокручиваю одну ячейку вниз, в следующей ячейке нет изображения. Пока я не прокручу его и не вернусь назад, или пока я не нажму на него. Это как должно работать кеширование? Или как лучше всего кэшировать изображения, загруженные из Интернета? Изображения обновляются редко, поэтому я был близок к тому, чтобы просто импортировать их в проект, но мне нравится иметь возможность обновлять изображения без загрузки обновления ..

Невозможно ли загрузить все изображения для всего tableview из кеша (учитывая, что в кеше что-то есть) при запуске? Поэтому я иногда вижу клетки без изображений?

И да, мне сложно понять, что такое кеш.

--EDIT--

Я пробовал это только с изображениями одинакового размера (500x150), и ошибка аспекта исчезла, однако, когда я прокручиваю вверх или вниз, во всех ячейках есть изображения, но сначала они ошибочны. После того, как ячейка находится в поле зрения в течение нескольких миллисекунд, появляется правое изображение. Это ужасно раздражает, но, может быть, как это должно быть? .. Похоже, что сначала он выбирает неправильный индекс из кеша. Если я прокручиваю медленно, я вижу, как изображения мигают с неправильного изображения на правильное. Если я прокручиваю быстро, я считаю, что неправильные изображения видны всегда, но я не могу сказать из-за быстрой прокрутки. Когда быстрая прокрутка замедляется и в конечном итоге останавливается, неправильные изображения по-прежнему отображаются, но сразу после прекращения прокрутки происходит обновление до правильных изображений. У меня также есть собственный класс UITableViewCell, но я не внес никаких серьезных изменений ... Я еще не очень много изучал свой код, но я не могу придумать, что может быть не так ... Может быть, у меня что-то есть в неправильный порядок ... Я много программировал на java, c #, php и т.д., но мне трудно понять Objective-c со всеми .h и .m ... У меня также есть `

@interface FirstViewController : UITableViewController{

/**/
NSCache *_imageCache;
}

(среди других переменных) в FirstViewController.h. Это не так?

Вот мой cellForRowAtIndexPath.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"hallo";
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil)
{
    cell = [[CustomCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}

NSMutableArray *marr = [hallo objectAtIndex:indexPath.section];
NSDictionary *dict = [marr objectAtIndex:indexPath.row];

NSString* imageName = [dict objectForKey:@"Image"];
//NSLog(@"url: %@", imageURL);

UIImage *image = [_imageCache objectForKey:imageName];

if(image)
{
    cell.imageView.image = image;
}
else
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        NSString* imageURLString = [NSString stringWithFormat:@"example.com/%@", imageName];
        NSURL *imageURL = [NSURL URLWithString:imageURLString];
        UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:imageURL]];

        if(image)
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                CustomCell *cell =(CustomCell*)[self.tableView cellForRowAtIndexPath:indexPath];
                if(cell)
                {
                    cell.imageView.image = image;
                }
            });
            [_imageCache setObject:image forKey:imageName];
        }
    });
}

cell.textLabel.text = [dict objectForKey:@"Name"];

return cell;
}

person Sti    schedule 16.07.2012    source источник
comment
Можете ли вы разместить свой код в cellRowRowAtIndexPath и что-нибудь еще, что может быть здесь актуально? Кэширование - это просто сохранение того, к чему вы ожидаете снова получить доступ, в более доступном месте для ускорения последующего доступа. В этом случае кэширование означает, что ваше приложение должно фактически сохранять данные изображения для каждой ячейки после ее загрузки в первый раз, а затем просто повторно использовать их каждый раз, когда необходимо отобразить эту конкретную ячейку (это то, что SDWebImage должен делать для ты).   -  person Dima    schedule 16.07.2012
comment
Выполнено. Я отключил SDWebImage, но все равно получаю странные результаты .. Иногда при прокрутке отображается неправильное изображение ..   -  person Sti    schedule 17.07.2012
comment
Просто инициализируйте cell.imageView.image прямо перед первым dispatch_async.   -  person Rob    schedule 17.07.2012
comment
Я знаю, что уже немного поздно, но мы выпустили библиотеку с открытым исходным кодом, которая отлично решает эту задачу - github.com/ Alterplay / APSmartStorage. Он получает файл из Интернета и сохраняет его на диске и в памяти.   -  person slatvick    schedule 10.02.2014
comment
checkout UIImageLoader github.com/gngrwzrd/UIImageLoader - это помогает именно в этой ситуации.   -  person gngrwzrd    schedule 23.12.2015


Ответы (6)


Кэширование означает просто сохранение копии необходимых данных, чтобы вам не приходилось загружать их из более медленного источника. Например, микропроцессоры часто имеют кэш-память, в которой хранятся копии данных, поэтому им не нужно обращаться к ОЗУ, что намного медленнее. Жесткие диски часто имеют кеш-память, из которой файловая система может гораздо быстрее получить доступ к блокам данных, к которым недавно обращались.

Точно так же, если ваше приложение загружает много изображений из сети, может быть в ваших интересах кэшировать их на вашем устройстве, вместо того, чтобы загружать их каждый раз, когда они вам нужны. Есть много способов сделать это - похоже, вы уже нашли один. Возможно, вы захотите сохранить загруженные изображения в каталоге вашего приложения / Library / Caches, особенно если вы не ожидаете, что они изменятся. Загрузка изображений из вторичного хранилища будет намного быстрее, чем их загрузка по сети.

Возможно, вас заинтересует малоизвестный NSCache класс для хранения в памяти нужных изображений. NSCache работает как словарь, но когда памяти становится мало, он начинает освобождать часть своего содержимого. Вы можете сначала проверить кеш для данного изображения, и если вы не найдете его там, вы можете заглянуть в свой каталог кэшей, и если вы не найдете его там, вы можете загрузить его. Ничто из этого не ускорит загрузку изображений в вашем приложении при первом запуске, но как только ваше приложение загрузит большую часть того, что ему нужно, оно станет гораздо более отзывчивым.

person Caleb    schedule 16.07.2012
comment
Вау, отличный ответ. Я проверю NSCache. Но я не могу представить, как это работает ... Если я создам объект NSCache, скажем, в viewDidLoad, не будет ли он пуст каждый раз, когда я запускаю свое приложение? Или, может быть, это работает как-то иначе .. Как мне получить доступ к моему предыдущему объекту кеширования ..? Хорошо, я прочитаю это утром. Спасибо! - person Sti; 17.07.2012
comment
Да, было бы. Однако прочитайте больше, и вы увидите, что у него есть метод делегата, который вызывается перед удалением объекта - вы можете использовать его для сохранения объектов во вторичное хранилище. Или вы можете реализовать свой собственный дисковый кеш и исключить NSCache. - person Caleb; 17.07.2012
comment
Привет, Калеб, отличный ответ, но у меня есть записка. Как доступ к ОЗУ может быть МЕДЛЕННЫМ, чем доступ к жесткому диску. Насколько мне известно, все наоборот, доступ к адресу RAM выполняется быстрее, чем любой другой способ хранения в памяти. Спасибо. - person Malloc; 07.06.2013
comment
@Malloc Вы, кажется, неправильно поняли; Я не говорил, что диски быстрее ОЗУ. У процессоров часто есть кеши, поэтому им не нужно обращаться к ОЗУ, и такие кеши создаются с использованием более быстрой памяти, чем та, которая используется для основной памяти. Точно так же диски часто содержат твердотельные кэши, которые работают быстрее, чем диск. Это экономит время из-за локальности ссылки, то есть идеи о том, что если процессор запрашивает один фрагмент данных, существует более высокая вероятность того, что ему снова понадобятся эти данные или некоторые данные в непосредственной близости. - person Caleb; 07.06.2013
comment
@Malloc Кроме того, поскольку OP запрашивает в отношении iOS, стоит отметить, что здесь нет реального диска - он все твердотельный. Тем не менее, есть диапазон скоростей ... В порядке убывания скорости: регистры, кеш процессора, основная память, вторичное хранилище, сеть. - person Caleb; 07.06.2013
comment
Я обнаружил, что огромная проблема с использованием NSCache заключается в том, что он освобождает всю свою память при переходе в фоновый режим. Это означает, что изображения нужно загружать заново. На мой взгляд не оптимально - person Pedroinpeace; 19.11.2014
comment
Если URLCache выполняет все кеширование, просто следуя cachePolicy, то почему существуют сторонние API-интерфейсы для кэширования изображений? Что они предлагают, чем не поставляется из коробки? - person Honey; 15.10.2018

Думаю, Калеб хорошо ответил на вопрос о кешировании. Я просто собирался коснуться процесса обновления вашего пользовательского интерфейса при получении изображений, например предполагая, что у вас есть NSCache для ваших изображений с именем _imageCache:

Сначала определите свойство очереди операций для представления таблицы:

@property (nonatomic, strong) NSOperationQueue *queue;

Затем в viewDidLoad инициализируйте это:

self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 4;

А затем в cellForRowAtIndexPath вы могли:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ilvcCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    // set the various cell properties

    // now update the cell image

    NSString *imagename = [self imageFilename:indexPath]; // the name of the image being retrieved

    UIImage *image = [_imageCache objectForKey:imagename];

    if (image)
    {
        // if we have an cachedImage sitting in memory already, then use it

        cell.imageView.image = image;
    }
    else
    {
        cell.imageView.image = [UIImage imageNamed:@"blank_cell_image.png"];

        // the get the image in the background

        [self.queue addOperationWithBlock:^{

            // get the UIImage

            UIImage *image = [self getImage:imagename];

            // if we found it, then update UI

            if (image)
            {
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    // if the cell is visible, then set the image

                    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
                    if (cell)
                        cell.imageView.image = image;
                }];

                [_imageCache setObject:image forKey:imagename];
            }
        }];
    }

    return cell;
}

Я упоминаю только об этом, поскольку недавно видел несколько примеров кода, плавающих на SO, которые используют GCD для обновления соответствующего свойства UIImageView image, но в процессе отправки обновления пользовательского интерфейса обратно в основную очередь они используют любопытные методы (например, , перезагружая ячейку или таблицу, просто обновляя свойство изображения существующего объекта cell, возвращенного в верхней части tableView:cellForRowAtIndexPath (что является проблемой, если строка прокручена за пределы экрана, а ячейка была исключена из очереди и повторно используется для нового ряд) и т. д.). Используя cellForRowAtIndexPath (не путать с tableView:cellForRowAtIndexPath), вы можете определить, видна ли ячейка и / или она могла быть прокручена, исключена из очереди и повторно использована.

person Rob    schedule 16.07.2012
comment
Большое спасибо за ответ и за код! Я попробовал код, но случилось что-то странное ... Когда я быстро прокручиваю, некоторые аспекты на некоторых изображениях неверны ... Кажется, что аспект изображения ниже переносится на изображение поверх него, или наоборот вокруг .. Можно ли этого избежать, или мне нужно сделать все изображения одинакового размера? (или, по крайней мере, с одинаковым аспектом) .. - person Sti; 17.07.2012
comment
@StianFlatby Это странно. Вы видите другое поведение при медленной прокрутке и при быстрой прокрутке? И выполняете ли вы какую-либо конфигурацию своего изображения вне этого dispatch_async в основной очереди? Я провел много тестов, но должен признать, что все изображения для моих табличных представлений имеют одинаковый размер (и небольшие эскизы изображений, оптимизированные для табличных представлений ... большие изображения в табличных представлениях всегда будут иметь ужасную производительность). Возможно, вы сможете обновить свой вопрос полным исходным кодом для своего cellForRowAtIndexPath. - person Rob; 17.07.2012
comment
@StianFlatby Я только что протестировал его с библиотекой изображений разного размера, и мне кажется, что (а) разница в отображении ячеек была функцией того, было ли изображение загружено из кеша (синхронно в основной очереди) или нет (асинхронно отправка из фоновая очередь), а не скорость прокрутки; и (б) похоже, что если вы делаете это синхронно, ячейка по умолчанию переформатируется в зависимости от размера / наличия изображений, поэтому вы либо хотите создать свою собственную ячейку, и, таким образом, вы можете настроить свой imageView любым способом хотите или просто измените размер изображений до определенного размера. - person Rob; 17.07.2012
comment
Спасибо за старания! Я обновил свой вопрос новой информацией и некоторым кодом. Буду признателен, если вы посмотрите и увидите, что не так. - person Sti; 17.07.2012
comment
Вы, вероятно, видите изображение из исключенной из очереди ячейки, поэтому вы можете захотеть инициализировать cell.imageView.image перед тем, как асинхронно уйти и попытаться получить новый. Итак, просто инициализируйте перед этим. Я соответствующим образом изменил свой фрагмент кода. - person Rob; 17.07.2012
comment
Значит, вместо того, чтобы показывать неправильное изображение, оно не будет отображаться? Я не думаю, что это решит эту проблему .. Это также происходит, когда все правильные изображения загружены правильно. Я медленно прокручиваю всю таблицу и могу подтвердить, что во всех ячейках отображаются правильные изображения, но когда я быстро прокручиваю назад, отображаются неправильные изображения. Также при полубыстрой прокрутке изображения заметно обновляются. Это означает, что изображение существует в кеше, поэтому это не должно быть проблемой blank_cell ..? Это должно быть в if (image) {}, а не в else {}. Может я ошибаюсь. Я попробую сейчас - person Sti; 17.07.2012
comment
@StianFlatby Когда вы прокручиваете строки, которые прокручиваются за пределы представления, их ячейки повторно используются для использования другими строками. Таким образом, когда ячейка удаляется из очереди и используется повторно, старое изображение все еще там. Итак, если вы решите, что ваша новая ячейка не имеет кэшированного изображения, прежде чем вы уйдете, чтобы получить новое изображение в фоновом режиме, вам нужно убедиться, что вы избавились от любого старого изображения, сидящего там, иначе вы ' Вы увидите, как старое изображение (для старой, исключенной из очереди строки) остается там, пока не будет загружено новое изображение. - person Rob; 17.07.2012
comment
позвольте нам продолжить обсуждение в чате - person Sti; 17.07.2012

Самое простое решение - использовать что-то активно используемое, прошедшее стресс-тестирование.

SDWebImage - это мощный инструмент, который помог мне решить аналогичную проблему и может быть легко установлен с капсулами какао. В подфиле:

platform :ios, '6.1'
pod 'SDWebImage', '~>3.6'

Настроить кеш:

SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image)
{
    // image is not nil if image was found
}];

Изображение кеша:

[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];

https://github.com/rs/SDWebImage

person Matt Perejda    schedule 24.10.2014

Думаю, пользователю лучше подойдет что-нибудь вроде DLImageLoader. Дополнительная информация -> https://github.com/AndreyLunevich/DLImageLoader-iOS

[[DLImageLoader sharedInstance] loadImageFromUrl:@"image_url_here"
                                   completed:^(NSError *error, UIImage *image) {
                                        if (error == nil) {
                                            imageView.image = image;
                                        } else {
                                            // if we got an error when load an image
                                        }
                                   }];
person MaeSTRo    schedule 14.08.2014

Что касается вопроса о неправильных изображениях, это из-за повторного использования ячеек. Повторное использование ячеек означает, что существующие ячейки, которые выходят из поля зрения (например, ячейки, которые выходят за пределы экрана вверху, когда вы прокручиваете вниз, возвращаются снова снизу). неправильные изображения. Но как только ячейка появляется, выполняется код для получения правильного изображения, и вы получаете правильные изображения.

Вы можете использовать заполнитель в методе prepareForReuse ячейки. Эта функция в основном используется, когда вам нужно сбросить значения, когда ячейка используется для повторного использования. Установка здесь заполнителя гарантирует, что вы не получите неправильных изображений.

person Swasidhant    schedule 01.06.2016

Кэширование изображений можно сделать так же просто.

ImageService.m

@implementation ImageService{
    NSCache * Cache;
}

const NSString * imageCacheKeyPrefix = @"Image-";

-(id) init {
    self = [super init];
    if(self) {
        Cache = [[NSCache alloc] init];
    }
    return self;
}

/** 
 * Get Image from cache first and if not then get from server
 * 
 **/

- (void) getImage: (NSString *) key
        imagePath: (NSString *) imagePath
       completion: (void (^)(UIImage * image)) handler
    {
    UIImage * image = [Cache objectForKey: key];

    if( ! image || imagePath == nil || ! [imagePath length])
    {
        image = NOIMAGE;  // Macro (UIImage*) for no image 

        [Cache setObject:image forKey: key];

        dispatch_async(dispatch_get_main_queue(), ^(void){
            handler(image);
        });
    }
    else
    {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0ul ),^(void){

            UIImage * image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[imagePath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]];

            if( !image)
            {
                image = NOIMAGE;
            }

            [Cache setObject:image forKey: key];

            dispatch_async(dispatch_get_main_queue(), ^(void){
                handler(image);
            });
        });
    }
}


- (void) getUserImage: (NSString *) userId
           completion: (void (^)(UIImage * image)) handler
{
    [self getImage: [NSString stringWithFormat: @"%@user-%@", imageCacheKeyPrefix, userId]
         imagePath: [NSString stringWithFormat: @"http://graph.facebook.com/%@/picture?type=square", userId]
        completion: handler];
}

SomeViewController.m

    [imageService getUserImage: userId
                    completion: ^(UIImage *image) {
            annotationImage.image = image;
    }];
person tsuz    schedule 06.01.2016
comment
Вы тестировали этот код и работает ли он? Кажется, что простой просмотр кода не сработает, поскольку ваш основной метод проверяет if( ! image || imagePath == nil || ! [imagePath length]), если изображение не существует, а затем устанавливает несуществующее изображение в кеш (плохая практика). В противном случае (иначе условие), если изображение существует, вы его синхронно извлекаете ??? - person Stunner; 24.12.2018