android.view.WindowManagerGlobal: findViewLocked / android memory leak 해결하기 (근데 이제 fragment 를 곁들인)
android.view.WindowManagerGlobal: findViewLocked
🥺 activity 가 finish 상태일 때 dialog 를 dismiss() 하려고 할 때 생기는 오류
에러 발생 상황
- 로그인 화면에서 로그인 저장 체크박스에 체크를 하고 로그인 버튼을 바로 누르는 경우
- 체크를 하면 CustomToast 가 뜬다.
- 로그인 버튼을 누르면 홈 화면으로 이동한다. (LoginActivity.finish())
- 로그아웃하는 경우
- 로그아웃을 클릭하면 한번 더 확인하는 CustomConfirmDialog 가 뜬다.
- ‘네’ 를 선택하면 로그아웃 되어 로그인 화면으로 이동한다.
해결 방안
💡 activity 가 finish 되기 전에 dialog 를 dismiss 해야한다.
첫번째 해결책
activity 가 destroy 될 때 다이얼로그들을 dismiss() 했다.
(내 커스텀 다이얼로그들은 싱글톤 패턴으로 구현이 되어있다.)
CustomToast.getInstance().dismiss();
결과 : 실패
android.app.ActivityThread: performDestroyActivity 에러가 발생했다.
두번째 해결책
다이얼로그 액션 인터페이스를 정의할 때 안드로이드에서 제공하는 다이얼로그 인터페이스처럼 alertDialog 를 인자로 넘겨준다.
binding.logoutTextView.setOnClickListener(
v -> CustomConfirmDialogTest.getInstance().showDialog(mContext, "로그아웃하시겠습니까?",
new CustomConfirmDialogTest.ConfirmDialogAction() {
@Override
public void setPositiveAction(AlertDialog alertDialog) {
alertDialog.dismiss();
...
}
@Override
public void setNegativeAction() {/**/}
}));
결과 : CustomToast 는 해결할 수 없다. → 커스텀 토스트에서도 같은 문제가 발생할 확률이 있는데, 해결할 수 없다는 의미 ⇒ 실패
세번째 해결책
각 다이얼로그 클래스들이 싱글톤으로 구현이 되어있기 때문에
각 객체들에 대해서 alertDialog 가 null 인지 아닌지 판별이 가능하다.
(예전의 로직은 계속 new 되는 상태였기 때문에 판별 불가능, 다이얼로그 중복 호출 가능성 있었음)
public void dismissDialog(){
if (alertDialog != null){
alertDialog.dismiss();
}
}
해당 메소드를 activity 가 destroy 될 때 호출한다.
@Override
protected void onDestroy() {
super.onDestroy();
CustomConfirmDialog.getInstance().dismissDialog();
}
결과 : 성공
fragment memory leak
MVVM 패턴을 적용하고 나서 profiler 로 memory leak 을 확인했는데 몇몇 fragment 들이 제때 수거되지 않고 남아서 메모리 릭을 일으키는 것을 확인했다.
왜...?
MVVM 패턴을 적용하기 전에는 메모리 누수를 찾아볼 수 없었는데..
ViewModel 이 fragment 보다 생명주기가 길기 때문에 context 를 참조하지 않는것이 좋다고 해서 참조하지 않았고,
ViewBinding 도 오랫동안 참조될 가능성이 있다고 해서 context, viewBinding 도
생명주기에 맞춰서 모두 명시적으로 해제해주었는데 자꾸만 누수가 일어나서 답답했다.
특정 fragment 가 수거가 되질 않으니..fragment context인 activity context 도 GC 에게 수거되지 않았다.
그렇게 답답해 하다가 LeakCanary 를 알게 되었고
적용해 보았다. (LeakCanary 에 대한 것은 따로 글을 작성해보도록 하겠다.)
프로파일러로 확인하는 것보다 훨씬 직관적이고 알아보기 쉽게 되어있어서 편했다.
그래서 하나하나 살펴보는데 ConstraintLayout 과 Button 이 누수를 일으킨다고 하니까
이게 도대체 무슨 소리일까 미치고 팔짝 뛸 것 같았는데 자세히 보니까..
Dialog 가 떴었던 화면들이 계속해서 참조가 되고 있는 것을 알 수 있었다.
-> 특정 fragment 가 계속 남아있었던 이유도 그 화면에서는 무조건 dialog 가 뜨게 되어있기 때문이었다.
그렇다면 문제의 원인은 CustomDialog 에 있다는 것인데..
다이얼로그를 띄우려고 컨텍스트를 참조하긴 하는데…(이 컨텍스트는 FragmentActivity context 이고 아주 강한 참조라고 볼 수 있다.)
근데 인자를 통해서 받아오는데 뭐가 문제일까? 하다가
다이얼로그는 싱글톤으로 구현되어있고..alertDialog 가 static 으로 선언되어 있는 것을 확인했다.
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
alertDialog = builder.create();
이때 참조되는 context 를.. static으로 선언된 alertDialog 가 계속 참조하고 있으니까
다이얼로그가 떴었던 화면들(fragment)이 제대로 수거될 수 없는 것이 아닐까 하는 생각을 했다.
그래서 아래와 같이 alertDialog 를 null 로 만들어 주었다.
public void dismissDialog(){
if (alertDialog != null){
alertDialog.dismiss();
alertDialog = null;
}
}
All retained objects have been garbage collected
해결이 됐다!
내가 생각한 것이 맞았던 건지는.. 정확히 알 수는 없지만 일단 현재까지 메모리 누수를 확인할 수 없었다.
alertDialog 에 대해 좀 더 정확히 공부해야겠다고 느꼈고
그동안 말로만 들어왔던 context 공포..(제대로 관리하지 못하면 어떻게 되는지를)
를 몸소 깨닫게 되었다.
References
아래 글을 참고하여 작성 되었습니다.