어떤 Context를 써야 하나요?



reference:

https://medium.com/@ali.muzaffar/which-context-should-i-use-in-android-e3133d00772c 





 Context

안드로이드 앱 개발을 하다보면  Context에 접근할 일이 많습니다. 그런데, Activity가 아닌 다른 클래스에서 Context를 접근해야 한다면, (물론 인자, 필드 주입 등으로 해결 가능하지만) getContext() 같은 메소드를 호출해야만 합니다. 

Context가 무엇일까요?

Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It
allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.


위는 Context 클래스의 주석입니다. 어플리케이션의 환경, Intent, Activity 이동 등의 행위를 할 수 있는 클래스라고 합니다. 개발하면서 여러분들도 생각한 그런 역할과 크게 다르지 않은 것을 알 수 있습니다.

그런데 문제는 getContext() 뿐 아니라 getBaseContext(), getApplicationContext() 등 다양한 Context가 있다는 것이죠. 잘 못된 Context는 메모리 누수를 발생시킬 수 있습니다.


Context의 종류

Context
| — ContextWrapper
|— — Application
| — — ContextThemeWrapper
|— — — — Activity
| — — Service
|— — — IntentService

Context는 위와 같은 구조를 가집니다. 사실 우리가 부르는 Context는 부르기에 따라서 Application일 수도 있고, Activity일 수도 있고 Service일 수도 있는 것입니다. Context 자체는 사실상 제공해주는 것이 없고, 그것을 상속받은 자식이 모든 행위를 하게 됩니다.

한 예로, ContextThemeWrapper를 보면 다음과 같은데,  전부 theme이 적용된 리소스를 제공하는 것을 알 수 있습니다. 즉, theme과 관련된 작업을 하기 위해서는 이 클래스를 사용해야 한다는 의미입니다.

@Override
public Resources getResources() {
if (mResources != null) {
return mResources;
}
if (mOverrideConfiguration == null) {
mResources = super.getResources();
return mResources;
} else {
Context resc = createConfigurationContext(mOverrideConfiguration);
mResources = resc.getResources();
return mResources;
}
}

@Override public Resources.Theme getTheme() {
if (mTheme != null) {
return mTheme;
}
mThemeResource = Resources.selectDefaultTheme(
mThemeResource,
getApplicationInfo().targetSdkVersion);
initializeTheme();
return mTheme;
}
@Override public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(
getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}

반대로 getApplicationContext() 등의 메소드로 화면의 theme 작업을 하면 적용되지 않습니다. 왜냐면 앞서 언급된 바와 같이 theme은 그 화면에 종속된 특성이며, application context는 말그대로 application-wide한 context이기 때문입니다.


 Memory Leak

잘못된 Context 호출이 어떻게 메모리 누수를 발생시킬까요. 

Context는 40여종의 직/간접적 children을 소유하고 있습니다. 잘못된 Context는 필요치 않은 children의 reference 까지 같이 딸려오게 되므로 메모리가 누수되는 것은 어찌보면 당연합니다. 그래서 우리는, 화면에 종속된 작업을 하는 경우에 getBaseContext() 같은 메소드 호출을 피해야 하는 것입니다. 당연히 해당 Activity or Fragment 등을 호출하는 것이 가장 좋겠죠.


 getApplicationContext()

어떤 경우에 getApplicationContext() 를 호출해야 하는지 생각해 봅시다. 말그대로 application 수준의 context 입니다. 스스로 체크해봅시다. (두 경우 모두 사실 같은 이야기 같네요)


1. 화면과 관계없는 long living object에서 접근하고자 하는가

예를 들어 Activity가 바뀌거나 죽은 뒤에도 계속 수행되는 Thread가 만약 ContextThemeWrapper(Activity or Fragment etc.)를 가지고 있는다면 당연히 메모리 누수.


2. Theme이 적용되어야 하는 작업을 하려고 하는가? 

예를 들어 View를 inflate하고 drawable에 tint를 먹인다던가 하는 따위의 작업은 application context가 사용되어선 안됨. 화면에 종속된 View 작업은 왠만하면 피하자.


getThemedContext()

이 메소드는 ActionBar와 연관이 있습니다. ActionBar에 연관된 theme을 적용한 View를 적용하고 싶을 때 사용합니다. ActionBar에 새로운 text나 icon등을 적용하고 싶을 때 사용된다고 생각할 수 있겠습니다. 


getContext()

ActionBar와 상관 없이 View를 조작하고자 할 때 사용됩니다. 심지어 long living object에서도 사용될 수 있지만, WeakRefernce로 사용되는 것을 권장합니다. 혹은 AlertDialog를 만들 때 쓰이기도 합니다. AlertDialog는 서로 다른 theme이 적용되기도 하는데, 이 경우에 사용됩니다. 위 언급된 바와 같이 theme과 연관이 깊습니다. 

Dark theme을 사용하다가 light theme을 적용하고 싶은 경우를 생각해 봅시다. 물론 매 생성마다 setColor 류의 메소드를 호출하여 일일히 구현해도 되지만, 아래 처럼 사용할 수도 있습니다.

ContextThemeWrapper contextThemeWrapper = 
new ContextThemeWrapper(getActivity(),
android.R.style.Theme_DeviceDefault_Light_Dialog);
LayoutInflater inflater = getActivity()
.getLayoutInflater()
.cloneInContext(contextThemeWrapper);

Context에 원하는 theme을 부여하여 inflate하는 예제입니다. 아래처럼 AlertDialog에 적용할수도 있겠지요.
AlertDialog.Builder builder = 
new AlertDialog.Builder(contextThemeWrapper);


Conclusion

이상으로 올바른 Context를 부르는 방법에 대해서 간략하게 알아봤습니다. 우리의 궁극적인 목표는 적절한 Context를 사용함으로써 불필요한 reference를 줄여 메모리 누수를 방지하는 것입니다. 

'programming > android' 카테고리의 다른 글

View - Widget  (0) 2018.08.23
Companion Objects in Kotlin  (0) 2018.04.08
기본적인 메모리 관리  (0) 2018.03.06
Splash 화면 구성하기  (3) 2018.02.20
Kotlin Coroutines #2  (0) 2018.01.28

메모리 관리하기



reference: 

https://blog.aritraroy.in/everything-you-need-to-know-about-memory-leaks-in-android-apps-655f191ca859





 GC?

JVM의 GC에 대해서 간략하게 정리를 해보겠습니다. 안드로이드 소스를 자바(혹은 코틀린)로 작성을 하지만, GC에 대해 자세히 공부해본 적이 없단 것은 살짝 부끄러운 부분입니다. 그래서 여기서 간략하게 정리하겠습니다.




어떤 자바 어플리케이션이든, 처음에 프로그램이 시작되고 객체를 생성하는 과정이 생길 것입니다. 그 처음 생성되는 객체는 GC의 root object가 됩니다. 그 root object로부터 또 다른 객체들이 생성되고 직/간접적으로 참조될 것입니다. 그 object들을 트리 구조로 관리하게 됩니다. 메모리 트리가 되는 것이죠.

GC는 이 트리의 root로 부터 순회하면서 방문되지 않는 객체들이 생기게 될텐데요, 그것이 바로 GC의 대상이 됩니다. 어떤 객체도 참조하고 있지 않은 메모리가 회수 대상이 되는 것이죠. 상당히 상식적이라고 생각할 수 있겠습니다.


 메모리 누수?

메모리가 누수된다는 의미는 더이상 쓸모가 없는 메모리를 계속해서 참조하고 있다는 것입니다. 사용 되지 않는 메모리임에도 불구하고 계속해서 어플리케이션으로부터 참조가 된다면, GC의 대상이 아니게 되기 때문에 불필요하게 메모리를 차지하겠죠. 바이트 단위로 메모리를 정말 타이트하게 메모리를 관리하기란 큰 프로젝트일수록 아무리 전문가라도 버거운 일일 것 같습니다. 하지만 기본적인 메모리 누수는 당연히 이루어져야 하는 일이겠죠? 

GC는 앱의 사용 메모리가 계속 증가하는 양상을 보인다면 short GC를 하게 되는데, 앱과 병렬로 진행되며 소요시간이 크지 않아 성능에 주는 영향이 미미합니다. 그럼에도 불구하고 정말 많은 메모리가 쓰이기 시작하면 "stop-the-world" GC가 수행됩니다. 이름이 말해주듯, 모든 동작을 멈추고 메모리를 회수하는 작업만 하게 됩니다. 어플리케이션 성능에 상당한 영향을 미칠 것입니다.


 어떻게 메모리 누수를 감지할까?

Android studio에서 기본적인 메모리 모니터를 제공하는데, 런타임에서 사용되는 메모리를 그래프 형식으로 시각화 해주기 때문에 상당히 유용합니다. 아마 거의 모든 개발자들이 이미 알고 있는 부분일 것입니다. 하지만 어떤 메모리가 많이 잡아먹고 있는지 분석하기 위해서는 메모리 덤프를 통해야 할텐데, 상당히 시간을 많이 소요하는 작업일 수 있습니다. 그런 부분을 도와주는 라이브러리가 있습니다. 

LeakCanary(https://github.com/square/leakcanary)가 그런 역할을 대신 수행해 줍니다. (역시 Square군요..)

이 라이브러리는 어플리케이션과 같이 동작하면서 필요할 때 메모리를 덤프 해주고, 메모리 누수가 일어날 것 같은 상황을 예측하고 원인 분석 및 스택 트레이스를 덤프 해주는 기능도 합니다. 굉장히 좋은 라이브러리 같네요. 조만간 사용해보고 새 블로그를 작성해보겠습니다.


흔한 메모리 관리 실패 사례

안드로이드(를 비롯해서 다른 플랫폼도 어쩌면 동일할 수 있는) 클라이언트에서 발생하는 메모리 실패 사례를 살펴보겠습니다. 기본적인 아이디어는 위에서 언급한 바와 마찬가지로, 변경되거나 더 이상 사용되지 않는 객체를 참조하는 일을 저지르는 사례들 입니다.

1. Unregistered Listeners

어떤 모듈로 부터 데이터를 얻어온 후 뷰에 적용하여 보여주는 역할이 클라이언트 대부분의 작업일 것입니다. 그럴 때 사용하는 것이 listener인데요, 그 데이터를 사용하는 액티비티가 죽거나 더이상 사용되지 않음에도 모듈이 계속 갖고 있는 때입니다. 굉장히 자주 일어나는 실수 중에 하나이며 놓치기 쉬울 뿐더러 발생하는 누수도 상당합니다.

해결법은 간단합니다. 액티비티의 onDestroy()에서 그 모듈로부터 자신을 listener 등록 해제 하는 것입니다.

2. Inner Classes

클래스 안의 클래스를 작성하는 경우는 많습니다. 액티비티에서 새로운 thread나 AsyncTask를 수행한다고 가정하면, 아래와 같은 코드가 되겠죠:

public class BadActivity extends Activity {
    private TextView mMessageView;
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_bad_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
        new LongRunningTask().execute();
}
    private class LongRunningTask extends AsyncTask<Void, Void, String> {
        @Override
protected String doInBackground(Void... params) {
return "Am finally done!";
}
        @Override
protected void onPostExecute(String result) {
mMessageView.setText(result);
}
}
}

문제가 딱히 발견되지 않을 수도 있겠습니다만, AsyncTask가 시간이 많이 걸리는 작업을 한다고 가정한다면? 위의 코드로는 액티비티보다 AsyncTask가 더 오래 살아있을 수도 있습니다. 그런 경우, AsyncTask는 더이상 데이터를 뿌려줄 액티비티가 없음에도 불구하고 불필요하게 액티비티의 TextView를 잡고있게 되는 것입니다. 상상만 해도 끔찍하네요.

이런 경우, 참조하는 객체를 WeakReference로 갖고있으면 간단히 해결할 수 있겠습니다. (GC가 수행되면 수거되는 메모리 대상)

3. Anonymous Class

2번 경우와 똑같습니다. 익명 클래스는 아주 유용하지만 메모리 누수에 쉽게 노출됩니다. 흔히 예를 들자면, Retrofit으로 API를 호출하고 그에 대한 응답 Callback을 작성할 것인데, 이를 익명 클래스로 작성하는 경우가 있을 것입니다.

public class MoviesActivity extends Activity {
    private TextView mNoOfMoviesThisWeek;
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_movies_activity);
mNoOfMoviesThisWeek = (TextView) findViewById(R.id.no_of_movies_text_view);
        MoviesRepository repository = ((MoviesApp) getApplication()).getRepository();
repository.getMoviesThisWeek()
.enqueue(new Callback<List<Movie>>() {

@Override
public void onResponse(Call<List<Movie>> call,
Response<List<Movie>> response) {
int numberOfMovies = response.body().size();
mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
}
                    @Override
public void onFailure(Call<List<Movie>> call, Throwable t) {
// Oops.
}
});
}
}

onResponse()가 호출되기 전에 액티비티가 무조건 살아야 있어야만 할 것입니다. 그래야 메모리가 누수되지 않겠죠? 2번과 똑같은 상황입니다. 

4. Wrong Contexts

안드로이드에서 Context는 매우 유용하게 여기저기서 사용되는 객체입니다. 그러나 어플리케이션, 액티비티, 서비스에서 모두 같은 Context를 사용하는 것이 아닙니다.

이를 잘 이해하고 적절한 Context를 참조하여 엉뚱한 메모리 누수가 발생하지 않을 필요가 있겠습니다.

이 부분 다음 포스트에서 작성해보도록 하겠습니다. 


Conclusion

위 사례들에서 관통되는 공통적인 이슈는, 액티비티와 그 상호작용하는 모듈의 lifecycle이 서로 일치하지 않는 데서 오는 것으로 볼 수 있습니다. AsyncTask나 Retrofit 모듈 모두, 액티비티에서 발생하는 상황을 모르고 있기 때문에 엉뚱한 메모리를 참조하거나 심지어 크래쉬까지 발생시키는 것입니다. 결론적으로, 액티비티가 회전되거나 파괴될 때 그 상황을 모듈에 적절히 알려야할 필요가 있습니다.

최근 구글에서 제안한 설계 라이브러리 중에서 ViewModel, LiveData 등은 모두 이러한 점을 보완하기에 적절한 것 같습니다. 위 언급과 마찬가지로 모든 문제는 모듈들이 액티비티의 lifecycle을 인지하지 못하는 것에서 비롯됩니다. 구글은 이 라이브러리들을 lifecycle-aware라는 표현으로 수식하였는데요, 말 그대로 모듈들에게 액티비티의 현재 lifecycle을 인지하도록 강제하여 위와 같은 사태를 좀 더 효율적으로 관리할 수 있을 것 같습니다.


'programming > android' 카테고리의 다른 글

Companion Objects in Kotlin  (0) 2018.04.08
어떤 Context를 써야 하나요?  (0) 2018.03.26
Splash 화면 구성하기  (3) 2018.02.20
Kotlin Coroutines #2  (0) 2018.01.28
Kotlin Coroutines #1  (1) 2018.01.27

Splash 화면 구성하기



reference: 

https://android.jlelse.eu/the-complete-android-splash-screen-guide-c7db82bce565




 방법

 1. Launcher Theme (The good)

  1. 2. Launcher Theme with a Dedicated Splash Activity (The okay)
  2. 3. Timers (The bad)
  3. 4. Smart Timers (The ugly)


 Launcher Theme

Activity가 onCreate() 조차도 불러지기 전, 

즉, 메모리에 앱이 올라가는 과정에다가 사용자가 지정한 화면을 넣는다는 것이 키포인트입니다.

앱 theme의 attributes 중에 windowBackground을 override 하는 것인데요, 

그 말인 즉, windowBackground라는 친구는 말그대로 activity 화면 뒤의 백그라운드를 뜻하는 것 같습니다.

windowBackground 위에 activity의 layout이 그려지기 때문에, 

그 백그라운드에 내가 원하는 화면을 그려넣겠다는 의미죠.

아주 단순합니다.


 Override windowBackground


<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Your AppTheme or other themes/styles here -->
<!-- The launcher theme. It sets the main window background to the launch_screen drawable -->
<style name=”AppTheme.Launcher”>
<item name=”android:windowBackground”>@drawable/launch_screen</item>
<!-- Optional, on Android 5+ you can modify the colorPrimaryDark color to match the windowBackground color for further branding-->
<!-- <item name="colorPrimaryDark">@android:color/white</item> -->
</style>
</resources>
view rawstyles.xml hosted with ❤ by GitHub

위 예제와 같이, AppTheme의 windowBackground를 지정해주고,

launcher activity의 테마를 위 테마로 지정해주기만 하면됩니다.

launch_screen의 이미지가 표시됩니다.



Conclusion


일반적이고 naive한 splash 구성 방법은 보통 timer나 animation을 통할 것 같은데요,

원본 포스트에서 위와 같이 좋은 방법, 나쁜 방법이라고 소개를 해놓은 점은 굉장히 자극적이라는 생각이 듭니다.

그래서 line-through 처리를 한 것이기도 하구요,

사실 원본 포스트에서도 밑 부분에는 각 방법마다 장단점을 작성해놓긴 했습니다.


프로젝트마다 splash 화면이 구성되는 동안, 

간단한 작업을 하거나 animation을 보여주는 경우가 있습니다.

launcher theme 방법의 경우 그것이 불가능합니다. 단순히 이미지를 표시하는 역할인 것이죠.

머무를 시간을 정한다던지,

animation을 표현한다든지,

login 등 간단한 request를 한다든지 그 어떤 행위랑은 연관이 없습니다.


따라서, splash 화면에서 간단한 작업(무거운 작업은 당연히 피해야겠죠)을 하고자 한다면, 

따로 화면을 구성하는 것이 더 적절하지 않나 생각합니다.


예를 들어, 

splash 화면을 뿌리는 동안 서버에 나의 상태를 요청하고, 

그상태에 따라 넘어가는 activity가 다른 경우가 있겠습니다.

물론 가능은 하겠지만, 하나의 activity에 splash가 종속되는 바람에 

launcher activity의 코드가 조금 장황해질 것 같다는 의견입니다.


따라서, 어떤 방법을 택할지는 본인의 판단하에 선택하면 될 것 같습니다 :)



'programming > android' 카테고리의 다른 글

어떤 Context를 써야 하나요?  (0) 2018.03.26
기본적인 메모리 관리  (0) 2018.03.06
Kotlin Coroutines #2  (0) 2018.01.28
Kotlin Coroutines #1  (1) 2018.01.27
Tools로 xml layoyt의 preview 제대로 표시하기  (0) 2018.01.22

+ Recent posts