WebView는 여러 프로세스에서 네이티브 코드를 실행하여 Android 앱에서 웹 콘텐츠를 렌더링합니다. WebView 인스턴스를 관리되지 않은 상태로 두면 메모리 누수, 메모리 부족 (OOM) 비정상 종료, 앱 성능 저하가 발생할 수 있습니다.
이 문서에서는 WebView 멀티 프로세스 메모리 모델을 설명하고, 누수를 방지하기 위해 수명 주기를 올바르게 관리하는 방법을 설명하며, 메모리 문제를 진단하기 위한 실용적인 워크플로를 제공합니다.
WebView 메모리 아키텍처 이해
WebView 메모리를 효과적으로 관리하려면 Android가 웹 콘텐츠에 리소스를 할당하는 방식을 이해해야 합니다.
다중 프로세스 실행: Android 8.0 (API 수준 26) 이상에서
WebView는 여러 프로세스에서 웹 콘텐츠를 앱의 핵심 기능과 분리합니다 (RAM이 부족한 기기에서는 단일 프로세스로 대체될 수 있음).- 호스트 (브라우저) 프로세스:
Activity및 Java 또는 Kotlin 코드가 실행되는 기본 앱 프로세스입니다. - 격리된 렌더러 프로세스: HTML 및 CSS를 파싱하고 JavaScript를 실행하며 웹페이지를 렌더링하는 별도의 샌드박스 처리된 프로세스(
SandboxedProcessService)입니다.
- 호스트 (브라우저) 프로세스:
네이티브 메모리 사용량: 렌더링된 그래픽, DOM 트리, JavaScript 런타임 메모리를 비롯한 대부분의
WebView메모리는 Java 힙이 아닌 네이티브 메모리에 할당됩니다. Java 힙 덤프 (.hprof)는 가벼운 Java 래퍼 객체만 표시하며 웹 콘텐츠에서 사용하는 실제 메모리를 캡처하지 않습니다.네이티브 메모리의 시스템 영향: 앱의
maxHeap한도로 제한되고OutOfMemoryError로 빠르게 실패하는 자바 힙 할당과 달리 네이티브 메모리는 기가바이트 단위로 자동으로 증가할 수 있습니다. 해제되지 않은 네이티브 메모리가 실제 RAM과 스왑 공간 (zRAM)을 채우면 Android의 Low Memory Killer (LMK)가 백그라운드 프로세스를 종료하여 메모리를 회수하기 시작합니다. 이로 인해 전반적인 기기 멀티태스킹 성능이 저하되고 결국 포그라운드 앱이 종료됩니다.
WebView 수명 주기 관리
적절한 수명 주기 관리는 메모리 누수를 방지하는 데 매우 중요합니다. 레이아웃에서 WebView를 삭제하거나 Activity를 완료하면 메모리가 자동으로 확보된다고 가정하는 것은 흔히 저지르는 실수입니다.
Java 컨텍스트 참조와 네이티브 렌더링 리소스를 모두 완전히 정리하려면 호스트 구성요소의 수명 주기 (예: onDestroy())에서 해체 시퀀스를 명시적으로 조정하여 활성 페이지 실행을 중지하고, 컨테이너에서 뷰를 분리하고, 네이티브 결합을 해제해야 합니다.
WebView 인스턴스 정리
Activity 또는 Fragment가 폐기될 때 정상적으로 종료하고 리소스를 해제하려면 다음을 수행합니다.
- 상위 컨테이너 (
ViewGroup)에서WebView를 삭제합니다. - 활성 로드를 중지하고 탐색 기록을 삭제합니다.
destroy()를 호출합니다.null에 대한 참조를 지웁니다.
다음 예는 WebView를 올바르게 정리하는 방법을 보여줍니다.
Kotlin
override fun onDestroy() { myWebView?.let { // Remove the WebView from its parent ViewGroup. (it.parent as? ViewGroup)?.removeView(it) // Stop active loading and clear history. it.stopLoading() it.clearHistory() // Destroy the instance. it.destroy() } myWebView = null super.onDestroy() }
자바
@Override
protected void onDestroy() {
if (myWebView != null) {
// Remove the WebView from its parent ViewGroup.
if (myWebView.getParent() instanceof ViewGroup) {
((ViewGroup) myWebView.getParent()).removeView(myWebView);
}
// Stop active loading and clear history.
myWebView.stopLoading();
myWebView.clearHistory();
// Destroy the instance.
myWebView.destroy();
}
myWebView = null;
super.onDestroy();
}
소멸 후 메모리 이해하기
destroy()를 호출하면 시스템은 Activity 컨텍스트를 해제하고 뷰 계층 구조를 정리하며 웹 백그라운드 작업을 중지합니다. 그러나 프로세스의 실제 메모리 (상주 세트 크기)가 WebView 이전 기준치로 즉시 떨어지지 않는 것을 확인할 수 있습니다.
이는 정상적인 동작입니다. 네이티브 런타임 캐시, 공유 라이브러리, 할당된 메모리 페이지는 운영체제가 회수하거나 프로세스가 종료될 때까지 프로세스에 상주합니다. destroy()의 기본 목표는 사용자가 웹 기반 화면을 탐색할 때 누적되는 Activity 메모리 누수를 방지하는 것입니다.
주요 디버깅 측정항목
WebView 메모리 소비를 분석할 때는 다음 측정항목에 집중하세요.
Resident Set Size (RSS): 공유 코드 및 라이브러리를 포함하여 프로세스에 매핑된 총 실제 RAM입니다 (Android 스튜디오 프로파일러에서 Total로 표시됨).
익명 RSS (RssAnon): 디스크의 파일로 지원되지 않는 프로세스에서 직접 할당한 메모리입니다 (예: 네이티브 힙 및 JavaScript 런타임 할당). 이는 웹 콘텐츠의 기본 메모리 비용을 나타냅니다(Android 스튜디오 프로파일러에서 Allocated로 표시됨).
비공개 메모리 사용량 (PMF): 익명 RSS와 스왑(zRAM)의 합계입니다. PMF는 앱이 시스템에 부과하는 실제 제거 불가능한 메모리 부담을 반영합니다.
브라우저 PMF와 렌더러 PMF: 앱의 기본 프로세스에서 사용하는 메모리와 격리된 렌더러 프로세스에서 사용하는 메모리입니다. 무거운 웹 콘텐츠는 주로 렌더기 프로세스에서 급증을 일으킵니다.
Live Object Counts (
WebViews,Activities,Views): 메모리에 보관된 활성 UI, 컨텍스트,WebView인스턴스의 수입니다. 이를 추적하면 메모리 증가가 유지된 Java 참조로 인해 발생하는지 아니면 네이티브 전용 할당으로 인해 발생하는지 식별할 수 있습니다.비공개 기타 및 네이티브 힙:
dumpsys meminfo에서 네이티브 C/C++ 할당 및 맞춤 메모리 매핑 (예: ChromiumPartitionAlloc또는 삽입된 자바스크립트 런타임 힙)은 자바 힙이 아닌 네이티브 힙 및 비공개 기타에 표시됩니다.
프로세스 메모리 카운터 및 카테고리에 관한 자세한 내용은 프로세스 메모리 용어집을 참고하세요.
실용적인 진단 워크플로
WebView는 여러 프로세스에서 작동하고 네이티브 메모리를 할당하므로 다음 도구와 기법을 사용하여 메모리 사용량을 검사합니다.
프로파일링 및 진단 도구
메모리 할당을 검사하고 누수를 진단하려면 다음 도구를 사용하세요.
Android 스튜디오 메모리 프로파일러: 메모리 프로파일러를 사용하여 네이티브 할당을 시각화하고, 시간 경과에 따른 메모리 카테고리를 추적하고, 화면 전환 전반에서
Activity누수를 감지합니다.Perfetto를 사용한 메모리 추적: Perfetto를 사용하여 시스템 수준 메모리 카운터 (예: RSS 및 익명 RSS)를 기록하여 전반적인 메모리 증가를 관찰합니다.
WebView네이티브 엔진 할당은 Perfetto의 힙 프로파일링 도구에서 호출 스택을 생성하지 않습니다. Chrome DevTools를 사용하여 웹 콘텐츠 내의 JavaScript 힙 스냅샷과 DOM 할당을 검사합니다.
라이브 객체 수 검사
메모리 증가가 유지된 Java 프레임워크 객체 (예: UI 구성요소) 또는 네이티브 할당으로 인해 발생하는지 확인하려면 dumpsys meminfo의 Objects 섹션을 검사합니다.
adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"출력에 활성 객체 수가 표시됩니다.
Objects
Views: 142 ViewRootImpl: 1
AppContexts: 3 Activities: 1
Assets: 12 AssetManagers: 0
Local Binders: 32 Proxy Binders: 45
Parcel memory: 15 Parcel count: 30
Death Recipients: 2 WebViews: 1
이 섹션에는 활성 프레임워크 객체, IPC 핸들, Parcel 할당 수가 표시됩니다. WebView 진단의 경우 주로 Activities 및 WebViews에 중점을 둡니다.
타겟 사용자 상호작용 (예: 웹 화면 열기 및 닫기)을 반복적으로 실행하고 개수를 비교합니다.
인스턴스 누수: 탐색할 때마다
WebViews또는Activities가 증가하고 기준선으로 돌아가지 않으면 앱에서 JavaWebView인스턴스 또는 호스트Activity가 누수되는 것입니다 (예:ViewGroup.removeView()가 누락되었거나 리스너 참조가 유지되었기 때문). 누출된Activity는 전체 뷰 트리와 디코딩된 이미지 리소스를 메모리에 고정하므로 반복 방문하면 Java 힙이 빠르게 소진되고OutOfMemoryError비정상 종료가 발생합니다.네이티브 또는 DOM 누수:
WebViews및Activities가 일정하게 유지되는 동안 총 프로세스 RSS 및 Private Other가 계속 증가하면 누수는 출시되지 않은 네이티브 리소스, DOM 요소 또는 JavaScript 엔진 바인딩에서 발생합니다. 이러한 할당은 네이티브 메모리에 상주하고 ART 가비지 컬렉터를 우회하므로 표준 Java 누수 감지 도구에 표시되지 않으며 운영체제가 앱을 종료할 때까지 계속 누적됩니다.
CLI를 사용하여 격리된 렌더러 프로세스 프로파일링
앱의 패키지 이름으로 dumpsys meminfo를 실행하면 기본 호스트 프로세스의 메모리만 출력됩니다. 웹페이지가 렌더링되는 격리된 렌더기 프로세스를 검사하려면 다음 단계를 따르세요.
분리된 렌더러 서비스의 프로세스 ID (PID)를 찾습니다.
adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"출력에 격리된 프로세스 레코드와 PID RENDERER_PID가 표시됩니다 (예:
22155).Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}PID를 사용하여 렌더기 프로세스의 메모리 분석을 검사합니다.
adb shell dumpsys meminfo <var>RENDERER_PID</var>호스트 앱 프로세스를 검사하여 브라우저 측 공간을 평가합니다.
adb shell dumpsys meminfo <var>PACKAGE_NAME</var>
메모리 맵 및 할당 검사
익명 메모리를 차지하는 네이티브 하위 시스템 또는 할당자를 확인하려면 프로세스 메모리 맵을 검사합니다.
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"다음 표에는 일반적인 익명 메모리 태그와 메모리 증가와의 관련성이 나와 있습니다.
| 메모리 태그 | 하위 시스템 | 앱 및 웹 콘텐츠와의 관련성 | 일반적인 메모리 증가 원인 |
|---|---|---|---|
[anon:partition_alloc] |
Chromium PartitionAlloc | WebView의 DOM 트리, 렌더링 버퍼, V8 JavaScript 힙, WebAssembly 실행에 대한 할당입니다. |
예 (높음): 무거운 웹페이지, 미디어가 많은 DOM을 로드하거나 삭제된 WebView 인스턴스에서 destroy()를 호출하지 못하면 이 태그가 직접 확장됩니다. |
[anon:scudo...] 또는 [anon:libc_malloc] |
Android 네이티브 힙 할당자 (Scudo / jemalloc) | NDK 라이브러리, JNI 브리지, 네이티브 그래픽 파이프라인에서 사용하는 일반 C/C++ 네이티브 할당입니다. | 예 (보통~높음): 네이티브 JNI 래퍼 또는 서드 파티 C++ 종속 항목이 탐색 전반에서 해제되지 않은 할당을 유지하는 경우 증가가 발생합니다. |
[anon:...](예: [anon:quickjs_heap...]) |
커스텀 스크립팅 또는 네이티브 런타임 | 삽입된 JavaScript 엔진, 커스텀 WebAssembly 런타임 또는 커스텀 네이티브 버퍼 풀 | 예 (컨텍스트에 따라 다름): 네이티브 뷰와 함께 스크립팅 엔진을 실행하고 런타임 결합을 정리하지 못하는 하이브리드 앱에서 흔히 발생합니다. |
인앱 메모리 API의 제한사항
인앱 메모리 API (예: Debug.getMemoryInfo 또는 ActivityManager.getProcessMemoryInfo)는 호출 프로세스만 측정합니다.
멀티 프로세스 모드에서는 이러한 API가 격리된 렌더기 프로세스에서 사용하는 메모리를 캡처할 수 없습니다. 정확한 총 메모리 평가를 위해서는 dumpsys meminfo, Perfetto 또는 Android 스튜디오 프로파일러와 같은 시스템 도구를 사용하세요.
하이브리드 앱의 높은 메모리 분류
반복되는 WebView 상호작용 (예: 웹 링크 열기 또는 웹 기반 피드 탐색) 중에 설명할 수 없는 메모리 증가를 진단할 때는 다음 분류 워크플로를 사용하여 누수가 Java 레이어에서 발생하는지 아니면 네이티브 엔진에서 발생하는지 격리합니다.
누출 유형 분리 (자바와 네이티브): 반복적인 사용자 전환 (예: 웹 기사 열기 및 닫기, 피드 스와이프) 전후에
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"를 실행합니다.- 관찰:
Activities및WebViews수가 일정하게 유지되면 (예: 활성 인스턴스 1~2개) 앱에서Activity컨텍스트 또는 자바WebView인스턴스가 누출되지 않습니다.
- 관찰:
상호작용 전반의 메모리 델타 측정 (시계열 추적): 여러 사용자 상호작용 전반에서
dumpsys meminfo스냅샷을 캡처하여 전환당 할당률을 계산합니다.- 관찰: Java 힙은 제한되고 정상 상태를 유지하지만 (사용 중에 급증하고 가비지 컬렉션 후에 감소) Private Other 및 Native Heap은 전환당 수 메가바이트씩 꾸준히 증가합니다. 이는 누수가 ART 런타임 외부의 네이티브 메모리에만 있음을 증명합니다.
표준 Java 힙 덤프 (
.hprof)에는 문제가 표시되지 않습니다.
- 관찰: Java 힙은 제한되고 정상 상태를 유지하지만 (사용 중에 급증하고 가비지 컬렉션 후에 감소) Private Other 및 Native Heap은 전환당 수 메가바이트씩 꾸준히 증가합니다. 이는 누수가 ART 런타임 외부의 네이티브 메모리에만 있음을 증명합니다.
표준 Java 힙 덤프 (
익명 메모리 맵 검사: ADB를 사용하여 프로세스 메모리 맵을 검사합니다(메모리 맵 및 할당 검사 참고).
adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"- 관찰: 메모리 증가는
[anon:partition_alloc]또는 삽입된 스크립팅 엔진 힙에 집중되며 JNI 전역 참조가 천천히 증가합니다. 이는 Java 뷰가 대체되었지만 기본 네이티브 페이지 객체 또는 JavaScript 바인딩이 해제되지 않았음을 나타냅니다.
- 관찰: 메모리 증가는
해결:
- 재활용되거나 삭제된 모든
WebView가 활성 스크립트 (stopLoading())를 명시적으로 중지하고 기록을 지우고destroy()를 호출하는지 확인합니다. - 닫힌 뷰와 연결된 맞춤 JavaScript 브리지 콜백 또는 JNI 전역 참조를 해제합니다.
- 탐색 전환 후
Private Other및 프로세스 RSS가 안정화되는지 확인합니다.
- 재활용되거나 삭제된 모든
추가 리소스
메모리 및 WebView 성능 디버깅 및 프로파일링에 관한 자세한 내용은 다음 리소스를 참고하세요.