본문 바로가기
프로그래밍/iOS

UIImageView에 원격이미지 비동기 로드 및 캐쉬 기능 넣기

by 백룡화검 2011. 7. 4.


아이폰 개발시작한지 3~4개월 되어가면서 조금 감각을 익히기 시작했습니다. 
어플도 그동안 3개 만들어 올려보고요.  

물론 사운드, 그래픽을 고급스럽게 다뤄보지는 못했지만 기본 UI기반으로 개발할때도 워낙 이슈가 많다보니 학습과 개발을 병행하는게 정말 쉽지 않더군요.... 

각설하고요 ㅎㅎ

여기에 소개할 것은 UIImageView에 원격 이미지를 비동기로 로드할 수 있도록 하는 기능과 이미지 캐쉬 기능을 추가한 소스를 공개하려고 합니다.(소스 분석은 주석을 참고 ^^)

아이폰 어플 개발하면 UIImageView를 매우 많이 사용할 겁니다.  번들이미지의 경우야 어짜피 문제 없지만 원격이미지를 로드할때는  몇가지 이슈가 발생합니다.

1. 원격이미지를 매번 로드하는 것은 네트워크 부하를 일으키며 특히나 3G사용자들에게 치명적이 될 수 있다.
2. 테이블에 원격이미지를 붙이는 경우에 동기적으로 이미지 로드하는 경우 멈춤 현상을 일으킬 수 있다. 
3. 비동기적으로 이미지를 로드하더라도 한번에 로드할 수 있는 이미지를 제어하지 않으면 어플의 전체적인 퍼포먼스가 죽는다.
4. TableView에서 TableViewCell는 캐쉬처리되어 재사용된다. 그러므로 거기에 붙은 UIImageView도 재사용하게 되는데 스크롤을 빨리 넘기는 경우 기존에 로드 요청한 이미지가 하나의 UIImageView에 계속 적재되는 현상이 발생할 수 있다.

위 이슈에서 1, 2는 금방 이해갈 갈겁니다. 3번의 경우 1,2번의 처리가 잘되었다고 해도, 한 화면에 5개 이상의 이미지를 비동기적으로 로드한다는 것은 스레드를 5개 이상 생성해서 처리한다는 의미와 같습니다. 어플 하나에 스레드를 너무 돌리면 별로 좋지 못하기 때문에 동시에 로드할 수 있는 이미지를 제어해주어야 합니다. 1~2개 정도로요. 
4번 이슈의 경우 예전에 문씨님이 올려주신 [문씨의 강좌]멀티스레딩2<NSOperation>에 올린 글에 소개된 소스의 경우에 발생합니다. 저는 여기에 이미지 캐쉬기능만 넣어서 실제로 썼습니다. 하지만 TableView에서 문제가 발생했습니다. 분명 NSOperation을 이용한 이미지 로드는 2,3 문제를 해결해줍니다. 하지만 TableViewCell을 재사용되기 때문에 빠른 스크롤을 하는 경우 거기에 붙은 UIImageView 하나에  지속적으로 다른 이미지가 붙도록 요청이 되어 이미지가 광고롤링되는 현상마냥 보이는 경우가 발생합니다. 

1,2,3,4 번의 이슈를 모두 해결하고자 간단하게 클래스를 제작했습니다.

사 용하는 방법은 너무도 간단합니다. 그저 UIImageView를 붙히고 (IB에서든 코드상이든) #import "UIImageView+AsyncAndCache.h"를  넣습니다. 그 다음 아래 UIImageView 카테고리 4개 함수중 하나를 쓰시면 됩니다.  UIImageView *imageView = [[UIImageView alloc]init]; 하신뒤 [imageView setImageURLString:@"이미지 원격 경로"]; 형태로 쓰시면 됩니다. 

UIImageView를 카테고리로 만들었으므로 기존 UIImageView 기능은 그대로 사용할 수 있겠고요.

UIImageView 카테고리 외에 내부적으로 2개의 클래스가 정의되어 있습니다. 이는 개발자가 직접 제어하지 않고 위 4개 이슈를 해결하기 위해 내부적으로 사용되는 클래스입니다. 소스 분석을 원하신다면 주석을 달아두었으니 참고하시면 되겠습니다.

아래는 header만 올려놓습니다. 구현부는 첨부파일을 참고하세요.

//

//  UIImageView+AsyncAndCache.h

//

//  Created by Yongho Ji on 10. 12. 3..

//  Copyright 2010 Wecon Communications. All rights reserved.

//


#import <UIKit/UIKit.h>


@class AsyncAndCacheImageOperator;

@class AsyncAndCacheImageOperatorManager;


//////////////////////////////////////////////////////////////

//

// UIImageView 대한 카테고리 

// 카테고리는 테이블뷰에 적용된 Cell안에 UIImageView에서 활용하면 좋다.

// UIImageView 주요 기능은 다음과 같다

//

// 1. 이미지 비동기 로드 

//   이미지를 비동기로 로드해서 화면에 이미지가 뜨는데 버벅거림을 없앤다.

// 2. 이미지 캐쉬기능 

//   이미 로드한 이미지는 cache 디렉토리에 캐쉬해서 나중에 반복 요청시 

//   로컬에 저장된 캐쉬 이미지를 로드해서 네트워크 부하를 없애준다.

// 3. 반복적인 이미지 요청에 대한 로드부하 최소화 

//    같은 UIImageView 중복으로 로드 요청한다면 마지막에 요청한 이미지가 적용되도록 한다.

//    그뿐 아니라 수십번 반복해서 요청하더라도 무조건 이미지 로드 요청하지 않고 되도록이면 

//   마지막 이미지를 로드요청하여 네트워크 부하를 줄여준다

// 

// 개선해야할 사항 

//

// 1. 캐쉬기능 강제삭제기능 

// 2. 지정된 시간이 지난 캐쉬 이미지 자동 삭제기능 

// 3. 캐쉬사용여부 결정기능  

// 

//////////////////////////////////////////////////////////////


@interface UIImageView (AsyncAndCache)


//String형태의 이미지 URL 초기화

-(id)initWithURLString:(NSString*)url;


//NSURL 형태의 이미지 URL 초기화 

-(id)initWithURL:(NSURL*)url;


//String형태의 이미지 URL 셋팅 

-(void)setImageURLString:(NSString*)url;


//NSURL 형태의 이미지 URL 셋팅 

-(void)setImageURL:(NSURL*)url;


//동시에 로드할 이미지 최대수  

+(void)setMaxAsyncCount:(NSUInteger)count;


@end


//////////////////////////////////////////////////////////////

//

// 한개의 ImageView 대한 오퍼레이터이다.

// 비동기적으로 로드하는 것을 지원하며 더불어 캐쉬기능까지 지닌다.

// 개발자가 클래스를 직접 사용하지 않는다.

// 클래스는 AsyncAndCacheImageOperatorManager 클래스에서 동작/관리한다.

//

//////////////////////////////////////////////////////////////

@interface AsyncAndCacheImageOperator : NSObject

{

NSURL *_url; //로드할 이미지의 URL 정보 

UIImageView *_imageView; //이미지를 적용할 View

BOOL _canceled; //이미지 적용을 막는다. UIImageView 재사용시 나중에 로드되더라도 이게 YES이면 적용하지 못하도록 해서 사용자들로 하여금 잘못된 이미지가 로드되는 것을 방지 한다.

id _loadCompleteTarget; //이미지 로드가 완료되었을때 호출할 target

SEL _loadCompleteSelector; //이미지 로드를 완료했을때 호출할 selector

}


@property (readonly) UIImageView *imageView;


//초기화 함수 

- (id)initWithURL:(NSURL*)url imageView:(UIImageView*)imageView;


//스레드 적용 함수 

- (void)main;


//이미지 적용 취소

//main 메서드가 실행중일때 스레드자체는 중단시킬 없지만 imageView 로드한 image 적용하는 것은 방지시킨다.

- (void)cancel;


//이미지 로드 완료후 호출할 target/selector 적용 

- (void)setLoadCompleteWithTarget:(id)target selector:(SEL)selctor;

@end


//////////////////////////////////////////////////////////////

//

// ImageView정보를 담은 여러개의 오퍼레이터(AsyncAndCacheImageOperator클래스 객체) 관리한다.

// 개발자가 클래스를 직접 사용하지 않는다.

// setMaxAsyncCount 이용해 한번에 로드할 있는 이미지 갯수를 설정할 있다.

// 중요한 것은 동일한 UIImageView 대해서 다른 이미지 로드 요청이 있는 경우 

// 마지막에 요청한 이미지가 붙도록 하며, 같은 UIImageView 이미지 로드 대기중인 경우에는 

// 이전 UIImageView 대한 Operator 삭제함으로써 부적절한 로드로 인한 네트워크 부하를 최소화 해준다.

//

//////////////////////////////////////////////////////////////

@interface AsyncAndCacheImageOperatorManager : NSObject

{

@private

NSUInteger _maxAsyncCount; //동시에 비동기적으로 로드할 이미지 갯수 

NSUInteger _currentAscynCount; //현재 비동기적으로 로드하고 있는 이미지 갯수 

NSMutableArray *_standByImageOperators; //대기중인 Image Operator

NSMutableArray *_loadImageOperators; //로드중인 Image Operator 

}


//한번에 로드할 있는 이미지 갯수(스레드 최대 갯수)

-(void)setMaxAsyncCount:(NSUInteger)count;


//오퍼레이터 추가 

-(void)addImageOperator:(AsyncAndCacheImageOperator*)imageOperator;


@end



제 소스는 많은 테스트는 거치지 못했습니다. 그러므로 여기 개발자 분들께서 필요하시다면 제 소스를 분석하고 수정하면서 개선해주셨으면 합니다.  

----------------------------
수정사항 1
역시 소스가 공개되니 많은 분들이 테스트도 해주시고 좋네요. 만 약 이미지 경로가 image.php?ggg=465.jpg로 되어 있다면 ?ggg=465.jpg 부분을 제대로 가져오지 못하는 버그가 있더군요. [_url path]로 되어 있는 부분을 [_url absoluteString]으로 하면 괜찮다고 합니다. 수정해서 쓰세요. 

수정사항 2
초기에 이미지가 붙어 있는데 주어진 이미지 경로에 이미지를 못불러오는 경우 image = nil이 되기 때문에 초기 이미지를 지우는 부분으로 이상하시다는 분도 계셨습니다. 이 부분은 아래처럼 처리하세요.

if (_canceled==NO) 
{
// 이 부분 추가
if (image)
_imageView.image = image;
}

if (_loadCompleteTarget!=nil) 
{
[_loadCompleteTarget performSelectorOnMainThread:_loadCompleteSelector withObject:self waitUntilDone:YES];
}