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로 빠르게 실패하는 Java 힙 할당과 달리 네이티브 메모리는 자동으로 기가바이트까지 증가할 수 있습니다. 출시되지 않은 네이티브 메모리가 실제 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 메모리 소비를 분석할 때는 다음 측정항목에 집중하세요.
상주 집합 크기 (RSS): 공유 코드 및 라이브러리 (Android 스튜디오 프로파일러에서 Total 로 표시됨)를 포함하여 프로세스에 매핑된 총 실제 RAM입니다.
익명 RSS (RssAnon): 디스크의 파일로 지원되지 않는 프로세스에서 직접 할당한 메모리입니다 (예: 네이티브 힙 및 JavaScript 런타임 할당). 이는 웹 콘텐츠의 기본 메모리 비용을 나타냅니다(Android 스튜디오 프로파일러에서 Allocated 로 표시됨).
비공개 메모리 공간 (PMF): 익명 RSS와 스왑(zRAM)의 합계입니다. PMF는 앱이 시스템에 부과하는 실제 제거 불가능한 메모리 부담을 반영합니다.
브라우저 PMF와 렌더러 PMF: 앱의 기본 프로세스에서 사용하는 메모리와 격리된 렌더러 프로세스에서 사용하는 메모리입니다. 대용량 웹 콘텐츠는 주로 렌더러 프로세스에서 급증을 일으킵니다.
라이브 객체 수 (
WebViews,Activities,Views): 메모리에 보관된 활성 UI, 컨텍스트,WebView인스턴스의 수입니다. 이를 추적하면 메모리 증가가 유지된 Java 참조 또는 네이티브 전용 할당으로 인해 발생하는지 확인할 수 있습니다.비공개 기타 및 네이티브 힙:
dumpsys meminfo에서 네이티브 C/C++ 할당 및 맞춤 메모리 매핑 (예: ChromiumPartitionAlloc또는 삽입된 JavaScript 런타임 힙)은 Java 힙이 아닌 네이티브 힙 및 비공개 기타에 표시됩니다.
프로세스 메모리 카운터 및 카테고리에 관한 자세한 내용은 프로세스 메모리 용어집을 참고하세요.
실용적인 진단 워크플로
WebView는 여러 프로세스에서 작동하고 네이티브 메모리를 할당하므로 다음 도구와 기법을 사용하여 공간을 검사하세요.
프로파일링 및 진단 도구
메모리 할당을 검사하고 누수를 진단하려면 다음 도구를 사용하세요.
Android 스튜디오 메모리 프로파일러: 메모리 프로파일러를 사용하여 네이티브 할당을 시각화하고, 시간 경과에 따른 메모리 카테고리를 추적하며, 화면 전환 전반에서
Activity누수를 감지합니다.Perfetto를 사용한 메모리 추적: Perfetto를 사용하여 시스템 수준 메모리 카운터 (예: RSS 및 익명 RSS)를 기록하여 전반적인 메모리 증가를 관찰합니다.
WebView네이티브 엔진 할당은 Perfetto의 힙 프로파일링 도구에서 호출 스택을 생성하지 않습니다. Chrome DevTools를 사용하여 웹 콘텐츠 내에서 JavaScript 힙 스냅샷과 DOM 할당을 검사합니다.
라이브 객체 수 검사
메모리 증가가 유지된 Java 프레임워크
객체 (예: UI 구성요소) 또는 네이티브 할당으로 인해 발생하는지 확인하려면 Objects
섹션의 dumpsys meminfo을 검사하세요.
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 누수: 총 프로세스 RSS 및 비공개 기타 가 계속 증가하는 동안
WebViews및Activities가 일정하게 유지되면 누수는 출시되지 않은 네이티브 리소스, 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 레이어 또는 네이티브 엔진에서 발생하는지 격리하세요.
누수 유형 격리 (Java 대 네이티브):
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"반복되는 사용자 전환 (예: 웹 기사 열기 및 닫기 또는 피드 스와이프) 전후에 실행합니다.- 관찰:
Activities및WebViews수가 일정하게 유지되면 (예: 활성 인스턴스 1~2개) 앱에서Activity컨텍스트 또는 JavaWebView인스턴스가 누수되지 않습니다.
- 관찰:
상호작용 전반에서 메모리 델타 측정 (시계열 추적): 여러 사용자 상호작용 전반에서
dumpsys meminfo스냅샷을 캡처하여 전환당 할당률을 계산합니다.- 관찰: Java 힙은 제한되고 정상 상태를 유지하지만 (사용 중에 급증하고 가비지 컬렉션 후 감소), 비공개 기타 및 네이티브 힙 은 전환당 수 메가바이트씩 꾸준히 증가합니다. 이는 누수가 ART 런타임 외부의 네이티브 메모리에 완전히 있음을 증명합니다.
표준 Java 힙 덤프 (
.hprof)는 문제를 표시하지 않습니다.
- 관찰: Java 힙은 제한되고 정상 상태를 유지하지만 (사용 중에 급증하고 가비지 컬렉션 후 감소), 비공개 기타 및 네이티브 힙 은 전환당 수 메가바이트씩 꾸준히 증가합니다. 이는 누수가 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 성능 디버깅 및 프로파일링에 관한 자세한 내용은 다음 리소스를 참고하세요.