어댑터 안에서 메모리 관리하기.

Android 2011.06.11 21:56 Posted by 기분째즈

얼마 전에 내 블로그에 안드로이드 메모리 누수 줄이기 포스팅을 했는데, 어댑터 뷰에서 관리하는 법을 예제와 곁들어 추가로 설명할까 합니다.

어댑터뷰 안에 데이터는 실질적으로 어댑터가 관리를 하기 되기에 메모리 관리하는 부분도 어댑터 안에서 관리하는 게 좀 더 수월합니다. 가비지 콜렉터가 어느정도 알아서 관리를 해주나, 이미지처럼 메모리를 많이 사용할 경우 빠르게 해제하기 위해 직접관리를 해야하는 경우도 생깁니다.

다음 동영상은 저번에 올렸던 한 장씩 넘어가는 갤러리뷰에다가 고용량의 이미지 파일을 넣어서 메모리 관리가 되고 있는 모습입니다. (예를 들기 위해 오버했습니다. 실제로는 당연히 모바일 환경에 맞게 이미지를 줄여서 써야겠죠. 이미지 읽어들이는 시간도 꽤 길어서 멈추는 느낌이 드는데 이거 해결하는 기법은 포스팅하겠습니다) 바탕화면급 고용량 이미지라 빠르게 3~4장 정도만 이미지를 읽어도 java.lang.OutOfMemoryError: bitmap size exceeds VM budget이 발생합니다. 동영상보면 빨간색과 주황색 로그가 올라가는데 그게 Error가 발생한 부분이고 동영상은 메모리를 해제해주고 계속 작동 시키는  모습을 볼 수 있습니다.



어댑터에서 구현한 부분은 다음 부분입니다.


    //액티비티에서 어댑터를 메모리에서 해제하기 위해쓰는 메소드.
    //어댑터를 사용하는 액티비티의 onDestroy에 넣어주면 된다
.

    public void recycle() {

RecycleUtils.recursiveRecycle(mRecycleList);

    }


    //만들었던 뷰 목록 중 반을 지우는 메소드 

    public void recycleHalf() {

int halfSize = mRecycleList.size() / 2;

List<WeakReference<View>> recycleHalfList = mRecycleList.subList(0,

halfSize);

RecycleUtils.recursiveRecycle(recycleHalfList);

for (int i = 0; i < halfSize; i++)

    mRecycleList.remove(0);

    }



여기 RecycleUtils.recursiveRecycle은 전에 올렸던 메모리 누수 관리하기를 참고해주세요.

RecycleUtils 소스보기


WeakReference로는 레퍼런스를 해도 가비지 콜렉터할 때 신경을 안쓰기 때문에 가비지콜렉팅 대상에 포함됩니다. 예를 들어 Reference가 아니라 그냥 List에 View를 포함했다면 어댑터에서 자동으로 메모리 해제를 안하기 때문에 쉽게 메모리 오류가 발생할 겁니다.

    @Override

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

ImageView i = new ImageView(mContext);


try {

    i.setImageResource(mResources.get(position));

} catch (OutOfMemoryError e) {

    if (mRecycleList.size() <= parent.getChildCount()) {

Log.e(this + "", "size:" + mRecycleList.size());

throw e;

    }

    Log.w(this + "", e.toString());

    recycleHalf();

    System.gc();

    return getView(position, convertView, parent);

}

i.setAdjustViewBounds(true);

i.setLayoutParams(new Gallery.LayoutParams(LayoutParams.MATCH_PARENT,

LayoutParams.MATCH_PARENT));

mRecycleList.add(new WeakReference<View>(i));

return i;




 이미지를 읽다가 OutOfMemoryError 예외가 발생하면 전에 어댑터에서 사용했던 뷰의 반을 메모리에서 해제하고, 재귀로 getView를 호출합니다. 오류가 안 날때까지 반씩 줄이다가 어댑터뷰의 크기만큼 줄였는데도 메모리오류가 나면 그냥 오류던지고 종료시키도록 구현했는데, 이 부분은 상황에 맞게 처리해주시면 되겠죠. 이건 그냥 메모리를 관리하는 하나의 예제일 뿐입니다. OutOfMemoryError가 자주 난다면 try/catch로 잡는 것보다 mRecycleList의 크기를 비교해서 미리 해제해주는 등의 더 섬세하게 관리를 해주셔야 될겁니다.

갤러리는 가로스크롤 되고 안에 있는 내용이 세로스크롤 되는 뷰는 다음에 설명하도록 하겠습니다. 완전히 세로로 스크롤 되는 건 원하시면 그냥 리스트뷰 쓰시면 되겠습니다.

ImageAdapter 소스 전체보기


소스는 이미지 리소스만 다르고 전에 올렸던 안드로이드 아이폰처럼 한장씩 넘기를 갤러리와 동일해서 생략하겠습니다.
신고
아래는 첨부된 소스 실행 동영상입니다. 보면 한장씩 자연스럽게 넘어갑니다. 앱 위에 보면 아이폰의 페이지 컨트롤처럼 보여지는 부분도 있는데, 그거까지 설명하면 글이 길어지니 일단 생략하겠습니다.
 


안드로이드 기본 뷰는 한장씩 넘어가는 게 안되서 ViewFlipper나 ViewSwitcher로 구현을 하게 되는데 손으로 드래그해서 스크롤되는 효과를 줄 수가 없게 되죠. 여기서는 Gallery뷰를 상속받아 스크롤이 미끄러질때 미끄러지는 속도를 딱 한장만 넘어갈 정도의 속도로 낮춰서 만들었습니다.

학창시절 물리시간에 배운 기억을 더듬어봐서 속도를 공식으로 표현하면 속도의 제곱은 2 * 마찰계수 * 중력가속도 * 이동거리가 됩니다. 물리에 관한 글이 아니므로 어떻게 유도했는지 자세한 설명은 생략하겠습니다.


일단 상수인 마찰계수와 중력가속도부터 구해야하는데 정보가 없으니 안드로이드 프레임워크 소스를 열어서 분석해봐야합니다. 실제로 저런식으로 구현됐는지도 확인해봐야하고요. 설마했는데 실제로 소스를 열어보니 실제로 지구의 중력가속도에 실제 픽셀을 거리로 변환해서 구현했습니다. (이렇게 구현한 것도 사실 이해가 안가는데 토성, 목성 같은 곳의 중력가속도 정보도 있습니다;;;)

public OneFlingGallery(Context context, AttributeSet attrs) {

super(context, attrs);

float ppi = context.getResources().getDisplayMetrics().density * 160.0f;

mDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2)

* 39.37f // inch/meter

* ppi // pixels per inch

* ViewConfiguration.getScrollFriction();

    }


중력가속도는 SensorManager.GRAVITY_EARTH * 39.37f * ppi
마찰계수는 ViewConfiguration.getScrollFriction() 입니다.
소스를 열어보면 마찰계수가 0.015로 되어있습니다. 저 마찰계수는 빙판과 아이스하키 공(퍽)의 마찰계수 수준입니다. 스크롤을 한번하면 멈추지 않고 쭉쭉 미끄러지는 게 이해가 가는군요. (기껏 물리학적으로 자연스럽게 구현해서 저런식으로 부자연스럽게 낮은 마찰계수를 적용했는지는 이해가 안갑니다만)

참고로 진저나 프로요 이하에서는 마찰계수가 프레임워크에 상수로 고정되어 있으나 API 11(3.0 허니콤) 부터는 setFriction메소드로 마찰계수의 변경이 가능합니다. 그때부터는 속도가 아니라 마찰계수를 바꾸면 되니 더 자연스럽게 구현을 할 수 있을 겁니다.

이제 이동거리를 구해봅시다. 갤러리같은 어댑터뷰는 사실 스크롤이 되는 게 아니라 자식뷰들을 반대쪽으로 이동시키는 구조로 되어 있습니다. getScrollX()로 구하면 항상 0이 나오기 때문에 onScroll을 오버로드해서 자식뷰들을 이동시킨 거리를 합해서 구해야합니다.

    @Override

    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,

    float distanceY) {

mDistanceX += distanceX;

return super.onScroll(e1, e2, distanceX, distanceY);

    }


    @Override

    public boolean onTouchEvent(MotionEvent event) {

if (event.getAction() == MotionEvent.ACTION_DOWN) {

    mDistanceX = 0;

}

return super.onTouchEvent(event);

    }

 
이렇게 하면 mDistanceX에 스크롤된 거리가 합산이 됩니다. onScroll을 오버로드 하지말고, onTouchEvent에서 이벤트거리만 합산해도 되지 않겠냐고 생각할지 모르지만 안됩니다. 마지막에 onScroll이벤트보다 fling이 먼저 발생하는데 그 때 1,2 픽셀정도 오차가 나게되고 한장씩 넘어가는 건 맞지만 딱 맞아떨어져서 멈추지 않기 때문에 묘하게 거슬립니다.

갤러리 크기에서 스크롤된 거리를 빼면 이동해야할 거리가 나옵니다.

    @Override

    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,

    float velocityY) {


float toMoveDistance = getWidth() - Math.abs(mDistanceX);

float maxVelocity = (float) Math.sqrt(toMoveDistance * mDeceleration

* 2);

float revisedVelocityX = 0;


if (velocityX > 0) {

    revisedVelocityX = Math.min(velocityX, maxVelocity);

} else {

    revisedVelocityX = Math.max(velocityX, -maxVelocity);

}


return super.onFling(e1, e2, revisedVelocityX, velocityY);

    } 


이렇게 구한 이동해야할 거리로 속도를 구합니다. 속도를 자연스럽게 구현할 때 속도를 낮추는 건 괜찮지만 빠르게 하는 건 이상하겠죠? 자연스럽게 하기위해 속도는 원래 속도와 이동해야할 속도 중 낮은 속도로 수정해줍니다. 이렇게 해서 가다가 멈춰도 알아서 적당한 위치로 붙기 때문에 속도를 낮춰도 괜찮습니다. 그리고 그렇게 구한 속도를 fling에 넣어주면 됩니다.

아래의 전체소스이용해서 갤러리를 만드시면 되고 사용법은 기존의 Gallery 뷰와 동일합니다.

전체소스 보기


첨부된 파일은 동영상처럼 아이폰의 페이지 컨트롤도 포함시켰고, 바로 전에 썼던 메모리관리 기법도 적용한 예제소스입니다. (이 부분도 설명할 부분이 있는데, 너무 방대해져서 그냥 소스만 첨부합니다. 나중에 기회되면 어댑터 내에서 메모리 관리하는 것도 설명할게요)

화면에 꽉차는 이미지 등장할 때부터 부터는 이미지가 좀 늦게 등장하는 현상이 있는데 그림파일 I/O 때문에 발생합니다. 다음에 I/O시간 때문에 UI가 멈추는 시간을 제거하는 기법을 설명하거나 갤러리 안에서 자연스럽게 세로스크롤 되는 기법을 설명하겠습니다.(첨부된 소스 안에 OneFlingScrollGallery는 세로스크롤 하는 갤러리 소스입니다)


소스가 첨부된 GitHub 링크
 
신고


 

티스토리 툴바