2010. 7. 24. 05:18

[번역] 성능을 위한 멀티쓰레딩 (Multithreading For Performance)



성능을 위한 멀티쓰레딩 (Multithreading For Performance)

 

http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed:+blogspot/hsDu+(Android+Developers+Blog)

 

번역: SSKK ( http://codemuri.tistory.com/ )

 

2010 7 19일 오전 11 41 Tim Bray 가 포스팅함

 

이 포스트는 멀티태스킹에 심취한 안드로이드 그룹의 엔지니어인 Gilles Debunne 에 의해 작성됨 – Tim Bray

 

응답성이 높은 애플리케이션을 만들기 위한 좋은 방법은 메인 UI 쓰레드는 최소한의 일만 하도록 하는 것이다. 애플리케이션을 중지시킬(hang) 수 있는 긴 작업은 다른 쓰레드에서 다루어져야 한다. 그러한 작업의 전형적인 예는 어느 정도 지연될 지 예측할 수 없는 네트워크 작업이다. 사용자는 무엇인가가 진행 중에 있다는 피드백을 제공하는 일부 멈춤에 대해서는 관대할 것이다. 그러나 냉동된 애플리케이션(frozen application)은 사용자에게 어떤 실마리도 주지 않는다.

 

이 문서에서는, 이러한 패턴을 설명하는 간단한 Image Downloader 를 만들 것이다. 인터넷으로부터 다운로드된 썸네일 이미지를 가지는 ListView를 생성할 것이다. 백그라운드에서 다운로드를 수행하는 비동기 태스크를 생성하는 것은 애플리케이션이 빠르게 동작하는 것을 유지해 줄 것이다.

 

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;
}

 

하나의 client HTTP request 가 생성된다. Request 가 성공하면, 이미지를 포함하고 있는 response 엔티티 스트림은 비트맵을 생성하기 위해 디코드(decode) 된다. 애플리케이션의 메니페스트는 이를 가능하게 하기 위해 INTERNET 에 대한 접근 권한을 명시해야 한다.

 

Note: BitmapFactory.decodeStream 의 이전 버전에 존재하는 버그 때문에 느린 연결 상태(over a slow connection)에서는 이 코드가 동작하지 않는다. 이 문제를 해결하기 위해서는 새로운 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;
   
}
}

 

이것은 만일 파일의 끝에 도달하지 않는 경우, skip() 함수가 제공된 바이트 수만큼 스킵하는 것을 보장한다.

 

ListAdapter getView 메소드 안에서 이 메소드를 직접 사용했다면, 스크롤은 불쾌하게 들쭉날쭉 할 것이다. 새로운 뷰의 각 화면은 이미지가 다운로드 되길 기다려야 하고, 이는 부드러운 스크롤을 방해할 것이다.

 

AdnroidHttpClient 자신이 메인 쓰레드에서 시작되는 것을 허용하지 않는 것은 아주 나쁜 생각이다. 대신 위의 코드는 “This thread forbids HTTP requests” 에러 메시지를 보여줄 것이다. 만약 정말로 당신의 발에 총을 쏘길 원한다면 (메인 쓰레드에서 HttpClient 를 시작하려고 한다면..) DefaultHttpClient 를 사용해라.

 

비동기 태스크 소개

 

AsyncTask 클래스는 UI 쓰레드에서 새로운 태스크를 시작하는 가장 간단한 방법중의 하나이다. 이러한 태스크 생성을 담당할 ImageDownloader 클래스를 생성하자. 이 클래스는 URL 로부터 다운로드 된 이미지를 ImageView 에 할당할 download 메소드를 제공할 것이다.

 

public class ImageDownloader {

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

   
/* class BitmapDownloaderTask, see below */
}

 

BitmapDownloaderTask 는 실제로 이미지를 다운로드 할 AysncTask 이다. 이 클래스는 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(다운로드 된 Bitmap) 을 받고 이 bitmap download 메소드에 의해 제공되어 BitmapDownloaderTask 에 저장된 ImageView 와 연결된다. ImageView WeakReference 에 저장된 다는 것을 명심하라. 이것은 다운로드가 진행 중일 때 종료된 액티비티의 ImageView 가 쓰레기 수거(garbage collected) 되는 것을 막지 않는다. (다운로드가 진행 중일 때 ImageView 가 쓰레기 수거되어 null 이 될 수 있다는 말) 원래 WeakReference 가 아니면 참조된 클래스는 쓰레기 수거되지 않는다. SoftReference 도 있는 데 이것은 Out of memory 상황일 때에 쓰레기 수거의 대상이 된다. 이것이 바로 onPostExecute 에서 imageViewReference imageView 를 사용하기 전에 weak reference imageView 둘 다 null 이 아닌지(쓰레기 수거되지 않았는지)를 체크해야만 하는 이유이다.

 

이 간단한 예제는 AsyncTask 의 사용법을 설명한다. 이를 테스트 해보면, 이 몇 줄 안되는 코드가 ListView 의 성능을 비약적으로 개선하는 것을 보게 될 것이고 이제 부드럽게 스크롤이 가능한 것 또한 볼 수 있다. AysncTask 에 대한 좀 더 자세한 설명은 Painless threading 을 읽어보기 바란다.

 

하지만, ListView 의 특정 동작으로 인해 현재 구현에 한가지 대해 한가지 문제가 있다. 메모리 효율의 이유로 이해 ListView 는 사용자가 스크롤 할 때 표시되는 view 들을 재사용한다. 리스트가 스크롤 될 때, 주어진 ImageView 객체는 여러 번 사용될 것이다. ImageView 가 올바르게 표시될 때마다 image download task 가 생성되고 결국 ImageView 의 이미지가 변경될 것이다. 문제가 어디에 있을까? 대부분의 병렬 애플리케이션에 있어서, 핵심 이슈는 정렬(ordering)에 있다. 우리의 경우에는, download 태스크가 시작된 순서대로 종료될 것이라는 보장이 어디에도 없다. 그 결과로 리스트에 최종적으로 표시되는 이미지는 이전의 아이템이 될 지도 모르고 이는 다운로드가 오래 걸리는 경우에 쉽게 일ㄹ어날 수 있다. 만약 다운로드 된 이미지가 단 한번 연결된다면 이것은 이슈가 아니다. 하지만 리스트에 사용되는 일반적인 경우를 위해 이것을 고쳐보자.

 

병행성 다루기 (Handling conccurrency)

 

이 이슈를 해결하기 위해서는, 최근에 시작된 것이 효과적으로 화면에 표시되도록 하기 위해 다운로드의 순서를 기억해야 한다. ImageView 가 자신의 최근 download 를 기억하는 것으로 충분하다. Download 가 진행 중인 동안 일시적으로 ImageView 와 결합되는 전용 Drawable subclass 를 사용하여 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();
   
}
}

 

이 구현은 ColorDrawable 의 도움을 받는다. 이 클래스는 다운로드가 진행중인 동안 검은색 배경으로 된 ImageView 를 보이게 할 것이다. 사용자에게 피드백을 제공하려면 대신에 “download in progress” 이미지를 사용할 수 있다. 다시 한번, 객체 의존성을 제한하기 위해 WeakReference 를 사용하는 것을 명심하라.

 

새로운 클래스를 적용하기 위해 코드를 변경하자. Download 메소드는 이 클래스에 대한 인스턴스를 생성하고 그것을 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 메소드를 사용한다. cancelPotienaltialDownload 메소드는 대부분의 경우에 true 를 반환함으로써 다운로드가 시작될 수 있도록 한다. 이것이 일어나지 않는 단 한가지 경우는 동일한 URL 에 대해서 이미 다운로드가 진행중인 경우 이를 지속하도록 하기 위할 때이다. 이 구현에서는, ImageView 가 쓰레기 수거된 경우 그 뷰와 결합된 다운로드가 멈추지 않는다는 것을 명심해라 이것을 멈추게 하기 위해서는 RecyclerListener 가 사용될 수 있다.

 

cancelPotientialDownload 메소드는 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;
}

 

마지막으로, ImageView 가 다운로드와 결합되어 있는 경우에만 Bitmap 과 결합되도록 하기 위해 onPostExecute 가 수정되어야 한다.

 

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;
}

 

이러한 수정들로 인해, ImageDownloader 클래스는 우리가 기대하는 기본적인 서비스를 제공한다. 응답성이 보장하기 위해 당신의 애플리케이션에 자유롭게 이것을 이용하거나 이 글이 설명하는 비동기 패턴을 이용하라.

 

데모

 

이 문서의 소스 코드는 Google Code 에서 다운 받을 수 있다. 이 문서에서 소개된 세가지 다른 구현(No task, no bitmap to task association, 그리고 최종 버전) 사이에서 전환하면서 비교해 볼 수 있다., 이 이슈를 보다 잘 설명하기 위해 캐쉬 사이즈는 10개의 이미지로 한정되었다는 것을 명심하라.

 


 

Future Work

 

이 코드는 병행 수행 측면에 초점을 두어 단순화 되었지만 많은 유용한 기능들이 구현에서 누락되어 있다. ImageDownloader 클래스는 먼저 캐쉬를 이용할 것이다. 특히 ListView 와 함께 사용된 경우, 사용자가 이리저리 스크롤 할 때마다 동일한 이미지를 여러 번 보여줄 수 있을 것이다. 이것은 Bitmap SoftReference 에 대한 URL LinkedHashMap 을 이용한 LRU(Least Recently Used) 캐쉬를 이용하여 쉽게 구현될 수 있다. This can be implemented using a Least Recently Used cache backed by a LinkedHashMap of URL to Bitmap SoftReferences. 더 복잡한 캐쉬 매커니즘(more involved cache mechanism)은 로컬 디스크에 저장된 이미지에 의존할 수도 있다. 필요하다면 썸네일 생성과 이미지 크기 조정 또한 추가될 수 있다.

 

다운로드 에러와 타임아웃은 모두 null bitmap 을 반환할 것이기 때문에 이 구현에서 올바르게 처리된다. 어떤이는 그대신 에러 메시지를 보여주고 싶어할 수 있다.

 

우리의 HTTP request 는 매우 단순하다. 어떤이는 특정 웹사이트에서 요구되는 parameter 와 쿠키를 추가하고 싶어할 수 있다.

 

이 문서에서 사용된 AsyncTask 클래스는 UI 쓰레드와 특정 작업을 구분지을 수 있는 정말 편리하고 쉬운 방법이다. 어떤이는 병렬로 수행중인 다운로드 쓰레드의 전체 개수를 제어하는 것과 같이 원하는 어떤 더 좋은 제어를 추가하기 위해 Handler 클래스를 사용하길 원할지도 모른다.

 

.