사용자 인터페이스(UI) 쓰레드 밖에서 비트맵 가공하기
'커다란 비트맵 효율적으로 불러오기'에서 논의한 BitmapFactory.decode )메소드들은 보조기억장치나 네트워크에서(메모리를 제외한 모든 소스에서) 데이터를 읽어올 경우 메인 유저 인터페이스( UI) 쓰레드에서 실행되어서는 안됩니다. 이미지 불러오기에 걸리는 시간은 예상하기 어려우며, 다양한 요소들(보조기억장치에서 데이터를 읽어오는 속도, 네트워크 속도, 이미지 크기, CPU성능 등)에 영향을 받습니다. 만약 UI 쓰레드의 작업을 이들 중 하나가 막는다면, 시스템은 당신의 애플리케이션이 반응하지 않는다고 알립니다. 그리고 사용자는 애플리케이션을 닫는 선택을 할 수 있습니다(상세 사항은 반응성에 관한 디자인(Designing for Responsiveness)을 확인하십시오. )
반응성에 관한 디자인은 '성능을 위한 최고의 실천법'의 '애플리케이션의 반응성 유지하기' 강의입니다. '애플리케이션에 반응이 없습니다' 'Application Not Responding' (ANR)다이얼로그에 관한 내용입니다.
이 강좌를 통해 비트맵을 백그라운드 쓰레드(Background thread)에서 비동기(AsyncTask)를 통해 처리하는 방법과 동시성(concurrency)문제들을 다루는 방법을 배울 수 있습니다.
비동기 사용하기
비동기(AsyncTask)클래스는 백그라운드 쓰레드에서 일을 처리하고 결과값을 메인 UI 쓰레드에 전달하는 쉬운 방법을 제공합니다. 이를 사용하기 위해서는 상속클래스(subclass)를 만들고 제공되는 메소드를 보수(Override)해야합니다. 아래 큰 크기의 이미지를 이미지 뷰에 비동기로 불러오는 소스코드입니다. decodeSampledBitmapFromResource()는 '커다란 비트맵 불러오기'에서 작성한 메소드입니다.
//비동기를 사용하기 위해 상속클래스를 만듭니다. 자바의 제네릭<>을 사용합니다
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// 약한 참조(weakReference)를 사용하여 가비지 콜렉터가 이미지뷰를 처리할 수 있도록 합니다.
imageViewReference = new WeakReference<ImageView>(imageView);
}
//백그라운드 쓰레드에서 디코딩하기
//부모클래스인 AsyncTask의 doInBackground를 Override합니다.
//Integer... params를 통해 하나 이상의 정수값이 인수로 들어올 수 있도록 합니다.
@Override
protected Bitmap doInBackground(Integer... params) {
//data는 비트맵의 리소스 값(ex. R.mipmap.xxx)
data = params[0];
//앞서 '커다란 비트맵 불러오기'에서 작성한 메소드를 통해 100X100의 썸내일을 디코딩합니다.
return decodeSampledBitmapFromResource(getResources(), data, 100, 100);
}
// onPostExecute를 overrride하여 완성시 비트맵을 이미지뷰에 넣습니다.
@Override
protected void onPostExecute(Bitmap bitmap) {
// 약한 참조(WeakRefrence)를 사용했으므로 null값인지를 확인하며, 방어코딩을 합니다
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
약한 참조(WeakReference)를 사용하여 이미지 뷰(ImageView)가 가비지 콜렉터에게 처리되는 것을 막는 경우를 방지합니다. 이미지 불러오기가 완료될 때 여전히 메모리에 존재할 지에 대해 확신이 없으므로, onPostExecute())에서 확인하는 작업을 합니다. 예를 들어 사용자가 액티비티에서 나가거나 또는 작업을 완료하기 전에 구성에 변화가 생긴다면 해당 이미지 뷰가 더이상 존재하지 않을 수 있습니다.
새 비동기 작업을 시행하는 소스코드는 다음과 같습니다.
public void loadBitmap(int resId, ImageView imageView) {
//상속하여 만든 비동기 작업의 인스턴스를 만듭니다.
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
//인스턴스를 실행합니다.
task.execute(resId);
}
동시성(concurrency) 다루기
순차성이 떨어지는 듯하여 완성된 소스코드를 아래 작성해 두었습니다. 미리 살펴보신 후 진행하셔도 좋을 거라 생각됩니다.
앞서서 설명되었듯, ListView, RecyerView, GridView 같은 뷰 요소들은 비동기와 결합할 때 또다른 문제를 만들어 냅니다. 메모리의 효율적 사용을 위해, 이러한 요소들은 사용자가 스크롤링할 때 하위 뷰들을 재사용합니다. 만약 각각의 하위 뷰들이 비동기 작업을 시행한다면, 언제 작업이 끝날지에 관한 보장이 없으며, 연관된 뷰가 다른 하위뷰 사용에 재활용되지 않습니다. 더욱이, 하위 뷰들의 순서와 하위 뷰들의 비동기 작업의 순서와 다를 수 있습니다.
성능을 위한 다중쓰레드(MultiThreading for Performance)란 블로그 글에서 더 깊은 동시성에 관한 논의가 진행됩니다. 그리고 가장 최근에 완료된 비동기 작업을 확인하는 참조를 이미지뷰 어디에 저장할 지에 관한 해결법이 제공됩니다. 유사한 방법을 통해, 그 비동기작업을 아래와같은 패턴으로 처리할 수 있습니다.
후 참조를 위해 Drawable의 하위 클래스를 만듭니다. 이 경우 작업이 완료되는 동안 이미지 뷰에 표시 될 수 있는 BitmapDrawable이 사용됩니다.
//BitmapDrwable을 상속한 클래스를 만듭니다.
static class AsyncDrawable extends BitmapDrawable {
//약한 참조를 형성하여 후 사용할 참조를 만듭니다.
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
//생성자에서 비트맵 생성을 위한 비동기 작업을 인수로 받습니다.
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
//비트맵을 만들 비동기 작업에 약한 참조를 연결합니다.
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
//참조를 전달하는 메소드입니다.
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
비동기 작업을 하기전에 앞서 만든 AsyncDrawable의 인스턴스를 생성해 이미지뷰에 연결합니다.
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
mPlaceHolderBitmap은 화면 밀도와 화면 수치를 타겟으로 한 비트맵으로서 이를 고려한 Drawable 객체를 만들기 위해 사용됩니다.
위에 사용된 cancelPotentialWork() 메소드는 다른 작업이 해당 이미지 뷰와 연결되어있는 지를 확인하기 위한 것입니다. 그럴경우 앞선 작업을 취소(cancel()))합니다. 몇 안되는 경우에 새로운 작업이 앞서 진행되는 작업과 일치할 수 있습니다 이 경우 후발 작업을 진행할 필요가 없습니다. 아래의 소스코드는 cancelPotentialWork()의 소스코드입니다.
//getBitmapWorkerTask는 해당 이미지 뷰와 연결된 작업을 검색하는 매소드 입니다. 밑에 해당 소스코드가 있습니다.
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
//앞서서 진행되는 작업이 없는 경우 과정을 진행하지 않습니다.
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// 만약 새로운 작업과 앞 선 작업이 다를 경우, 또는 그 작업의 결과물이 적용되지 않았다면
if (bitmapData == 0 || bitmapData != data) {
// 이전 작업을 취소합니다.
bitmapWorkerTask.cancel(true);
} else {
// 앞의 작업과 동일하다면 새로운 작업을 시행하지 않습니다.
return false;
}
}
// 이미지뷰와 연결된 작업이 없거나 앞선 작업이 취소되었다면 후발 작업을 시행합니다.
return true;
}
특정 이미지 뷰와 연결된 작업을 얻기위한 헬퍼 메소드 geBitmapWorkerTask()입니다
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
//이미지 뷰의 Drawable이 비동기로 형성되는 경우, AsyncDrawable의 인스턴스임을 확인합니다.
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
//위 AsyncDrawble을 만들 때 작성한 참조를 얻는 메소드를 사용하여 해당 비동기 작업의 참조를 얻습니다.
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
마지막작업으로 작업이 취소되지 않았을 때에만 UI에 결과물을 반여하기 위해 onPostExecute()에서 취소여부를 확인합니다.
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
@Override
protected void onPostExecute(Bitmap bitmap) {
//취소 여부를 확인합니다. 취소될 경우 작업이 완료되어도 null값을 리턴합니다.
if (isCancelled()) {
bitmap = null;
}
//참조하던 이미지 뷰가 특정 이유에 의해 가비지 콜렉터에 의해 처리되거나, 취소되는 경우를 제외하여 작업합니다.
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);
//이미지 뷰에 연결된 작업이 현재의 작업과 동일할 경우 이미지 뷰에 결과물을 반영합니다.
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
이제 하위 뷰를 재활용하는 뷰들에 적합한 형태로 적용되었습니다. loadBitmap과 같이 간단한 함수로 이미지를 적용할 수 있습니다. GridView의 경우 예를 들면 어댑터의 getViews())에서 해당 매소드를 사용합니다.
설명에 있어 순차성이 떨어지는 듯 하여 하나의 완성된 소스코드를 만들어봤습니다.
public class BackroundThreadBitmapLoader {
public void loadBitmap(Resources res, int resId, ImageView imageView,Bitmap placeHolder, int width, int height) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView, res, width, height);
final AsyncDrawable asyncDrawable = new AsyncDrawable(res, placeHolder, task);
//초기 이미지 뷰에 임시로 placeHolder를 적용합니다.
imageView.setImageDrawable(asyncDrawable);
//비동기작업을 시행하며 완료시 임시로 적용한 이미지 대신에 새롭게 형성된 파일을 적용하게 됩니다.
task.execute(resId);
}
}
//이미지를 적용하려는 이미지 뷰에 앞서서 연결된 다른 비동기 작업이 있는지 확인하는 메소드입니다.
private boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData == 0 || bitmapData != data) {
bitmapWorkerTask.cancel(true);
} else {
return false;
}
}
return true;
}
// 이미지 뷰와 연결된 비동기 작업의 참조를 얻기 위한 매소드입니다.
private BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
//비동기적으로 비트맵을 Drawable 객체를
private class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
//백그라운드 UI 쓰레드에서 비트맵을 형성하는 비동기 작업입니다. 완료시 연결된 이미지 뷰에 이미지를 표시합니다.
private class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
private Resources res;
int width, height;
public BitmapWorkerTask(ImageView imageView, Resources res, int width, int height) {
imageViewReference = new WeakReference<ImageView>(imageView);
this.res =res;
this.width = width;
this.height = height;
}
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeBitMap(res, data, width, height);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
//앞서서 '커다란 비트맵 불러오기'에서 작성한 코드입니다.
private Bitmap decodeBitMap(Resources res, int resId, int reqWidth, int reqHeight){
Bitmap result = null;
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res,resId,opts);
opts.inSampleSize = calInSampleSize(opts,reqWidth,reqHeight);
opts.inJustDecodeBounds = false;
result = BitmapFactory.decodeResource(res,resId,opts);
return result;
}
private int calInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight){
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
while ((height / inSampleSize) >= reqHeight
&& (width / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
}