woong's

Multithreading For Performance (성능 향상을 위한 멀티쓰레딩 기법) 본문

Develop/Android

Multithreading For Performance (성능 향상을 위한 멀티쓰레딩 기법)

dlsdnd345 2016. 2. 13. 23:35
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
Posted by Tim Bray on 19 July 2010 at 11:41 AM 
 
[멀티태스크 상황에 처하는 것을 기꺼워하는 안드로이드 그룹 엔지니어 
Gilles Debunne이 쓴 글. - Tim Bray]

응답성 있는 애플리케이션을 만드는 좋은 사례는 여러분의 메인 UI 쓰레드에게 최소한의 작업만을 
시키는 것이다. 시간이 오래 걸릴 가능성이 있어서 애플리케이션에 장애가 될만한 작업은 별도의 
쓰레드를 통해 처리해야 한다. 그러한 작업의 전형적인 예로는 예측하기 어려운 지연 현상들이 
수반하는 네트워크 오퍼레이션을 들 수 있다. 사용자들은 일시적인 멈춤 정도라면 참아줄 수 있을 것이다. 
특히, 뭔가를 진행하고 있다고 피드백을 제공해주면 인내심을 발휘할 수 있다. 
그러나 아무 공지도 없이 멍 때리는 애플리케이션이라면 사용자들 입장에서 무슨 일이 벌어지는지, 
얼마나 기다리면 되는 것인지 도저히 감을 잡을 수가 없을 것이다.  
 
오래 걸리는 작업을 별도의 쓰레드로 처리하는 패턴을 설명하기 위해, 이 글에서는 간단한 이미지 
다운로더를 만들 것이다. 우리는 인터넷에서 다운로드받은 썸네일 이미지를 보여줄 ListView를 사용할 것이다. 
백그라운드에서 다운로드 받는 비동기적인 작업을 만들면 애플리케이션이 좋은 속도를 유지하게 할 수 있다. 

An Image downloader(이미지 다운로더)
 
프레임워크에 의해 제공되는 HTTP 관련 클래스들을 사용하여 웹에서 이미지를 다운로드 하는 것은 
아주 간단하다. 예를 들어, 아래와 같은 구현이 있을 수 있다 : 

staticBitmap downloadBitmap(String url){
finalAndroidHttpClient client=AndroidHttpClient.newInstance("Android");
finalHttpGet getRequest=newHttpGet(url);

try{
HttpResponse response= client.execute(getRequest);
finalint statusCode= response.getStatusLine().getStatusCode();
if(statusCode!=HttpStatus.SC_OK){ 
Log.w("ImageDownloader","Error "+ statusCode+" while retrieving bitmap from "+ url); 
returnnull;
}
        
finalHttpEntity entity= response.getEntity();
if(entity!=null){
InputStream inputStream=null;
try{
                inputStream
= entity.getContent(); 
finalBitmap 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();
}
}
returnnull;
}

 

 

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

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

staticclassFlushedInputStreamextendsFilterInputStream{
publicFlushedInputStream(InputStream inputStream){
super(inputStream);
}

@Override
publiclong skip(long n)throwsIOException{
long totalBytesSkipped=0L;
while(totalBytesSkipped< n){
long bytesSkipped=in.skip(n- totalBytesSkipped);
if(bytesSkipped==0L){
intbyte= 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 하나에 할당하는 다운로드 메쏘드를 제공한다. 

publicclassImageDownloader{

publicvoid download(String url,ImageView imageView){
BitmapDownloaderTask task=newBitmapDownloaderTask(imageView);
            task
.execute(url);
}
}

/* class BitmapDownloaderTask, see below */
}

 

 


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

classBitmapDownloaderTaskextendsAsyncTask<String,Void,Bitmap>{
privateString url;
privatefinalWeakReference<ImageView> imageViewReference;

publicBitmapDownloaderTask(ImageView imageView){
        imageViewReference
=newWeakReference<ImageView>(imageView);
}

@Override
// Actual download method, run in the task thread
protectedBitmap 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
protectedvoid 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 클래스의 코드는 아래와 같다: 
 

staticclassDownloadedDrawableextendsColorDrawable{
privatefinalWeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;

publicDownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask){
super(Color.BLACK);
        bitmapDownloaderTaskReference
=
newWeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
}

publicBitmapDownloaderTask getBitmapDownloaderTask(){
return bitmapDownloaderTaskReference.get();
}
}


 

 

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

publicvoid download(String url,ImageView imageView){
if(cancelPotentialDownload(url, imageView)){
BitmapDownloaderTask task=newBitmapDownloaderTask(imageView);
DownloadedDrawable downloadedDrawable=newDownloadedDrawable(task);
         imageView
.setImageDrawable(downloadedDrawable);
         task
.execute(url, cookie);
}
}

 

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

 

 

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

privatestaticBitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView){
if(imageView!=null){
Drawable drawable= imageView.getDrawable();
if(drawableinstanceofDownloadedDrawable){
DownloadedDrawable downloadedDrawable=(DownloadedDrawable)drawable;
return downloadedDrawable.getBitmapDownloaderTask();
}
}
returnnull;
}

 

 

마지막으로, 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개의 이미지로 제한하였다. 


Comments