안드로이드 개발에서 메모리 누수는 상대적으로 흔한 문제입니다. 안드로이드 프로그래밍 경험이 있는 어린이라면 이런 문제를 겪어야 하는데 메모리 누수는 왜 발생할까요? 메모리 누수의 영향은 무엇입니까?
Android 프로그램 개발에서 객체가 더 이상 필요하지 않아 재활용해야 하지만 사용 중인 다른 객체가 해당 객체에 대한 참조를 보유하여 재활용되지 않는 경우 재활용해야 하는 객체를 재활용할 수 없는 경우가 발생합니다. 재활용되어 힙 메모리에 머무르면 메모리 누수가 발생합니다.
메모리 누수의 영향은 무엇인가요? 이는 OOM 적용의 주요 원인 중 하나입니다. Android 시스템에서 각 애플리케이션에 할당하는 메모리는 제한되어 있으므로 애플리케이션에 메모리 누수가 많이 발생하면 필연적으로 애플리케이션에 필요한 메모리가 시스템에서 할당한 메모리 제한을 초과하게 되어 메모리 오버플로가 발생하고 응용프로그램이 충돌하게 됩니다.
메모리 누수의 원인과 영향을 이해한 후 우리가 해야 할 일은 일반적인 메모리 누수를 숙지하고 향후 Android 프로그램 개발에서 이를 방지하도록 노력하는 것입니다.
Java의 메모리 할당
정적 저장 영역: 컴파일 중에 할당되며 프로그램 실행 내내 존재합니다. 주로 정적 데이터와 상수를 저장합니다.
스택 영역: 메서드가 실행되면 메서드 본문 내부의 로컬 변수가 스택 영역 메모리에 생성되고 메서드가 끝난 후 메모리가 자동으로 해제됩니다.
힙 영역: 일반적으로 new로 생성된 개체를 저장합니다. Java 가비지 수집기에 의해 재활용됩니다.
네 가지 참조 유형 소개
강한 참조: JVM은 GC에서 강력한 참조가 있는 객체를 재활용하도록 하는 것보다 OOM을 사용합니다.
소프트 참조(SoftReference):
약한 참조(WeakReference): GC 중에 약한 참조만 있는 객체가 발견되면 현재 메모리 공간이 충분하든 아니든 관계없이 해당 객체의 메모리가 반환됩니다.
팬텀 참조: 언제든지 GC에 의해 재활용될 수 있습니다. 가비지 수집기가 객체를 재활용할 준비가 되어도 여전히 가상 참조가 있는 것으로 확인되면 재활용됩니다. 객체의 메모리에 들어가면 이 가상 참조를 연관된 참조 큐에 추가합니다. 프로그램은 참조 큐에 객체에 대한 가상 참조가 있는지 여부를 확인하여 객체가 재활용되는지 여부를 알 수 있습니다. GC가 객체를 재활용하기 위한 플래그로 사용될 수 있습니다.
우리가 자주 이야기하는 메모리 누수는 새로운 객체가 GC에 의해 재활용될 수 없다는 것을 의미합니다. 즉, 이는 강력한 참조입니다.
메인 메모리 누수 발생 시 증상 메모리 지터로 인해 사용 가능한 메모리가 천천히 감소합니다.
Andriod의 메모리 누수 분석 도구 MAT
MAT(Memory Analyser Tools )는 Eclipse 플러그인으로, 빠르고 기능이 풍부한 JAVA 힙 분석 도구로, 메모리 누수를 찾고 메모리 소비를 줄이는 데 도움이 됩니다.
QQ와 Qzone에서 메모리 누수를 모니터링하는 방법
QQ와 Qzone에서 메모리 누수는 SNGAPM 솔루션을 채택하여 성능 모니터링 및 분석을 위한 통합 솔루션입니다. 모니터링 정보를 집계하여 차트로 표시하고 분석 정보를 분석하여 주문을 제출하고 개발자에게 알리는 백엔드로 이동합니다.
SNGAPM은 앱(MagnifierApp)과 웹 서버(MagnifierServer)로 구성됩니다. );
MagnifierApp은 자동 메모리 누수 감지에서 감지 구성요소(LeakInspector)와 자동화된 클라우드 분석(MagnifierCloud)을 연결하는 중간 플랫폼입니다.
MagnifierServer 배경 분석 작업은 정기적으로 MagnifierCloud에 제출됩니다.
MagnifierCloud 분석이 완료된 후 데이터가 돋보기 웹에 업데이트되고 개발자에게 버그 티켓 형식으로 알림이 전송됩니다.
일반적인 메모리 누수 사례
사례 1. 싱글톤으로 인한 메모리 누수
싱글턴의 정적 특성으로 인해 애플리케이션이 실행되는 만큼 수명 주기가 길어집니다.
해결책:
이 속성의 참조 방법을 약한 참조로 변경하세요.
Context에서 전달하는 경우 ApplicationContext를 사용하세요.
예: 유출됨 code Fragment
private static ScrollHelper mInstance; private ScrollHelper() { } public static ScrollHelper getInstance() { if (mInstance == null) { synchronized (ScrollHelper.class) { if (mInstance == null) { mInstance = new ScrollHelper(); } } } return mInstance; } /** * 被点击的view */ private View mScrolledView = null; public void setScrolledView(View scrolledView) { mScrolledView = scrolledView; }
해결책: WeakReference 사용
private static ScrollHelper mInstance; private ScrollHelper() { } public static ScrollHelper getInstance() { if (mInstance == null) { synchronized (ScrollHelper.class) { if (mInstance == null) { mInstance = new ScrollHelper(); } } } return mInstance; } /** * 被点击的view */ private WeakReference<View> mScrolledViewWeakRef = null; public void setScrolledView(View scrolledView) { mScrolledViewWeakRef = new WeakReference<View>(scrolledView); }
사례 2. InnerClass 익명 내부 클래스
Java에서는 비정적 내부 클래스와 익명 클래스 모두 잠재적으로 자신이 사용하는 개체를 참조합니다. 외부 클래스에 속하지만 정적 내부 클래스는 그렇지 않습니다. 이 비정적 내부 클래스 인스턴스가 시간이 많이 걸리는 작업을 수행하는 경우 주변 개체가 재활용되지 않아 메모리 누수가 발생합니다.
해결책:
내부 클래스를 정적 내부 클래스로 변경합니다.
활동에 속성에 대한 강력한 참조가 있는 경우 속성의 참조 방법을 변경합니다. 약한 참조로 ;
비즈니스에서 허용하는 경우 Activity가 onDestory를 실행할 때 이러한 시간 소모적인 작업을 종료하세요.
예:
public class LeakAct extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_leak); test(); } //这儿发生泄漏 public void test() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
해결책:
public class LeakAct extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.aty_leak); test(); } //加上static,变成静态匿名内部类 public static void test() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
사례 3. Activity Context의 잘못된 사용
Android 애플리케이션에서 사용할 수 있는 Context 개체에는 일반적으로 Activity와 Application의 두 가지 유형이 있습니다. 클래스나 메서드에 Context 개체가 필요한 경우 일반적인 방법은 첫 번째 개체를 Context 매개 변수로 사용하는 것입니다. 이는 View 개체가 전체 활동에 대한 참조를 유지하므로 활동에 대한 모든 참조를 유지한다는 의미입니다.
애플리케이션에 상대적으로 큰 비트맵 형식의 이미지가 있고, 이미지를 회전할 때마다 이미지를 다시 로드하는 데 많은 시간이 걸리는 시나리오를 가정해 보겠습니다. 화면 회전 및 활동 생성 속도를 향상시키기 위한 가장 간단한 방법은 이 비트맵 개체에 정적 장식을 사용하는 것입니다. Drawable이 View에 바인딩되면 View 객체는 실제로 Drawable의 콜백 멤버 변수가 됩니다. 정적변수의 수명주기는 Activity의 수명주기보다 길다. 결과적으로 화면이 회전하면 Activity를 재활용할 수 없어 메모리 누수가 발생합니다.
해결책:
ActivityContext 대신 ApplicationContext를 사용합니다. 왜냐하면 ApplicationContext는 애플리케이션 존재와 함께 존재하고 Context에 대한 활동
의 수명 주기에 의존하지 않기 때문입니다. 참조는 자체 수명 주기를 초과해서는 안 되며 컨텍스트에 대해 "정적" 키워드를 주의해서 사용해야 합니다. 컨텍스트에 스레드가 있으면 onDestroy()에서 적시에 중지해야 합니다.
예:
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); }
해결 방법:
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 = getApplicationContext().getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }
사례 4. 핸들러로 인한 메모리 누수
지연된 작업이 있거나 실행 대기 중인 작업이 있는 경우 핸들러 작업 대기열이 너무 깁니다. 메시지가 핸들러에 대한 참조를 보유하고 핸들러가 외부 클래스에 대한 잠재적인 참조를 보유하므로 이 참조 관계는 메시지가 처리될 때까지 유지되어 활동을 수행할 수 없게 됩니다. 가비지 수집기에 의해 재활용되어 메모리 누수가 발생했습니다.
해결책:
Handler 클래스를 별도의 클래스 파일에 넣거나 정적 내부 클래스를 사용하여 누출을 방지할 수 있습니다.
내부에서 호출하려는 경우; 핸들러 활동이 있는 경우 핸들러 내부의 약한 참조를 사용하여 활동을 가리킬 수 있습니다. 정적 + WeakReference를 사용하여 핸들러와 활동 간의 참조 관계를 끊습니다.
해결책:
rree사례 5. 등록된 청취자 유출
系统服务可以通过Context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果Context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有Activity 的引用,如果在Activity onDestory时没有释放掉引用就会内存泄漏。
解决方案:
使用ApplicationContext代替ActivityContext;
在Activity执行onDestory时,调用反注册;
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
Solution:
mSensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
下面是容易造成内存泄漏的系统服务:
InputMethodManager imm = (InputMethodManager) context.getApplicationContext().get SystemService(Context.INPUT_METHOD_SERVICE);
Solution:
protected void onDetachedFromWindow() { if (this.mActionShell != null) { this.mActionShell.setOnClickListener((OnAreaClickListener)null); } if (this.mButtonShell != null) { this.mButtonShell.setOnClickListener((OnAreaClickListener)null); } if (this.mCountShell != this.mCountShell) { this.mCountShell.setOnClickListener((OnAreaClickListener)null); } super.onDetachedFromWindow(); }
case 6. Cursor,Stream没有close,View没有recyle
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null. 在我们的程序退出时一定要确保我们的资源性对象已经关闭。
Solution:
调用onRecycled()
@Override public void onRecycled() { reset(); mSinglePicArea.onRecycled(); }
在View中调用reset()
public void reset() { if (mHasRecyled) { return; } ... SubAreaShell.recycle(mActionBtnShell); mActionBtnShell = null; ... mIsDoingAvatartRedPocketAnim = false; if (mAvatarArea != null) { mAvatarArea.reset(); } if (mNickNameArea != null) { mNickNameArea.reset(); } }
case 7. 集合中对象没清理造成的内存泄漏
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
解决方案:
在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。
Solution
private List<EmotionPanelInfo> data; public void onDestory() { if (data != null) { data.clear(); data = null; } }
case 8. WebView造成的泄露
当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。
解决方案:
为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
case 9. 构造Adapter时,没有使用缓存的ConvertView
初始时ListView会从Adapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象 缓存起来。
当向上滚动ListView时,原先位于最上面的List Item的View对象会被回收,然后被用来构造新出现的最下面的List Item。
这个构造过程就是由getView()方法完成的,getView()的第二个形参View ConvertView就是被缓存起来的List Item的View对象(初始化时缓存中没有View对象则ConvertView是null)。