모바일앱/앱성능 최적화

[안드로이드앱성능최적화] MAT을 이용한 메모리누수 점검

오픈이지 2013. 7. 16. 16:04

MAT(Memory Analysis Tool) 이란?




[SLIDE_01]





[SLIDE_02]





eclipse plug-in으로 MAT 설치하고 실행하기



[SLIDE_03]




[SLIDE_04]




[SLIDE_05]





분석결과 보기




[SLIDE_06]




[SLIDE_07]




[SLIDE_08]




[SLIDE_09]




[SLIDE_10]




[SLIDE_11]




[SLIDE_12]




[SLIDE_13]




[SLIDE_14]




[SLIDE_15]




[SLIDE_16]





[SLIDE_17]




[SLIDE_18]




[SLIDE_19]




[SLIDE_20]





[SLIDE_21]





HCGellary 앱을 이용한 메모리 누수 모니터링 해보기


안드로이SDK설치폴더/sample 폴더에 가면 "Honey Comb Gellary" 앱을 사용하여 메모리 누수 모니터링 작업을 수행해 본다.

sample프로젝트는 메모리릭이 발생되지 않도록 작성된 프로그램이다. 

우리는 여기서 메모리가 문제가 발생하도록 소스코드를 약간 수정한 뒤 테스트 해보도록 하겠다.

리소스로 부터 읽어온 Bitmap 객체를 ImageView에 붙여서 보여주는 작업을 수행하는데 우리는 이 Bitmap을 HashMap에 캐싱해서 한번 본 이미지에 대한 처리 속도를 높이도록 프로그램을 다음과 같이 수정한다.




[SLIDE_22]




[SLIDE_23]




[SLIDE_24]




[SLIDE_25]



LogCat에는 OOM(Out-of-Memory)에 어느 파일에 몇번째 라인에서 발생했는지에 에러 메시지를 확인할 수 있다. 우리는 좀더 자세한 OOM원인과 상태를 확인하기 위해 해당 프로세스에 대한 메모리 덤프를 확보하고 MAT로 분석 해보려고 한다.




[SLIDE_26]




[SLIDE_27]


Histogram View에 의하면 byte[] 클래스의 Shallow Heap과 Retained Heap이 앱이 사용할 수 있는 Heap 90% 이상을 차지하는 것을 확일 할 수 있다.




[SLIDE_28]

해당 클래스 객체를 참조하는 클래스 객체정보를 확인해 보니 SBitmapCache의 누적된 값이 무척이나 큰것을 확일할 수 있다. 이것은 메모리 누수의 원인으로 판단가능한 데이터이다.




[SLIDE_29]




위 예제의 해결책은?



위 예제의 문제점은 Bitmap 이미지를 캐시하기위해 사이즈제한이 없는 HashMap을 사용하여 캐싱하다 OOM이 발생한 경우이다. 이런 경우에는 LruCache 을 이용하여 이미지 캐시를 하도록 프로그램을 수정할 수 있다.


LruCache은 메모리캐시를 구현하는 API 로 지정된 사이즈만큼 캐시한다. 캐시 사이즈보다 더 많은 데이터를 저장해야 하는 경우 LRU 알고리즘에 따라 가장 최근에 사용하지 않은 데이터를 밀어내고 새로운 데이터를 저장하는 방식을 사용한다.



private static HashMap<String,Bitmap> sBitmapCache = new HashMap<String,Bitmap>();

==> private static LruCache  mMemoryCache; 


LruCache의 사이즈를 앱의 heap메모리 한계의 1/8 정도로 설정하려고 한다면 프래그먼트가 액티비티에 붙을 때 호출되느 onAttach() 메소드를 재정의하여 캐쉬의 사이즈를 설정하도록 한다.


  @Override
   public void onAttach(Activity activity) {

	super.onAttach(activity);
	
       // 앱이 사용할 수 있는 Heap Size 
	final int memClass =((ActivityManager)(activity.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE))).getMemoryClass();

      // 힙사이즈의 1/8 을 캐시영역으로 할당하기 위해 캐시크기 계산
       cacheSize=1024*1024*memClass/8;
	    
       mMemoryCache=new LruCache(cacheSize);
	   
  }



  
  // 캐시에 저장되어 있지 않으면 Bitmap을 메모리에 캐싱한다.
  public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    	if (getBitmapFromMemoryCache(key) == null) {
    		mMemoryCache.put(key,bitmap);
    	}
    }
    
    // 해당 키값으로 이미지가 캐시되어 있으면 Bitmap을 찾아서 반환한다.
    public Bitmap getBitmapFromMemoryCache(String key) {
            return (Bitmap)mMemoryCache.get(key);
   	}
  



   void updateContentAndRecycleBitmap(int category, int position) {
        mCategory = category;
        mCurPosition = position;

        if (mCurrentActionMode != null) {
            mCurrentActionMode.finish();
        }

        // 캐시에 키로 사용할 ID를 생성한다.
        String bitmapId=""+category+"."+position;

        // 캐시로 부터 Bitmap 이미지를 찾아온다.
        mBitmap=  getBitmapFromMemoryCache(bitmapId);

        // 캐시된 이미지가 없으면 Resource로 부터 이미지를 읽어서 Bitmap으로 반환하고, 캐시에 저장한다.
        if ( mBitmap == null ) {
               mBitmap = Directory.getCategory(category).getEntry(position)
                                  .getBitmap(getResources());
              addBitmapToMemoryCache(bitmapId, mBitmap);        
        }
        // 읽어온 Bitmap 이미지를 화면에 보여준다.
        ((ImageView) getView().findViewById(R.id.image)).setImageBitmap(mBitmap);
    }