2012년 6월 13일 수요일

View에 할당한 리소스 완전히 해제하기


앞선 포스트  Android 개발가이드 - Bitmap.recycle()은 가급적 호출해 주는 편이 좋다. 에서는 명시적인 오브젝트 해제의 필요성을 알아보았다. VM 을 사용하는 언어에서 코딩함에도, c 스타일의 리소스 해제를 왜 해주어야 하는지에 대한 내용이었다. (안드로이드 프레임웍의 문제 및 특수성, 한정적인 VM Heap size 와 같은 이유 등)

그러나 안드로이드의 내부 동작으로 인해 Bitmap.recycle() 과는 별개로 view 에서의 메모리 누수 위험은 항상 잠재해 있다(자세한 내용은 안드로이드 내부 구현에 대한 이야기로 이어져야 하므로 생략). 결국 가장 안전한 방법은, view 에 할당된 모든 것을 초기화 해 버리는 것이다.

그럼 어떻게 할 것인가? 화면에 보이는 수많은 뷰들에 대해 일일히 null을 할 것인가?
이 점에 착안하여 아래와 같은 지저분한 트릭을 고안해 보았다.
왜 지저분하냐면, 정리 과정에 어느 정도의 성능 저하가 우려되기 때문이다.
주요 성능저하의 원인은 반복되는 재귀 호출 및 Root View 부터의 완전 트리 탐색 로직 때문이다.
게다가 마지막의 System.gc() 콜은 설명이 필요없이 지저분하다. (자세한 내용은 API 문서 참고 http://developer.android.com/reference/java/lang/System.html#gc() - GC호출은 GC를 보장하지 않음)

그러므로 가벼운 앱에서는 이 내용은 고민하지 않아도 무방하다.
View 갯수가 많은 화면을 Tab 혹은 ViewFlipper 등으로 구현한 경우와 같이,
메모리가 절박한 경우라면 이 트릭이 도움이 될 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.ImageView;
 
/**
 * Utility class to help unbinding resources consumed by Views in Activity.
 *
 * @author Hwan Jo(nimbusob@gmail.com)
 */
public class ViewUnbindHelper {
    /**
     * Removes the reference to the activity from every view in a view hierarchy
     * (listeners, images etc.). This method should be called in the onDestroy() method
     * of each activity.
     * This code may stinks, but better than worse - suspiciously, Android framework
     * does not free resources immediately which are consumed by Views and this leads to
     * OutOfMemoryError sometimes although there are no user mistakes.
     *
     * @param view View to free from memory
     */
    public static void unbindReferences(View view) {
        try {
            if (view != null) {
                unbindViewReferences(view);
                if (view instanceof ViewGroup) {
                    unbindViewGroupReferences((ViewGroup)view);
                }
            }
        } catch (Exception ignore) {
            /* whatever exception is thrown just ignore it because a crash is
             * always worse than this method not doing what it's supposed to do
             */
        }
    }
 
    /**
     * Removes the reference to the activity from every view in a view hierarchy
     * (listeners, images etc.). This method should be called in the onDestroy() method
     * of each activity.
     * This code may stinks, but better than worse - suspiciously, Android framework
     * does not free resources immediately which are consumed by Views and this leads to
     * OutOfMemoryError sometimes although there are no user mistakes.
     *
     * @param view View to free from memory
     */
    public static void unbindReferences(Activity activity, int viewID) {
        try {
            View view = activity.findViewById(viewID);
            if (view != null) {
                unbindViewReferences(view);
                if (view instanceof ViewGroup) {
                    unbindViewGroupReferences((ViewGroup)view);
                }
            }
        } catch (Exception ignore) {
            /* whatever exception is thrown just ignore it because a crash is
             * always worse than this method not doing what it's supposed to do.
             */
        }
    }
 
    private static void unbindViewGroupReferences(ViewGroup viewGroup) {
        int nrOfChildren = viewGroup.getChildCount();
        for (int i = 0; i < nrOfChildren; i++) {
            View view = viewGroup.getChildAt(i);
            unbindViewReferences(view);
            if (view instanceof ViewGroup) {
                unbindViewGroupReferences((ViewGroup)view);
            }
        }
 
        try {
            viewGroup.removeAllViews();
        } catch (Exception ignore) {
            // AdapterViews, ListViews and potentially other ViewGroups don't support the removeAllViews operation
        }
    }
 
    private static void unbindViewReferences(View view) {
        // Set everything to null (API Level 8)
        try {
            view.setOnClickListener(null);
        } catch (Exception ignore) {}
 
        try {
            view.setOnCreateContextMenuListener(null);
        } catch (Exception ignore) {}
 
        try {
            view.setOnFocusChangeListener(null);
        } catch (Exception ignore) {}
 
        try {
            view.setOnKeyListener(null);
        } catch (Exception ignore) {}
 
        try {
            view.setOnLongClickListener(null);
        } catch (Exception ignore) {}
 
        try {
            view.setOnClickListener(null);
        } catch (Exception ignore) {}
 
        try {
            view.setTouchDelegate(null);
        } catch (Exception ignore) {}
 
        Drawable d = view.getBackground();
        if (d != null) {
            try {
                d.setCallback(null);
            } catch (Exception ignore) {}
        }
         
        if (view instanceof ImageView) {
            ImageView imageView = (ImageView)view;
            d = imageView.getDrawable();
            if (d != null) {
                d.setCallback(null);
            }
             
            if (d instanceof BitmapDrawable) {
                Bitmap bm = ((BitmapDrawable)d).getBitmap();
                bm.recycle();
            }
             
            imageView.setImageDrawable(null);
        } else if (view instanceof WebView) {
            ((WebView)view).destroyDrawingCache();
            ((WebView)view).destroy();
        }
 
        try {
            view.setBackgroundDrawable(null);
        } catch (Exception ignore) {}
         
        try {
            view.setAnimation(null);
        } catch (Exception ignore) {}
 
        try {
            view.setContentDescription(null);
        } catch (Exception ignore) {}
 
        try {
            view.setTag(null);
        } catch (Exception ignore) {}
    }
}

위 코드를 Activity 화면의 onDestroy 에서 호출해 주면 확실히 메모리 절약 효과가 있다. 우리가 미처 신경쓰지 못한 내부 callback 등까지 확실히 제거해 주므로 View 가 올바른 GC의 대상이 되기 때문이다. 그러나 뷰가 무지 많은 activity 에서는 모든 view에 위의 일을 일일이 해 주기도 짜증난다.

그래서 나는 모든 view 들에 대한 처리는 super 에 맡기고 화면의 코드에만 좀더 집중하기 위해, 위의 역할을 해 주는 별도의 super activity 를 아래와 같이 만들어서 사용하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import android.app.Activity;
import android.os.Bundle;
 
/**
 * Wraps global attributes of all screens(Activities).
 *
 * @author Hwan Jo(nimbusob@gmail.com)
 */
public abstract class BaseActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
 
    /* (non-Javadoc)
     * @see android.app.Activity#onDestroy()
     */
    @Override
    protected void onDestroy() {
        /* This chain of invocation is like a horseshit, but necessary since Android
         * does not free Views and any View related resources immediately which are
         * declared in Activity. Also the JavaDoc said GC is not guaranteed but occurs ASAP.
         * (By watching 10000 times of GC request, most of them occur in 100ms)
         */
        ViewUnbindHelper.unbindReferences(this, getContentViewId());
        System.gc();
 
        super.onDestroy();
    }
 
    protected abstract int getContentViewId();
}

위의 super activity 를 상속받는 자식 activity 는 getContentViewId() 를 아래와 같이 구현해 주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import net.oomtest.R;

import android.os.Bundle;
import android.widget.ListView;
 
/**
 * Main entry point of application.
 *
 * @author Hwan Jo(nimbusob@gmail.com)
 */
public class MainActivity extends BaseActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.setContentView(R.layout.main);
    }
 
    @Override
    protected int getContentViewId() {
        return R.id.mainViewContainer; // 최상위 view 에 부여한 id
    }
}


그러나 역시 최선은 위의 코드들이 필요없어 지도록 안드로이드 프레임웍이 개선되는 방향일 것이다.

댓글 없음:

댓글 쓰기