: 대부분의 메모리 누수는 필요 이상으로 객체가 메모리에 오래 남아있기 때문에 발생한다. 메모리 누수를 피하거나 완화하는 기본적인 방법을 알아보자.


- 정적 내부 클래스 사용

:보통 액티비티의 내부 클래스에서 액티비티에 있는 뷰 계층의 참조로 인해서 메모리 누수가 많이 발생하다. 이를 피할 방법으로는 일반적인 내부 클래스 대신 정적 내부 클래스를 사용하는 것이다. 정적 내부 클래스는 인스턴스 객체가 아닌 오직 전역 클래스 객체만 참조 가능하기 때문에 외부 인스턴스 필드에 접근할 수 없다.


- 약한 참조 사용

: 정적 내부 클래스는 외부 인스턴스 필드에 접근할 수 없다. 따라서 작업을 수행할 때 제한사항이 많아진다. 이에 대한 대책으로 java.lang.ref.WeakReference를 사용할 수 있다. WeakReference는 약한 참조를 지원한다. 약한 참조는 일반적인 참조, 강한 참조와 달리 참조한 객체의 가비지 컬렉션이 가능하다. 다음은 약한 참조를 사용한 예이다.

public class Outer{
	private int mField;
	private static class SampleThread extends Thread{
		private final WeakReference<Outer> mOuter;
		
		SampleThread(Outer outer){
			mOuter = new WeakReference<Outer>(outer);
		}
		
		@Override
		public void run(){
			//외부 인스턴스가 가비지 컬렉션되었는지 null로 확인
			if(mOuter.get() != null){
				mOuter.get().mField = 1;
			}
		}
	}
}


- 스레드 실행 중지 및 메시지 큐 정리

: 위에서 배운 두 가지 방법으로는 메모리 누수를 막을 수 없다. 작업을 다한 스레드는 꼭 종료시켜야 한다. 핸들러를 사용한다면 메시지 큐에 보내진 메시지를 사용하지 않을 때는 removeCallbacksAndMessages 메서드로 메시지를 종료시켜야 한다. 참고로 handler.hasMessage(int what), handler.hasMessage(int what, Object tag) 메서드를 사용하면 해당 메시지가 메시지 큐에 현재 존재하는 지 알 수 있다.



- 스레드 유지

: 안드로이드에서 메모리 누수의 많은 원인 중 하나는 스레드와 안드로이드 구성요소 간 생명주기의 불일치이다. 예로 화면 회전과 같이 구성 변경이 일어나게 되면 액티비티가 새로 생기게 되는 데 기존 스레드는 기존의 액티비티를 계속 참조해 액티비티가 GC 되지 않을 수도 있다. 따라서 이런 구성 변경에 대처하기 위해서 스레드를 유지하면서 기존의 안드로이드 구성요소는 버리고 새로운 안드로이드 구성요소를 참조하도록 해야 한다. 구현 방법은 액티비티와 프래그먼트로 나뉜다.


1. 액티비티에서 스레드 유지

: 액티비티에서 스레드 유지는 onRetainNonConfigurationInstance()와 getLastNonConfigurationInstance() 메서드를 이용해 구현한다. 전자 메서드는 구성 변경 전에 콜백되고 새로운 액티비티 객체에 전달하고자 하는 객체를 반환해야 한다. 스레드 유지를 위해서는 스레드를 반환하면 된다. 구성 변경 후에 새로운 액티비티의 onCreate나 onStart 메서드에서 후자 메서드를 호출해 반환값을 전달 받은 객체로 캐스팅하면 된다. 단, 구성 변경이 아닌 액티비티 재시작에서는 null이 반환된다. 액티비티에서 스레드 유지는 현재 잘 사용되지 않는다. 대부분 애플리케이션의 액티비티가 프래그먼트로 구성되 있고 프래그먼트에서 스레드 유지 방법이 더 간단하기 때문이다. 다음은 이에 대한 예이다. 

public class ThreadRetainActivity extends Activity{
	private static class MyThread extends Thread{
		private ThreadRetainActivity mActivity;
		
		public MyThread(ThreadRetainActivity activity){
			mActivity = activity;
		}
		
		private void attach(ThreadRetainActivity activity){
			mActivity = activity;
		}
		
		@Override
		public void run(){
			final String text = getTextFromNetwork();
			mActivity.setText(text);
		}
		
		//긴 동작
		private String getTextFromNetwork(){
			// 네트워크 동작 시뮬레이션
			SystemClock.sleep(5000);
			return "Text from network";
		}
	}
	
	private static MyThread t;
	private TextView textView;
	
	@Override 
	public void onCreate(Bundle savedInstanceState){
		...
		Object retainedObject = getLastNonConfigurationInstance();
		if(retainedObject != null){
			t = (MyThread)retainedObject;
			t.attach(this);
		}
	}
	
	@Override
	public Object onRetainNonConfigurationInstance(){
		if(t!=null && t.isAlive()){
			return t;
		}
		return null;
	}
	
	.....
}


2. 프래그먼트에서 스레드 유지

: Fragment.onCreate()에서 setRetainInstance(true)를 호출하면 된다. 그러면 프래그먼트는 설정 변경동안 유지되고 프래그먼트에서 실행한 스레드는 계속 살아있게 된다. 
public class ThreadRetainActivity extends Activity{
	private ThreadFragment mThreadFragment;
	
	public void onCreate(Bundle savedInstanceState){
		...
		
		FragmentManager manager = getFragmentManager();
		mThreadFragment = (ThreadFragment)manager.findFragmentByTag("threadfragment");
		if(mThreadFragment == null){
			FragmentTransaction transaction = manager.beginTransaction();
			mThreadFragment = new ThreadFragment();
			transaction.add(mThreadFragment, "threadfragment");
			transaction.commit();
		}
		
	}
	
	publicvoid onStartThread(View v){
		mThreadFragment.execute();
	}
	
	....
}

public class ThreadFragment extends Fragment{
	private ThreadRetainActivity mActivity;
	private MyThread t;
	
	private class MyThread extends Thread{
		@Override
		public void run(){
			final String text = getTextFromNetwork();
			mActivity.setText(text);
		}
		
		//긴 동작
		private String getTextFromNetwork(){
			// 네트워크 동작 시뮬레이션
			SystemClock.sleep(5000);
			return "Text from network";
		}
	}
	
	@Override
	public void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);
		setRetainInstance(true);
	}
	
	@Override
	public void onAttach(Activity activity){
		super.onAttach(activity);
		mActivity = (ThreadRetainActivity)activity;
	}
	
	@Override
	public void onDetach(){
		super.onDetach();
		mActivity = null;
	}
	
	public void execute(){
		t = new MyThread();
		t.start();
	}

}


: 안드로이드는 UI 변경을 메인 스레드에서 밖에 하지 못 한다. 그래서 메인 스레드의 블록을 최소화하기 위해 파일 I/O, 네트워킹, 문자열 파싱, 복잡한 연산 등은 별도의 백그라운드 스레드를 만들어서 처리한다. 처리 결과는 사용자에게 보여주기 위해서 대부분 UI 스레드로 보내진다. 따라서 백그라운드 스레드와 UI 스레드 사이에 통신이 필요한 데 이 때 사용하는 게 핸들러이다. 핸들러는 스레드들 사이의 통신을 가능하게 해주는 메커니즘이다. 핸들러는 루퍼와 메시지 큐를 이용해서 스레드 사이에 통신을 가능하게 한다. 그렇기 때문에 핸들러는 안드로이드 개발자들이 코딩할 때 많이 사용하지만 잠재적인 메모리 누수의 원인이 될 수 있기에 조심해야 한다. 


우리는 여기서 핸들러로 인해 일어날 수 있는 메모리 누수에 대해 알아볼 것이다. 먼저 가비지 컬렉션이 되는 대상은 다른 객체에 의해 참조되지 않는 미사용 객체를 일컫는 다는 걸 참고 바란다. 해당 포스트를 읽기 전에 "(익명)내부 클래스/정적 내부 클래스 스레드에서 GC(가비지 컬렉션)" 포스트를 읽으면 도움이 될 것이다.  핸들러의 구현을 위해서는 루퍼, 메시지 큐가 필요하다. 따라서 핸들러가 구현된 스레드에는 루퍼와 메시지 큐를 참조하게 된다. 메시지 큐는 핸들러에 보낼 메시지들을 담고 있기 때문에 메시지를 참조하게 된다. 메시지 내부에는 핸들러, Object, Runnable과 참조를 유지한다. 따라서 메시지 큐에서 처리를 대기하고 있는 메시지들은 스레드가 실행되는 동안 가비지 컬렉션의 대상이 될 수 없다. 구체적인 예로 한 번 알아보자. 핸들러는 데이터 메시지 전송과 태스크 메시지 전송으로 나눌 수 있다. 각각에 대해서 잠재적인 메모리 누수 위험을 알아보자.  


※ 데이터 메시지 전송

public class Outer{
	Handler mHandler = new Handler(){
		public void handleMessage(Message msg){
			....메시지 처리
		}
	};
	
	public void doSend(){
		Message message = mHandler.obtainMessage();
		message.obj = new SampleObject();
		mHandler.sendMessageDelayed(message, 60*1000);
	}
}

위 코드에서 핸들러에 보내진 메시지가 오랫동안 메시지 큐에 남아있다면 SampleObject가 제거되야할 상황이 와도 가비지 컬렉션이 불가능하다. 잠재적인 메모리 누수 위험이 있다.


※ 태스크 메시지 전송

public class Outer{
	Handler mHandler = new Handler(){
		public void handleMessage(Message msg){
			....메시지 처리
		}
	};
	
	public void doPost(){
		mHandler.post(new Runnable(){
			public void run(){
				
			}
		})
	}
}

위 코드도 마찬가지다. Runnable에서 외부 객체를 참조하고 있다면 오랫동안 메시지 큐에 남아있을 시 잠재적인 메모리 누수 위험이 된다. 


위에서 보았듯이 대기하는 메시지가 큐에 오래있게 되면 메모리 누수 위험이 커지게 된다. 이런 메모리 누수를 방지하는 방법으로는 정적 내부 클래스를 사용, 약한 참조 사용, 메시지 큐 정리 등이 있다. 이에 대해서는 "안드로이드 기본적인 메모리 누수 방지" 포스트에서 확인 바란다.

+ Recent posts