android memory leak 처리

Inner Class 누수

Activity 내부에 아래와 같이 Inner Class를 정의 할 경우 잠재적으로 누수의 대상이 된다.

...

public class MainActivity extends Activity {

    private static Innserclass inner;
    private String mStr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        inner = new Innserclass();
        new Thread(inner).start();
        mStr = "hello";
    }

    public class Innserclass implements Runnable {
        public Innserclass() {
            mStr+="nono!!";
        }

        @Override
        public void run() {
            getApplicationContext();
        }
    }
}

첫번째 누수 사유는 static이다.

private static Innserclass inner;

일단 static에 memory를 assigne하게 되면 해당 instance는 모 class인 activity와는 별도로 메모리 상에 살아 남게 된다.

두번째 사유는 thread 처리이다.

inner class를 thread처리 함으로써 (이경우는 postdelay도 마찬가지다) UI Thread가 죽어도 살아 남을 가능 성이 있다. Destroy 시점에 반듯이 thread를 종료 처리 해줘야 한다. handler도 이 경우에 속하게 된다.

세번째 사유는 InnerClass 자체에 있다. 첫번째 사유든 두번째 사유든 해당 Instance는 잠재적으로 모 Class의 Context를 접근 할 수 있게 된다. 코드상으론

mStr+="nono!!";

이 부분이다.

해결 방법은 다음고 같다.

Innser Class를 절대로 static 처리 하지 말아라

-    private static Innserclass inner;
+    private Innserclass inner;

Destroy시점에 Activity와 같이 종료할 수있다면 상관이 없을 수는 있다. 그러나 라이프 사이클이 Activity와 같다면 Static을 쓸 이유가 없다.

Inner Class는 static으로 정의 하던가 완전히 별도 파일로 제외를 시켜라

-    public class Innserclass implements Runnable {
+     public static class Innserclass implements Runnable {

static 처리를 하게 되면 해당 클래스는 모 class와 독립된 메모리 참조 구조를 갖게 되어 독립하게 된다.

weekRefernece를 사용해라

+    public static class Innserclass implements Runnable {
+        private final WeakReference<MainActivity> mActivity;

        public Innserclass(MainActivity activity) {
+           mActivity = new WeakReference<>(activity);
        }

        @Override
        public void run() {
-            getApplicationContext();
+            mActivity.getApplicationContext();
        }
    }

직접적인 Reference를 주입해서는 안된다. 반듯이 weekRefernece를 이용해서 context를 공유 해야한다. thread와 같이 Main Thread의 범위 밖에서 작동이 된다면 MainActivity가 명시적으로 종료 되어도 해당 Context가 별도의 Thread에 남아있게 되어서 Memory Leak이 나게 된다.

상기의 문제는 ViewModel을 사용하게 되면 더 큰 문제가 된다. ViewModel의 경우 Activity나 Fragment와 lifecycle을 같이 하게 되는데, Context가 삭제 되지 않는 동안은 ViewModel도 데이터와 함께 삭제되지 않게 된다.

viewModel에 대한 자세한 내용은 하기 링크를 확인 바란다.

2022.02.05 - [Android] - Android ViewModel & ListView 사용하기

Handler 누수

내용상으로는 Inner class와 동일하다. weekreference를 이용해서 instance를 생성하게 해야한다.

    private static class MainHandler extends Handler {

        private final WeakReference<MainActivity> mActivity;

        public MainHandler(MainActivity activity) {
            mActivity = new WeakReference<>(activity);
        }

기본 적인 handler의 constructor는 인수가 없음으로 강제로 넣어야만 한다.

그리고 종료시에는 반듯이 다음 행위를 처리해줘야 한다.

    @Override
    protected void onDestroy() {

        super.onDestroy();
        if (MainHandler != null) {
            MainHandler.removeCallbacksAndMessages(null);
            MainHandler = null;
        }

혹시 남아 있을지 모르는 메시지 큐 내용을 clear해줘야 한다.

Webview 누수

webview를 사용할때는 해당 view item이 destory가 완전히 되었고, 그에따른 메모리 반납이 이루어 졌는지 확인해야 한다. 그렇지 않다면 Native영역과 Others영역의 메모리가 지속 누적되는 현상이 발생 하게 된다. 이러한 현상은 layout상에 있는 webview를 call하게 되면 굉장히 높은 확률로 Leak이 발생하게 된다.

아래는 activity_main.xml과 MainActivity 수도 코드이다

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/webviewHell"
    tools:context=".MainActivity">    

    <WebView
        android:id="@+id/activity_main_webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>
public class MainActivity extends Activity {

    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        getApplicationContext();
        mWebView = findViewById(R.id.activity_main_webview);

Destory시 다음과 같은 code를 넣어 보아도 메모리 leak이 나는 것을 확인 할 수있다.

    .....
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mWebView.clearHistory();
        mWebView.destroyDrawingCache();
        mWebView.setWebViewClient(null);
        mWebView.setWebChromeClient(null);
        mWebView.removeAllViews();
        mWebView.clearCache(true);
        mWebView.freeMemory();
        mWebView.removeAllViewsInLayout();
        mWebView.setVisibility(View.GONE);
        mWebView.destroy();
        mWebView = null;

webview는 instance 생성을 통해서 처리해라

...
    RelativeLayout mWebviewlayout;
    WebView mWebView;

    protected void onCreate(Bundle savedInstanceState) {        
        mWebviewlayout = (RelativeLayout) findViewById(R.id.webviewHell);
        mWebView = new WebView(getApplicationContext());
        mWebView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
        mWebviewlayout.addView(mWebView);

 ...

완전히 연관 리로스를 clear해라

   @Override
    protected void onDestroy() {
        super.onDestroy();
        mWebView.removeJavascriptInterface("bridge");

        mWebView.loadUrl("about:blank");

        jsInterface = null;
        mJSCallback = null;

        mWebviewlayout.destroyDrawingCache();
        mWebviewlayout.removeAllViews();
        mWebviewlayout.removeAllViewsInLayout();
        mWebviewlayout.setVisibility(View.GONE);
        mWebviewlayout = null;

        mWebView.clearHistory();
        mWebView.destroyDrawingCache();
        mWebView.setWebViewClient(null);
        mWebView.setWebChromeClient(null);
        mWebView.removeAllViews();
        mWebView.clearCache(true);
        mWebView.freeMemory();
        mWebView.removeAllViewsInLayout();
        mWebView.setVisibility(View.GONE);
        mWebView.destroy();
        mWebView = null;

상위 destroy 코드는 다소 과하다 싶을 정도로 remove하고 있다.

여기서 주의 깊게 봐야할 것은

mWebView.freeMemory();

이것이다. freeMemory api는 deprecated되었다. 공식 문서에서는 이 기능을 대체 하는 기능으로

mWebView.loadUrl("about:blank");

사용을 권장한다.

최후에는 app data를 전부 삭제해라

    public static void deleteCache(Context context) {
        try {
            File dir = context.getCacheDir();
            deleteDir(dir);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static boolean deleteDir(File dir) {
        if (dir != null && dir.isDirectory()) {
            String[] children = dir.list();
            for (int i = 0; i < children.length; i++) {
                boolean success = deleteDir(new File(dir, children[i]));
                if (!success) { return false; }
            }
            return dir.delete();
        } else if(dir!= null && dir.isFile()) {
            return dir.delete();
        } else {
            return false;
        }
    }

안드로이드는 apk별로 데이타를 임시 저장할 수 있는 공간이 있다.

일반적으로 adb shell을 진입했을때

/data/<<app package>>

이하를 가르키게 된다. 해당 폴더 이내에 있는 디렉토리를 모두 다 지우는 행위임으로, 주의 깊게 사용해야 한다.

ImageView 사용

Bitmap에 대한 이미지 릭은 매우 유명하다. 허니콤(3.2)을 기준으로 이전에는 Bitmap 정보를 Native Heap영역에 담고 이를 포인팅 처리하도록 되어있었다.

이미지포인터(java heap) -> char[] (native heap)

하지만 이는 포인터를 잊는 순간 Native영역에 해소 되지 않는 메모리 릭을 발생 시켰고 이점을 해결하기 위해 메모리 영역을 Heap 영역으로 옮겼다 . Profiler에서도 보면 비트맵 힙 영역을 별도록 볼수있게 하였다.

그렇다고 Bitmap에 대한 메모리 릭이 사라지진 않았다. 무조건 Image View는 recycle 처리를 해줘야 한다.

...
        ImageView image = (ImageView) findViewById(R.id.loadingImage);
        Bitmap myBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath());
        image.setImageBitmap(myBitmap);

...
    @Override
    protected void onDestroy() {
        super.onDestroy();
        image.recycle();

그러나 이와 같은 코드 작성을 위해서 Bitmap을 모두 class 변수로 끌어 내는건 좋지 않음으로 다음 코드를 사용해서 recycle처리 할 수 있다.

    private static void recycleBitmap(ImageView imageView) {
        if (imageView != null) {
            Drawable d = imageView.getDrawable();
            if (d != null && d instanceof BitmapDrawable) {
                Bitmap b = ((BitmapDrawable)d).getBitmap();
                if (b != null) {
                    b.recycle();
                }
            }
            imageView.setImageResource(android.R.color.transparent);
            imageView.setImageBitmap(null);
        }
    }

단, 주의해야할 코드가 있다.

        ImageView image = (ImageView) findViewById(R.id.loadingImage);
        Bitmap myBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath());
-        image.setImageBitmap(myBitmap);
+        image.setBackgroundResource(myBitmap);

setBackgroundResource의 경우는 recycle 처리가 어렵다. 해당 메서드는 ImageView가 아닌 View자체의 메서드를 사용함으로써 Drawable 객체를 반환하지 않는다.

fragment에서 viewbinding은 반듯이 null 처리 해라

Fragment를 사용할 경우 onCreateView에서 View binding을 처리하고 onDestoryView에서 반듯이 null 처리를 해줘야 한다.

class TestFragment : Fragment() {
    var _binding: FragmentTestBinding? = null
    val binding get() = _binding!!
    lateinit var viewModel: DataViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        _binding = FragmentTestBinding.inflate(inflater,container,false)

         ....

        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        Log.d(this.javaClass.name,"finish view")
        _binding = null
    }

}

_binding = null 처리를 했다고 해서 메모리에서 즉각적으로 사라지는 것은 아니다. 다만 null 처리를 함으로써 GC에 삭제 대상이 되었다고 조금더 빠르게 알릴 수 있게 된다.

만약에 해당 처리를 하지 않는다면 불특정 시간내에 삭제가 될것 이다.

마지막으로 왜 onDestroy에서 처리 하지 않는가? onDestroy는 Data의 처리를 위해 적합하지 않고, Android OS에서도 해당 Method의 Call을 100% 확신하지 않는다. 또한 불리우는 타이밍도 프로그램상으로 정확하게 이루어진다고 볼 수 없다. 최대한 onDestory에 Businiess Logic 처리 및 리소스 해제는 사용하지 않는것이 좋다.

다만 신기한건 Android 공식 문서상에 보면 _binding 해제는 Fragment에서만 진행하고 있다. 그래서 Activity에서 메모리 Leak을 강제로 만들어 보려고 했는데 생각처럼 재현이 되진 않았다.

728x90
반응형

'Android' 카테고리의 다른 글

AOSP system app install  (0) 2020.08.20
Android Grafika Texture Surface  (0) 2020.03.27
android graphic architecture  (0) 2020.03.27
android ART GC Log  (0) 2020.03.27
android keyboard show on the web  (0) 2020.03.27