칸드로이드 저널- "Beyond Android" 
[번역] Multithreading For Performance - 송경희
작성자
작성일 2010-08-06 (금) 01:35
ㆍ추천: 0  ㆍ조회: 11933      
IP: 221.xxx.153
 
이 글은 송경희님이 번역해서 보내주신 글 중 하나입니다.
원문 : http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html


 
Multithreading For Performance (성능 향상을 위한 멀티쓰레딩 기법)
 
Posted by Tim Bray on 19 July 2010 at 11:41 AM
 
[멀티태스크 상황에 처하는 것을 기꺼워하는 안드로이드 그룹 엔지니어 Gilles Debunne이 쓴 글. - Tim Bray]
 
 
 
응답성 있는 애플리케이션을 만드는 좋은 사례는 여러분의 메인 UI 쓰레드에게 최소한의 작업만을
시키는 것이다. 시간이 오래 걸릴 가능성이 있어서 애플리케이션에 장애가 될만한 작업은 별도의
쓰레드를 통해 처리해야 한다. 그러한 작업의 전형적인 예로는 예측하기 어려운 지연 현상들이
수반하는 네트워크 오퍼레이션을 들 수 있다. 사용자들은 일시적인 멈춤 정도라면 참아줄 수 있을 것이다.
특히, 뭔가를 진행하고 있다고 피드백을 제공해주면 인내심을 발휘할 수 있다.
그러나 아무 공지도 없이 멍 때리는 애플리케이션이라면 사용자들 입장에서 무슨 일이 벌어지는지,
얼마나 기다리면 되는 것인지 도저히 감을 잡을 수가 없을 것이다. 
 
오래 걸리는 작업을 별도의 쓰레드로 처리하는 패턴을 설명하기 위해, 이 글에서는 간단한 이미지
다운로더를 만들 것이다. 우리는 인터넷에서 다운로드받은 썸네일 이미지를 보여줄 ListView를 사용할 것이다.
백그라운드에서 다운로드 받는 비동기적인 작업을 만들면 애플리케이션이 좋은 속도를 유지하게 할 수 있다.
 
An Image downloader(이미지 다운로더)
 
프레임워크에 의해 제공되는 HTTP 관련 클래스들을 사용하여 웹에서 이미지를 다운로드 하는 것은
아주 간단하다. 예를 들어, 아래와 같은 구현이 있을 수 있다 :
 
static Bitmap downloadBitmap(String url) {
   
final AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
   
final HttpGet getRequest = new HttpGet(url);

   
try {
       
HttpResponse response = client.execute(getRequest);
       
final int statusCode = response.getStatusLine().getStatusCode();
       
if (statusCode != HttpStatus.SC_OK) {
           
Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url);
           
return null;
       
}
       
       
final HttpEntity entity = response.getEntity();
       
if (entity != null) {
           
InputStream inputStream = null;
           
try {
                inputStream
= entity.getContent();
               
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
               
return bitmap;
           
} finally {
               
if (inputStream != null) {
                    inputStream
.close();  
               
}
                entity
.consumeContent();
           
}
       
}
   
} catch (Exception e) {
       
// Could provide a more explicit error message for IOException or IllegalStateException
        getRequest
.abort();
       
Log.w("ImageDownloader", "Error while retrieving bitmap from " + url, e.toString());
   
} finally {
       
if (client != null) {
            client
.close();
       
}
   
}
   
return null;
}


위에서 클라이언트와 HTTP request가 생성되었다. Request가 성공하면 이미지를 보유하고 있는
response 개체 스트림이 디코딩되어 비트맵 결과를 생성한다.
이를 위해, 여러분 애플리케이션의 manifest는 INTERNET 퍼미션을 가져야 한다.

주: 이전 버전의 BitmapFactory.decodeStream을 쓰면, 네트워크 연결이 느린 경우 이 코드가
제대로 작동하지 않을 수도 있다. 이 문제를 해결하려면 이것 대신 새로운 FlushedInputStream(inputStream)을
디코드하라. 이 헬퍼 클래스의 구현은 아래와 같다:

static class FlushedInputStream extends FilterInputStream {
   
public FlushedInputStream(InputStream inputStream) {
       
super(inputStream);
   
}

   
@Override
   
public long skip(long n) throws IOException {
       
long totalBytesSkipped = 0L;
       
while (totalBytesSkipped < n) {
           
long bytesSkipped = in.skip(n - totalBytesSkipped);
           
if (bytesSkipped == 0L) {
                 
int byte = read();
                 
if (byte < 0) {
                     
break;  // we reached EOF
                 
} else {
                      bytesSkipped
= 1; // we read one byte
                 
}
           
}
            totalBytesSkipped
+= bytesSkipped;
       
}
       
return totalBytesSkipped;
   
}
}


이 코드에서는 EOF에 도달하지 않으면 skip() 메쏘드가 제공된 바이트 수만큼 실제로 스킵 하도록 보장한다.

이 메쏘드를 여러분의 ListAdapter의 getView 메쏘드에 직접 사용하고자 한다면, 스크롤링 결과는
불쾌할 정도로 버벅거릴 것이다. 새로운 뷰를 디스플레이할 때마다 이미지 다운로드를 대기해야 하고,
부드럽게 스크롤링할 수 없을 것이다.
 
이것은 정말 좋지 않은 방법이므로, AndroidHttpClient는 그 자신이 메인 쓰레드에서 시작되는 것을 불허한다.
위의 코드는 “이 쓰레드는 HTTP 요청을 허용하지 않습니다”라는 오류 메시지를 보일 것이다.
그렇다고 AndroidHttpClient 대신 DefaultHttpClient를 쓰면 정말로 후회할 일이 생길 것이다.

Introducing asynchronous tasks(비동기적 태스크 도입)
 
AsyncTask 클래스는 UI 쓰레드에서 새로운 작업으로 분기하게 하는 가장 간단한 방법들 중 하나를 제공해준다.
이러한 작업을 생성하는 임무를 띠는 ImageDownloader 클래스를 만들어보자.
이 클래스는 특정 URL로부터 다운로드된 이미지 하나를 ImageView 하나에 할당하는 다운로드 메쏘드를 제공한다.

public class ImageDownloader {

   
public void download(String url, ImageView imageView) {
           
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
            task
.execute(url);
       
}
   
}

   
/* class BitmapDownloaderTask, see below */
}


BitmapDownloaderTask는 실제적으로 이미지를 다운로드하는 AsyncTask이다. 이것은 execute를 통해
시작되고, execute는 즉시 리턴된다. 따라서 이 메쏘드가 아주 빨리 실행될 수 있다.
이 메쏘드는 UI 쓰레드로부터 호출될 것이므로, 이러한 속도 향상은 바로 우리가 목표하는 바이다.
이 클래스에 대한 구현은 아래와 같다: 

class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
   
private String url;
   
private final WeakReference<ImageView> imageViewReference;

   
public BitmapDownloaderTask(ImageView imageView) {
        imageViewReference
= new WeakReference<ImageView>(imageView);
   
}

   
@Override
   
// Actual download method, run in the task thread
   
protected Bitmap doInBackground(String... params) {
         
// params comes from the execute() call: params[0] is the url.
         
return downloadBitmap(params[0]);
   
}

   
@Override
   
// Once the image is downloaded, associates it to the imageView
   
protected void onPostExecute(Bitmap bitmap) {
       
if (isCancelled()) {
            bitmap
= null;
       
}

       
if (imageViewReference != null) {
           
ImageView imageView = imageViewReference.get();
           
if (imageView != null) {
                imageView
.setImageBitmap(bitmap);
           
}
       
}
   
}
}

 
doInBackground 메쏘드는 자기 자신의 프로세스에서 태스크에 의해 실제적으로 실행되는 메쏘드이다.
이 메쏘드는 단순히 이 글 서두에 구현했던 downloadBitmap 메쏘드를 사용한다.

onPostExecute는 태스크가 끝날 때, 호출하는 UI 쓰레드에서 실행된다. 이 메쏘드는 결과 Bitmap을
파라미터로 취한다. 그 비트맵은 단순히 다운로드에 제공된 imageView에 연결되고
BitmapDownloaderTask에서 저장되었던 것이다. 이 ImageView는 WeakReference로 저장된 것을 주목해 보자.
이렇게 하면 진행 중인 다운로드 작업 때문에, 종료된 액티비티의 ImageView가 가비지 콜렉션되는 것을
방해하지 않게 된다. onPostExecute에서 사용을 하기 전, weak reference와 imageView
모두가 not null임을(즉, 가비지 콜렉션되지 않았다는 것) 확인해야 하는 이유가 바로 이 점 때문이다.
 
간단하게 만든 이 예제는 AsyncTask를 사용하는 방법을 보여준다. 여러분이 시도를 해본다면,
이 몇 줄 안 되는 코드도 ListView의 성능을 크게 향상시켜 스크롤이 부드럽게 되는 것을 확인할 수 있을 것이다.
AsyncTasks에 대한 보다 상세한 정보를 얻으려면 Painless threading을 읽어보라.

그런데 현재 구현에서는 ListView와 관련된 동작에 문제가 있다. 메모리 효율을 위해서 ListView는
사용자가 스크롤 할 때 뷰를 재사용한다. 사용자가 리스트를 넘길 때, 하나의 ImageView 객체가
여러 번 사용될 것이다. ImageView는 디스플레이될 때마다 이미지 다운로드 태스크 하나를 정확하게
트리깅하고, 그 결과 이미지가 적절히 변경될 것이다. 그럼 무엇이 문제인가?
대부분의 병렬 애플리케이션들과 같이, 중요한 이슈는 ‘순서’에 있다.
우리 예제의 경우, 다운로드된 태스크들이 시작 순서대로 끝난다는 보장이 없다.
그 결과, 리스트에서 최종적으로 디스플레이되는 이미지가 이전 이미지의 것일 수 있다.
다운로드하는 시간이 오래 걸리기만 하면 이런 일이 얼마든지 발생될 수 있다.
다운로드된 이미지들을 각각 특정한 ImageView들에 영속적으로 바운드시킨다면 문제가 되지 않겠지만,
리스트에서 사용되는 이 흔한 케이스를 위해, 진짜 해결책을 마련해보자.
 
Handling concurrency(동시성 처리하기)
이 문제를 해결하기 위해 우리는 다운로드 순서를 기억해야 한다. 그래서 맨 끝에 다운로드를 시작한
이미지가 디스플레이되게 해야 한다. 각각의 ImageView가 자신의 마지막 다운로드를 기억하게 하면 충분하다.
우리는 전담 Drawable 서브클래스를 사용하여 이 추가 정보를 ImageView에 더할 것이다.
이 Drawable 서브클래스는 해당 이미지에 대한 다운로드가 진행되는 동안 일시적으로 ImageView에 바인드될 것이다.
DownloadedDrawable 클래스의 코드는 아래와 같다:
 
static class DownloadedDrawable extends ColorDrawable {
   
private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;

   
public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
       
super(Color.BLACK);
        bitmapDownloaderTaskReference
=
           
new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
   
}

   
public BitmapDownloaderTask getBitmapDownloaderTask() {
       
return bitmapDownloaderTaskReference.get();
   
}
}


구현 클래스 DownloadedDrawable는 ColorDrawable의 지원을 받아, 다운로딩이 진행되는 동안 ImageView가
검정색 백드라운드를 디스플레이하게 될 것이다. 이것 대신 “다운로드 진행 중”임을 알리는 이미지를 사용하면
사용자에게 작업 상황을 피드백할 수 있을 것이다. 여기서도 객체의 의존성을 줄이기 위해 WeakReference를
사용하고 있음을 주목하라.
 
이 새로운 클래스를 고려하여 우리의 코드를 수정해보자. 먼저, 다운로드 메쏘드는 이제 이 클래스의 인스턴스를
생성하여 imageView와 바인드시킨다.
 
public void download(String url, ImageView imageView) {
     
if (cancelPotentialDownload(url, imageView)) {
         
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
         
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
         imageView
.setImageDrawable(downloadedDrawable);
         task
.execute(url, cookie);
     
}
}


cancelPotentialDownload 메쏘드는 이 imageView에 다운로드를 진행하고 있을지도 모르는 잠재적 작업을
중단시킨다. 곧 새로운 이미지를 위한 다운로딩을 시작할 것이기 때문이다. 그러나 이것만으로는 항상
최종적으로 다운로드하는 이미지를 디스플레이 하도록 보장할 수는 없다. 만약 그 태스크가 끝이 나
onPostExecute 메쏘드에서 기다리고 있다가, 새로 다운로드한 것 이후에 실행될 수도 있기 때문이다. 

private static boolean cancelPotentialDownload(String url, ImageView imageView) {
   
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);

   
if (bitmapDownloaderTask != null) {
       
String bitmapUrl = bitmapDownloaderTask.url;
       
if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
            bitmapDownloaderTask
.cancel(true);
       
} else {
           
// The same URL is already being downloaded.
           
return false;
       
}
   
}
   
return true;
}


cancelPotentialDownload는 AsyncTask 클래스의 cancel 메써드를 사용하여 진행 중인 다운로드를 멈추게 한다.
이 메쏘드는 대부분의 경우 true를 리턴하여 그 다운로드를 시작할 수 있게 한다. 유일한 예외 상황은
동일한 URL로부터 이미 다운로드가 진행되고 있는 경우로서, 이 때에는 취소시키는 대신 다운로드를 계속할 수
있게 해야 한다. 이 구현의 경우는 ImageView 하나가 가비지 콜렉션되었다면, 그와 연관된 다운로드는
중단되지 않는다는 점을 주목하자. 그러한 용도를 위해서는 RecyclerListener가 사용될 수 있다.
 
이 메써드는 헬퍼인 getBitmapDownloaderTask 메쏘드를 사용한다. 이 메쏘드는 다음과 같이 매우 직관적이다:
 
private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
   
if (imageView != null) {
       
Drawable drawable = imageView.getDrawable();
       
if (drawable instanceof DownloadedDrawable) {
           
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
           
return downloadedDrawable.getBitmapDownloaderTask();
       
}
   
}
   
return null;
}

 
마지막으로, onPostExecute를 변경하여 이 ImageView가 아직 이 다운로드 프로세스에 연결되어 있을 때에만
Bitmap을 바인드하도록 만들어야 한다.
 
if (imageViewReference != null) {
   
ImageView imageView = imageViewReference.get();
   
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
   
// Change bitmap only if this process is still associated with it
   
if (this == bitmapDownloaderTask) {
        imageView
.setImageBitmap(bitmap);
   
}
}


이렇게 수정하여 이제 ImageDownloader 클래스는 우리가 기대하는 기본적인 서비스를 수행할 수 있다.
여기에서 다룬 예제, 혹은 이를 통해 설명한 비동기식 패턴을 자유롭게 사용하여 여러분의 애플리케이션들의
응답성을 높일 수 있기를 바란다.
 
Demo(데모)  
이 글의 소스 코드는 Google Code(http://code.google.com/p/android-imagedownloader/)에서 얻을 수 있다.
여러분은 이 글에서 설명한 세 가지 상이한 구현들(비동기 태스크를 쓰지 않는 경우,
태스크에 비트맵이 바인딩되지 않은 경우, 그리고 최종 수정 버전)을 비교하고 변환해 보아도 좋다.
여기서 다룬 문제를 좀더 잘 보여주기 위해 캐쉬 크기를 10개의 이미지로 제한하였다.

 
 
Future work(향후 과제)

이 코드는 병행성 측면에 초점을 맞추고자 간소화된 것이며 많은 유용한 기능들이 빠져 있다.
우선, ImageDownloader 클래스에 캐쉬를 사용하면 유익을 얻을 것이 분명하다. 특히 ListView와 함께
사용될 때는 더욱 그러하다. 사용자가 앞뒤로 스크롤할 때, 동일한 이미지를 여러 번 디스플레이할 가능성이
높기 때문이다. 이는 URL을 Bitmap SoftReferences에 맵핑하는 LinkedHashMap의 지원을 받는
LRU(Least Recently Used, LRU) 캐쉬를 사용하여 쉽게 구현될 수 있다. 좀 더 복잡한 캐쉬 메커니즘을
만드는 것은 이미지의 로컬 디스크 저장소에 영향을 받는다. 필요하다면 썸네일 생성과 이미지 리사이징
기능도 더할 수 있다.

우리가 구현한 코드로도 다운로드 오류와 타임아웃을 정확히 처리할 수 있다. 이런 경우 null Bitmap이
리턴될 것이다. 원하는 경우 오류가 난 이미지를 디스플레이할 수도 있다.

이 글에서 다룬 HTTP request는 매우 간단한 것이다. 개발자들 중에는 특정 웹사이트의 요청에 따라
request에 파라미터나 쿠키를 더하고자 할 수도 있다.
 
이 글에서 사용된 AsyncTask 클래스는 매우 편리하며, 시간이 걸리는 작업들을 UI 쓰레드에서 우회시킬 수 있다.
보다 정교하게 작업을 컨트롤하기 위해 Handler 클래스 사용을 원할 수도 있다.
예를 들어, 이 예제의 경우, 병렬로 실행되고 있는 다운로드 쓰레드의 총 개수 등을 컨트롤하는 기능을 더할 수 있다.


 
 

이름아이콘 인베인
2010-08-06 08:22
의미있는 내용의 예제 감사합니다. 송경희님 짱입니다. ㅎㅎ OS에서의 응답성 설비 향상을 요청하기 전에 API프로그래머가 성능을 향상시키기 위한 엔지니어적 고찰이 너무 감동적이옵니다.
인베인 제목을 "애플리케이션 응답성 향상을 위한 멀티쓰레딩 기법"도 괜찮아보이네요.. 역시나 의역은 정말 난해해요..^^ 8/6 08:40
   
이름아이콘 likemiller
2010-08-06 09:35
감사합니다.. 이거 읽어보고 싶었는데 영어가 안되서 대충 봤었는데.. 이제 읽을 수 있겠네요~ㅎㅎ
   
이름아이콘 무림하수
2012-01-10 13:02
좋은 글 감사합니다~~!
칸드로이드에 잘 하시는 분들이 너무 많으셔서 행복하네요~~!
   
 
덧글 쓰기 0
3500
※ 회원등급 레벨 0 이상 읽기가 가능한 게시판입니다.