티스토리 툴바


'IT /Android 개발'에 해당되는 글 1건

  1. 2011/07/13 GHGallery 부분 커스터마이징을 통해 보는 getView메소드 최적화
IT /Android 개발2011/07/13 11:17



GHGallery : 원문 보기
 

안녕하십니까? 화박입니다.

이번 글에서는 GHGallery 소스를 약간 수정하면서 체험적 지혜를 나눠볼까 합니다. 

아래는
이 글을 쓰며 참고한 사이트들입니다. 

GsBoB 님의 블로그 글
Android-GridView를-사용할-때-getView에-대한-이해 

Android 레퍼런스 사이트 
GridView

StackOverflow 

Android: Strange out of memory issue



문제.
GHGallery의 코드를 수정하기로 결정하고 대충 갖다 붙혀넣었을때 생기는 문제는 크게 두 가지였다. 이미지가 스크롤의 이동에 따라 갱신이 되고 있었으므로, 3G에서는 도저히 사용못할 오버해드가 되고 있었고 테스트를 위해 불러올 이미지 사이트를 늘였을때는 VM won't let us allocate  라는 메시지를 내뿜으며 뻗어버리는 것이다.


이를 수정하기 위해 두 부분의 코드를 살펴봐야 했다.
첫 째는, 이미지 갱신을 위한 코드인 Adapter의 getView 메소드.
둘 째는, imageView개체에 Bitmap을 넣는 메소드. 


문제파악

첫 번째 문제
첫 번째 문제부터 살펴보면, GHGallery 코드의 getView메소드는 다음과 같이 구현되어 있었다. 

 
public View getView(int position, View view, ViewGroup parent) {

if (view == null) {
view = new ImageView(parent.getContext());
view.setLayoutParams(new GridView.LayoutParams(95, 95));
view.setPadding(2, 2, 2, 2);
}

imageDownloader.download(url[position], (ImageView)view);

return view;
}
  

상기 소스의 의미는 짧게 풀어보면 "갱신하고자 하는 position의 view가 null이면 새 imageView를 넣고, 아닐 경우 그냥 imageDownloader의 download메소드를 호출해 정해진 position에 맞는 url이미지를 view에 넣어준다." 가 되겠다.

이 경우 문제가 되는 이유는, 간단하다. GridView의 경우 화면에 표시되는 position들의 경우 갱신이 없지만, 스크롤을 이동하면서 계속해서 이동하는 position에 맞는 갱신을 하고자 하기 때문이다. 

 0  1  2
 3  4  5
 6  7  8

위와 같은 GridView가 스크롤이 내려간다면 어떻게 될까? 


 0  1  2
 3  4  5
 6  7  8
 9  10  11
 12  13  14

스크롤을 내려 화면에 보이는 imageView 가 3~11이라고 가정했을때, 
9~11까지는 새로 갱신되며, 0~2와 12~14는 차후 갱신 대상이 되는 것이다. 



로컬 작업이면 전혀 문제될 일이 아니겠지만, 만약 이대로 코딩을 하면 프로그램을 쓰는 사람이 와이파이를 찾아 발로 뛰게 될 것 같다는 생각이 든다. 이미지 갱신은 막을 수 없더라도, 최대한 download메소드 호출은 막아야 한다. 


두 번째 문제
vm won't let us allocate... 안드로이드와 같은 운영체제는 임베디드 OS이다. 데스크탑 PC에 비해 조그마한 자원을 가지고 나눠써야 하는 것이다. 그래서 한 프로그램이 너무 많은 메모리를 소유하지 못하도록 할당 가능한 메모리 량에 제한을 두는데 이 제한을 넘어서면 프로그램은 바로 강제종료된다. 그러면서 Log에 남기는 메시지가 바로 vm won't let us allocate ( vm이 할당을 원치 않아요!) 이다. 어쨌든 이 메시지가 나는 이유는 자명한데, 한 GridView에 많은 이미지를 로드했기 때문일 것이다. 
하지만 나는 이 이미지 수를 유지하고 싶다. 어떻게 해야할까? 방법을 찾아보기로 했다. 일단 GridView의 각 View에 ImageView를 넣어주는 부분은 위의 getView메소드에 나와있고, 그렇다면 imageView에 무엇을 채우는지 살펴봐야겠다. 
 


Bitmap bitmap = downloadBitmap(url);
addBitmapToCache(url, bitmap);
imageView.setImageBitmap(bitmap);

downloadBitmap {
 ...
InputStream inputStream = null;
inputStream = entity.getContent();
try {
// Bug on slow connections, fixed in future release.
// return BitmapFactory.decodeStream(inputStream);
return BitmapFactory.decodeStream(new FlushedInputStream(inputStream));
} finally {
if (inputStream != null) {
inputStream.close();
}
entity.consumeContent();
}
}
 ...
 

이 코드는 BitmapFactory를 사용해 다운받은 컨텐트를 Bitmap화 하고 그 Bitmap을 imageView에 넣어줌을 알 수 있다. GridView에서 보여주는 imageView가 40개 정도 된다고 했을때, 꽤나 큰 용량이다. 테스트 시에는 30정도를 로드하고 바로 뻗어버렸다. Bitmap은 기본적으로 Full-size이미지를 의미한다. 

하지만 GridView에 표시되는 해상도라고 해봤자 96x96이다. 어떻게 이미지 사이즈를 줄일 수 없을까?


 
문제 해결 방안
 
첫 번째 문제를 해결하기 위해 가장 먼저 소개하고 싶은 것은 Google의 레퍼런스 코드이다.  일부를 발췌해서 보자. 

 // create a new ImageView for each item referenced by the Adapter
public View getView(int position, View convertView, ViewGroup parent)
 
{
     
ImageView imageView;
     
if (convertView == null) {  
      imageView
= new ImageView(mContext);
       imageView
.setLayoutParams(new GridView.LayoutParams(85, 85));
      imageView
.setScaleType(ImageView.ScaleType.CENTER_CROP);
       imageView
.setPadding(8, 8, 8, 8);
   
} else {
      imageView
= (ImageView) convertView;
   
}
    imageView
.setImageResource(mThumbIds[position]);
   
return imageView;
}
 
 
레퍼런스 코드는 평균점은 맞는 코드이다. GHGallery와 대동소이하다. 생각보다 잘 짜여진 것을 알았지만 왠지 유쾌하지 않다. 현 시점에서 문제가 되는 부분은 imageView의 갱신이 계속 일어난다는 점이기 때문이다. 

갱신을 막아야한다. 

GHGallery 의 내부적으로는 캐쉬를 사용해 반복적인 이미지 다운의 오버해드를 최대한 줄이고 있지만, 이정도로는 만족할 수 없는 것이다. 이를 해결하기 위해 고안한 코드는 다음과 같다. 


private HashMap<Integer, ImageView> container = new HashMap<Integer, ImageView>();
...
 

ImageView imageView = null;

if (convertView == null) {

imageView = new ImageView(parent.getContext());

imageView.setLayoutParams(new GridView.LayoutParams(95, 95));

imageView.setPadding(2, 2, 2, 2);

imageDownloader.download(url[position], imageView);

container.put(position, imageView);

}

else{

imageView = (ImageView)convertView;

}

 

//  Gridview 특징.

//  Gridview 화면에 표시되는 Grid 개체들은 ConverView null 하여 넘긴다

//  하지만 adapter getCount만큼 내부 개체들을 초기화 하기 때문에

//  실제로 내용은 없지만 ConvertView null 아닐 있다.  

 

//그러므로 이미지를 넣은 개체들은 컨테이너에 저장하고 

//이미지뷰를 넣지 않은 개체들은 ( 하지만 초기화 ) 새로 이미지뷰를 넣어준다.   

if( position < container.size() ){

if( (imageView = container.get(position)) == null ){

imageDownloader.download(url[position], imageView);

container.put(position, imageView);

}

}

else{

// 이곳이 문제.

imageView = new ImageView(parent.getContext());

imageView.setLayoutParams(new GridView.LayoutParams(95, 95));

imageView.setPadding(2, 2, 2, 2);

imageDownloader.download(url[position], imageView);

container.put(position, imageView);
 

 <수정안 1>

의도는 container 를 만들어, imageView가 생성될 시 저장하여 갱신시에 꺼내 쓰자는 것이다. 이것만으로 download 메소드 호출 횟수를 상당부분 줄일 수 있다. 의도와는 조금 다르게 이번에는 완전히 사용자 측면에서 속도만을 빠르게 수정한다면 어떻게 할 수 있을까? 




ImageView imageView;

if (convertView == null){

Log.d("DEBUG","position : "+position);

imageView = new ImageView(mContext);

imageView.setLayoutParams(new GridView.LayoutParams(95, 95));

//imageView.setAdjustViewBounds(false);

//imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);

imageView.setPadding(2, 2, 2, 2);

}else{

imageView = (ImageView) convertView;

}

//if(position < container.size())

imageView.setImageBitmap(container.get(position));


return imageView;

 
 
<수정안 2>

위의 코드를 위해 생성자도 수정했다. 

 

private HashMap<Integer, Bitmap> container = new HashMap<Integer, Bitmap>();
...
 

public ImageAdapter(String url[], Context context) {


if (url != null) {

this.url = url;

}

this.mContext = context;

for( int i = 0 , size = url.length ; i < size ; i++  ){

//ImageView temp = new ImageView(context);

//temp.setLayoutParams(new GridView.LayoutParams(95, 95));

//temp.setPadding(2, 2, 2, 2);

//imageDownloader.download(url[i], temp );

container.put( i, imageDownloader.downloadBitmap(url[i]));

}

}

 
<수정안 2 - 생성자와 컨테이너>

한번 테스트해보길... 초기 로드 속도는 답답함을 금치 못하게 하지만 로딩이 끝나고 난 스크롤 속도나 이미지 표시 속도는 거의 딜레이가 없을 정도로 빠르다. 하지만 아직 문제가 남아있는데, imageDownloader는 비동기로 가끔 데이터를 받아올 수 없는 상황이 생긴다. 이게 수정안 1에서는 이미지의 갱신시마다 검증을 했기 때문에 별 문제될 일이 아니지만 수정안 2에서는 꽤 큰 문제가 된다. 하지만 속도는 최고이기 때문에 이 코드를 어떻게든 살리고 싶다. 

 그 방안은 차차 살펴봐야 할듯 --_--;



두 번째 문제 해결 방안은, 필자의 아이디어가 아니므로 코드를 설명하고 끝내기로 하겠다. 

 
//decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f){
   
try {
       
//Decode image size
       
BitmapFactory.Options o = new BitmapFactory.Options();
        o
.inJustDecodeBounds = true;
       
BitmapFactory.decodeStream(new FileInputStream(f),null,o);

       
//The new size we want to scale to
       
final int REQUIRED_SIZE=70;

       
//Find the correct scale value. It should be the power of 2.
       
int width_tmp=o.outWidth, height_tmp=o.outHeight;
       
int scale=1;
       
while(true){
           
if(width_tmp/2<REQUIRED_SIZE || height_tmp/2<REQUIRED_SIZE)
               
break;
            width_tmp
/=2;
            height_tmp
/=2;
            scale
*=2;
       
}

       
//Decode with inSampleSize
       
BitmapFactory.Options o2 = new BitmapFactory.Options();
        o2
.inSampleSize=scale;
       
return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
   
} catch (FileNotFoundException e) {}
   
return null;
}

이는 구글링을 통해 StackOverflow사이트에서 찾은 소스코드이다. 
Bitmap을 리사이즈 하는 방법이 나와 있다. 

Bitmap 을 만들 때 option 사항을 줄 수 있는데, 
간단히 option의 inSampleSize 를 조정하는 것으로 사이즈를 조정할 수 있다. 하지만 위의 소스는 Bitmap의 원본 사이즈와 허용 사이즈를 비교해 스케일을 정하는 알고리즘이 포함되어 있어서 원본의 크기에 따른 스케일링이 가능하다. 


오늘도 문제 해결~~! '-^vv




긴 글 읽어주셔서 감사합니다. -fin



 
저작자 표시 비영리
크리에이티브 커먼즈 라이선스
Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시 2.0 대한민국 라이선스에 따라 이용하실 수 있습니다.
Posted by 화박 화박