IT_Programming/Android_Java

[펌_번역] 안드로이드 메모리릭 회피하기

JJun ™ 2011. 10. 31. 22:49

------------------------------------------------------------------------------------------------

 출처: http://blog.naver.com/PostView.nhn?blogId=huewu&logNo=110082062273

 [출처] [번역] 안드로이드 메모리릭 회피하기|작성자 휴우

------------------------------------------------------------------------------------------------

 



  JAVA 에서도 자원 및 메모리 관리를 개발자가 전혀 신경 쓰지 않아도 되는 것은 아니더군요.
 특히 제 경우, 안드로이드 플랫폼에서
Process 생명주기에 대하여 잘 몰라, 어플리케이션이 필요 이상으로 메모리를
 사용하곤 했습니다.
안드로이드 개발자 사이트에 안드로이드 상에서 메모리 누수 문제를 피하기 위해
 주의해야 할 점에 관해서 짧지만 유용한 글이 있어 번역해 봅니다.
 

 

 

안드로이드 어플리케이션 (최소한 G1 의 경우)은 최대 16MB Heap 메모리 공간을 갖을 수 있다. 이 16MB 라는 공간은 일반적인 폰 어플리케이션에서는 충분히 큰 공간이지만, 보다 다양한 일을 수행하고자 하는 개발자들에게는 부족한 공간일 수도 있다. 또, 비록 개발자가 이 메모리를 모두 사용하지 않더라도, 다른 어플리케이션이 메모리 부족으로 죽지 않고 잘 작동할 수 있도록. 최소한의 공간만을 사용하도록 주의를 기울여야 한다. 안드로이드 플랫폼이 많은 어플리케이션을 메모리 상에 유지할 수 있다면, 사용자들이 어플리케이션을 훨씬 빠르게 변경할 수 있다. (이미 메모리상에 떠있으니까...). 이와 관련하여, 안드로이드 어플리케이션의 메모리릭 문제에 관해 살펴본 결과 많은 경우 메모리릭 문제는 동일한 실수 - Context 에 대한 참조를 오랫동안 유지 하는 것 - 으로 인해 발생하는 것임을 알게 되었다.

안드로이드에서 Context 는 다양한 작업 (주로 리소스를 읽어 오는데) 을 수행하기 위해 사용된다. 때문에 UI 요소들 생성 하는 경우, 항상 Context 를 인자값으로 넘겨 주어야 한다. 안드로이드 어플리케이션 상에서 개발자는 두 종류의 Context 를 사용할 수 있다. 하나는 Activity Context 이고, 다른 하나는 Application Context 이다. 일반적으로 개발자들이 Context 를 클래스 생성 시 혹은 함수 호출 시 인자로 사용하는 경우, 첫번째 Context 를 사용한다.

@Override
protected void onCreate(Bundle state) {
super.onCreate(state);

TextView label = new TextView(this);
label
.setText("Leaks are bad");

setContentView
(label);
}

위 코드에서 TextView 는 Activity 를 참조하고 있다. 다시 말해 Activtiy 와 Activity 에 포함된 그 밖의 다른 요소 - View 전체 적인 계층 구조를 비롯하여, 리소스 요소들에 대한 참조를 유지하고 있다는 뜻이다. 그럼으로, 만일 특정 어플리케이션에서 Context 릭이 발생하게 되면, (Leak 의 의미는 Context 에 관한 참조를 누군가가 계속 유지 하고 있음으로, GC 가 해당 Context 를 Collect 하지 못하게 됨을 의미 한다. ), 큰 공간의 메모리가 누수된다.

간단한 예를 들어보자. 기본적으로, 화면의 가로-세로가 변화되는 경우, 시스템은 상태값만을 유지하고 현재 Activity 를 종료 시킨 후, Activtiy 를 다시 생성한다. 이 과정에서, 해당 Activity의 UI 를 구성하기 위한 리소스를 다시 로드하게 된다. 만일 어떤 개발자가 크기가 큰 비트맵을 사용하는 어플리케이션을 작성했고, 화면이 변경될 때 마다, 해당 비트맵을 다시 로드하지 않기를 바란다고 생각해 보자. 가장 쉬운 방법은 해당 비트맵 자원을 Static 변수로 관리하는 것이다.

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
super.onCreate(state);

TextView label = new TextView(this);
label
.setText("Leaks are bad");

if (sBackground == null) {
sBackground
= getDrawable(R.drawable.large_bitmap);
}
label
.setBackgroundDrawable(sBackground);

setContentView
(label);
}
이 코드는 매우 빠르게 작동하지만, 매우 잘못되었다. 이 코드 상에서 최초로 생성된 Activity의 Context 릭이 발생한다. 어떠한 Drawable 요소가 View 에 추가될 때, 해당 View 는 Drawable의 Callback 으로 등록 된다. 위의 코드에서, TextView 에 추가된 Drawable(sBackground) 은 TextView 에 대한 참조, 더 나아가 Activity 자체(Context)에 대한 참조를 갖게 된다. (TextView 생성 시 Context 가 인자로 넘어갔음으로...) 따라서, 해당 Static 변수가 살아 있는 동안, 첫번째로 생성된 Activity에서 사용된 메모리 공간은 누수된다.

이 예제는 Context 릭이 발생하는 가장 단순한 예중에 하나이다.이러한 경우를 해결하기 위해 구글 개발자들이 어떤식으로 코드를 작성했는지, HomeScreen Activity 의 소스 코드를 살펴보면 알 수 있다. (unbindDrawables 를 살펴 보라) HomeScreen Activity 가 종료되어 onDestroy 가 호출되는 경우, 저장하고 있는 Drawable 의 Callback 을 모두 null 로 설정해 주었다. 보다 상황이 나쁜 경우, 개발자가 연속적인 Context Leak 을 발생 시킬 수도 있으며, 이럴 경우 사용가능한 메모리 공간은 빠르게 줄어든다.

Context 와 관련된 메모리 누수 문제를 해결 하기 위한 두 가지 쉬운 방법이 있다. 가장 확실한 첫번째 방안은, Context 가 자신의 Scope 외에서는 사용되지 않도록 하는 것이다. 두번째 해결책은 Application Context 를 사용하는 것이다. Application Context 는 Activity 의 생명 주기와는 관계없이. Application과 동일한 생명주기를 갖게된다. 만일 오랫동안 지속되먄서 Context 가 요구되는 객체를 사용하고자 한다면, Application 객체를 기억하라. Context.getApplicationContext() 혹은 Activity.getApplication() 을 호출 하면, Application Context 를 손쉽게 얻을 수 있다.

요역하자면, Context 와 관련된 메모리 누수 문제를 피하고자 할 경우, 다음 사항을 명심해라.

  • Activity Context 에 관한 참조를 오랫동안 유지하지 말아라. (해당 참조는 반드시 Activity 의 생명주기와 일치 해야 한다.)
  • Activity Context 대신, Application Context 를 사용할 것을 고려하라.
  • 만일 Activity 내부 클래스의 생명 주기를 잘 관리하는 경우가 아니라면, Activity 를 참조하고 있는 내부 클래스를 사용하지 말아라. 대신 Static 내부 클래스를 사용하고 해당 클래스가 Activity 와 WeakReference 를 갖도록 해라. 구체적인 구현 방식은 ViewRoot 를 참고 해라.
  • 가비지 콜렉터는 메모리 누수 문제가 일어나지 않도록 보장하지 않는다.
     

 


 

 

 


출처: http://givenjazz.tistory.com/48


 

안드로이드 메모리 누수 줄이기

 

 

안드로이드 진저브리드(2.3)부터 이미지 기본 디코딩방식이 16비트에서 32비트로 변경되었고, 이미지를 처리할 때 메모리를 3~4배쯤 더 사용하는 듯하다.
메모리누수는 더 심해져서 액티비티를 종료해도 상황에 따라 메모리가 다 반환이 되질 않는다. 결국 메모리를 직접 환원해줘야한다.


내일인 17일부터 갤럭시S의 진저브리드 업데이트가 시작되고, 앱이 죽는 걸 많은 사람들이 겪게 될텐데, 이 문제를 해결하기 위해 자원마다 null로 설정해주고
gc를 하는 것은 자바에서 작성하기 꽤나 괴로운 일이다. 다행히 메모리를 많이 잡아먹는 drawable만 리커시브로 해제해줘도 대부분의 메모리는 환원이 된다.


스택오버플로우랑 구글을 검색해도 질문만 있고 이렇다할 해결방법이 없길래 그냥 직접 작성해서 아파치2.0 라이센스로 공개한다.
다음의 메소드는
View에 붙어있는 View의 child를 리커시브로 null로 설정해주는 메소드다. 액티비티가 죽으면 가비지콜렉팅을 해도 레퍼런스가 삭제되서
메모리 환원이 안되므로
onDestroy안에서 System.gc()를 해줘야한다.  

 

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

 
package com.givenjazz.android;
 
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageView;
 
/**
 * @author givenjazz
 */

public class RecycleUtils

{
      
    private RecycleUtils(){};
 
    public static void recursiveRecycle(View root)

    {
        if (root == null)
            return;
        root.setBackgroundDrawable(null);
    

        if (root instanceof ViewGroup) {
            ViewGroup group = (ViewGroup)root;
            int count = group.getChildCount();
            for (int i = 0; i < count; i++) {
                recursiveRecycle(group.getChildAt(i));
            }
 
            if (!(root instanceof AdapterView)) {
                group.removeAllViews();
            }
 
        }
      
        if (root instanceof ImageView) {
            ((ImageView)root).setImageDrawable(null);
        }


        root = null; 
        return;
    }
}
 

 

    [사용 예제]

 

    @Override
    protected void onDestroy() {
        RecycleUtils.recursiveRecycle(getWindow().getDecorView());
        System.gc();
 
        super.onDestroy();
    }

 

 

위에 코드만으로도 어느정도 효과를 볼 수 있을 것이다.

하지만 코드를 보면 짐작할 수 있듯이 AdapterView는 제대로 환원되지 않는다.

AdapterView를 사용한다면 Adapter에 null로 설정해주는 메소드를 만들어서 onDestory()에

삽입해야한다. -수정- WeakReference로 만들지 않아도 액티비티를 종료할 때는 메모리 환원이 되지만

액티비티가 돌아가는 동안 어댑터에서 활용하는 뷰들은 메모리 환원이 되질 않는다. Reference를 활용하면 어댑터가 돌아가는 동안 안쓰는 뷰도 메모리 환원이 가능하다.

어댑터가 가비지컬렉팅을 제대로 못해서 죽는 경우가 있는데, 이 때는 OutOfMemoryError 예외를 잡아내서 제대로 환원 못한 뷰를 수동으로 풀어줘야한다.
다음에 예제와 곁들여서 제대로 설명하겠다.


null 설정해주는 메소드 만드는 예제) 주석으로 설명한 부분만 예제처럼 만들어주면 되고 나머지는 그냥 예제일 뿐이다.

 

package com.givenjazz.android;
 
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
 
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
 
import com.givenjazz.android.RecycleUtils;
 
public class OptionAdapter extends BaseAdapter {
    private ArrayList<Integer> mOptionList;
    private Activity mContext;
    // 멤버변수로 해제할 Set을 생성
    private List<WeakReference<View>> mRecycleList = new ArrayList<WeakReference<View>>();
    public int selectedIndex = -1;
 
    OptionAdapter(Activity c, ArrayList<Integer> list) {
        mContext = c;
        mOptionList = list;
    }
 
    // onDestory에서 쉽게 해제할 수 있도록 메소드 생성
    public void recycle() {
        for (WeakReference<View> ref : mRecycleList) {
            RecycleUtils.recursiveRecycle(ref.get());
        }
    }
 
    @Override
    public int getCount() {
        return mOptionList.size();
    }
 
    @Override
    public Object getItem(int position) {
        return mOptionList.get(position);
    }
 
    @Override
    public long getItemId(int position) {
        return position;
    }
 
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ImageView i = new ImageView(mContext);
        i.setImageResource(mOptionList.get(position));
      
        // 메모리 해제할 View를 추가
        mRecycleList.add(new <WeakReference<View>(i));
        return i;
    }
}

 

액티비티에서 어댑터까지 제거하는 예제)

    

    @Override
    protected void onDestroy()

    {
        // Adapter가 있으면 어댑터에서 생성한 recycle메소드를 실행
        if (mImageAdapter != null)
            mImageAdapter.recycle();
        RecycleUtils.recursiveRecycle(getWindow().getDecorView());
        System.gc();
 
        super.onDestroy();
    }

 

 

액티비티를 종료하기 전부터 메모리 오류가 발생한다면 가비지콜렉터 문제가 아니라 메모리에 로딩자체를 못하는 것이므로 xml을 inflate시키지 말고 BitmapFactory.Options로 작게 디코드하거나 16비트타입으로

디코딩 해야 한다.

 

예제.txt

 

 


 

 

[출처] http://www.androidpub.com/1080886

 

 

비트맵 관련 앱에서 많이 발생하는 힙메모리 관련 오류 해결하기

 


개발 환경 - 안드로이드 2.1

1. 액티비티
- onCreate에서 dalvik 버추얼 머신에게 힙메모리 임계값 지정하기 (70%)

   dalvik.system.VMRuntime.getRuntime().setTargetHeapUtilization(0.7f); 

   → 메인 액티비티에서 한번만 지정


- onDestroy 이벤트에서 모든 지역변수 null 처리히기, 모든 비트맵 인스턴스 변수 recycle 시키기,

   메인앱에서 System.gc() 한번 호출하기

 

- dalvik 버추얼 머신에게 dalvik.system.VMRuntime.getRuntime().runFinalizationSync()를 호출하여

   강제로 클래스들의 finalization 호출하기


2. 뷰를 상속한 차일드뷰
- destroyDrawingCache 메소드를 상속 받아서 이부분에서 비트맵 인스턴스들 recycle 및 null 처리하기

 

 

 


예제.txt
0.0MB