WebView 및 메모리

WebView는 Android 애플리케이션 내에 웹 콘텐츠를 표시할 수 있는 강력한 구성요소입니다. 하지만 기본적으로 완전한 기능을 갖춘 브라우저 엔진 (Chromium)이므로 메모리 사용량이 많고 복잡한 다중 프로세스 아키텍처가 있습니다.

기술적 배경: 멀티 프로세스 아키텍처

최신 Android 지원 기기에서 WebView는 멀티 프로세스 모델을 사용하여 보안과 안정성을 개선합니다. 앱에서 WebView를 사용하는 경우 메모리는 다음과 같이 여러 프로세스에 분산됩니다.

  1. 브라우저 프로세스 (앱 프로세스): 애플리케이션의 기본 프로세스입니다. 여기에는 Java WebView 객체와 Chromium 엔진의 '브라우저' 부분이 포함됩니다. 이 프로세스는 UI, 네트워크 요청, GPU 렌더링 (Android HWUI 렌더링 파이프라인과 직접 통합됨)을 관리합니다. Chrome과 달리 WebView에는 별도의 GPU 프로세스가 없습니다.
  2. 렌더러 프로세스: 이 프로세스는 HTML 파싱, JavaScript 실행, 레이아웃을 담당합니다. 보안을 위해 나머지 시스템과 격리되어 있습니다. 현재 앱은 Chrome과 달리 모든 WebView에 대해 하나의 렌더러 프로세스만 가져옵니다(드문 특수 사례 제외). Chrome은 서로 다른 사이트에 대해 별도의 렌더러 프로세스를 사용하는 경우가 많습니다.

WebView 아키텍처

메모리에 중요한 이유

dumpsys meminfo <your_package>를 사용하면 브라우저 프로세스 (앱 프로세스)에서 사용한 메모리만 표시됩니다. 렌더러 프로세스에서 사용한 메모리는 별도로 계산됩니다.

브라우저 프로세스 내에서 WebView 메모리는 다음과 같이 분산됩니다.

  • Java 힙: WebView Java 래퍼 및 관련 객체를 포함합니다.
  • 네이티브 힙: Chromium 브라우저 엔진의 내부 데이터 구조, 캐시, 상태를 포함합니다. PartitionAlloc 사용으로 인해 일부 WebView 네이티브 할당이 dumpsys meminfo의 '네이티브 힙'에 포함되지 않고 '기타' 또는 '알 수 없음'에 표시될 수 있습니다.
  • 공유 메모리: 그래픽 버퍼 및 기타 데이터를 공유하는 데 사용됩니다. dumpsys meminfo에서 명확하게 분류하지 않을 수 있습니다.

문제 해결 도구

Chrome DevTools

WebView (렌더러 프로세스) 내부의 메모리를 분석하는 가장 강력한 도구는 Chrome DevTools입니다.

  1. 앱에서 WebView 디버깅을 사용 설정합니다.

    // NOTE: In production, this should be gated behind a developer setting
    // or only enabled for debuggable builds to prevent reverse engineering.
    WebView.setWebContentsDebuggingEnabled(true);
    
  2. USB를 통해 기기를 연결합니다.

  3. 호스트 머신에서 Chrome을 열고 chrome://inspect/#devices로 이동합니다.

  4. 앱을 찾아 검사를 클릭합니다.

  5. 개발자 도구 창에서 메모리 탭으로 이동하여 힙 스냅샷을 만들거나 JavaScript 힙의 할당 타임라인을 기록합니다.

dumpsys meminfo

adb shell dumpsys meminfo --all <package>를 사용하여 메모리 분석을 확인합니다. 출력에서 WebView 카테고리와 객체 수를 확인합니다.

렌더러 프로파일링

렌더러는 별도의 프로세스에서 실행되므로 앱을 프로파일링하는 것만으로는 네이티브 힙을 프로파일링할 수 없습니다. 렌더러 프로세스의 PID를 구체적으로 식별해야 합니다.

여러 WebView가 활성 상태일 때 올바른 렌더러 PID를 식별하려면 다음을 실행하세요.

  1. dumpsys activity 사용:

    adb shell dumpsys activity processes <your_package_name>
    

    mConnections 섹션을 찾습니다. 앱을 SandboxedProcessService에 연결하는 ConnectionRecord이 표시됩니다. 이 프로세스의 PID는 렌더러입니다. 예:

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. 프로세스 이름 확인: 렌더러 프로세스의 이름은 일반적으로 com.google.android.webview:sandboxed_processX 또는 이와 유사합니다. 하나의 앱만 WebView를 사용하는 경우 하나만 있을 가능성이 큽니다.

PID가 있으면 heapprofd를 사용하여 프로파일링할 수 있습니다.

WebView 메모리 권장사항

명시적 폐기

앱은 인스턴스 사용이 완료된 시점을 나타내기 위해 WebView.destroy()를 호출해야 합니다.

WebView는 인스턴스가 가비지 수집되고 모든 리소스를 자동으로 해제할 수 있도록 노력하지만, 모든 경우에 100% 보장하기는 어렵습니다. 자동 가비지 컬렉션이 작동하더라도 크게 지연되어 앱이 예상보다 훨씬 오래 리소스를 보유할 수 있습니다.

앱이 적절한 시간에 (예: Activity.onDestroy()에서) WebView.destroy()를 호출하는 경우 WebView 객체 자체에 대한 참조를 유지해도 중요한 네이티브 리소스가 누수되지 않습니다. 활동이 가비지 수집될 때 정리되므로 활동 필드에서 WebView 객체 참조를 소멸한 후 null로 설정할 필요는 없습니다.

실습: WebView 메모리 직접 사용

연습 1: 다중 프로세스 사용 공간 관찰

  1. MemoryLab을 실행하고 앱 메모리의 기준 측정값을 가져옵니다.

    adb shell dumpsys meminfo com.android.memorylab
    

    샘플 기준 (rango): TOTAL PSS: 18915 KB

  2. Launch WebView (Normal)을 탭합니다.

  3. WebView에서 Allocate JS Memory (1000 DIVs)를 여러 번 탭합니다.

  4. 앱의 메모리를 다시 확인합니다.

    adb shell dumpsys meminfo com.android.memorylab
    
  5. 앱 프로세스의 메모리가 기준선에 비해 크게 증가하지 않는지 확인합니다. DOM 요소가 렌더러 프로세스에 있기 때문입니다.

  6. 렌더러 프로세스를 찾습니다.

    adb shell ps -A | grep webview | grep sandboxed
    

    출력 예:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. 렌더러 프로세스의 메모리를 확인합니다 (PID 사용).

    adb shell dumpsys meminfo 14227
    
  8. 렌더러 프로세스의 높은 TOTAL PSS를 관찰합니다. 샘플 실행에서 할당이 몇 번 이루어진 후 ~55MB로 증가했습니다. JavaScript 할당 (V8 엔진에서 처리)은 일반적으로 Dalvik 힙이 아닌 dumpsys meminfo비공개 기타 또는 알 수 없음 (mmap) 섹션에 기여합니다.

연습 2: Java 측 WebView 누수

일반적인 실수는 정적 필드나 누수되는 수명이 긴 객체에 WebView 인스턴스를 유지하는 것입니다. WebView 객체는 네이티브 리소스와 잠재적으로 전체 렌더러 프로세스를 보유하는 무거운 '앵커'이므로 누수되면 비용이 많이 듭니다.

WebView 누수 영향

  1. MemoryLab에서 Launch WebView (Java Leak)을 탭합니다.
  2. 페이지가 로드되면 활동이 자동으로 닫힙니다 (반복된 탐색 및 누수 축적 시뮬레이션).
  3. 버튼을 4번 탭합니다.
  4. 앱의 WebView 인스턴스 수를 확인합니다.

    adb shell dumpsys meminfo com.android.memorylab
    

    하단의 객체 섹션을 찾습니다. WebViews의 개수가 4로 증가한 것을 확인할 수 있습니다.

    rango의 샘플 출력 (누수된 인스턴스 4개):

     Objects
               Views:       51         ViewRootImpl:        5
         AppContexts:       14           Activities:        5
              Assets:       38        AssetManagers:        0
       Local Binders:       55        Proxy Binders:       77
       Parcel memory:       41         Parcel count:       68
    Death Recipients:        3             WebViews:        4
    
  5. 힙 덤프를 캡처하고 AHAT를 사용하여 누수를 찾습니다. 경로에 ahat가 없는 경우 Android 트리에서 빌드할 수 있습니다.

    # Dump heap from device
    adb shell am dumpheap com.android.memorylab /data/local/tmp/heap.hprof
    adb pull /data/local/tmp/heap.hprof
    # Run ahat using the built JAR (found in out/host/linux-x86/framework/)
    java -jar out/host/linux-x86/framework/ahat.jar -p 8888 heap.hprof
    
  6. AHAT 웹 인터페이스 (localhost:8888)에서 상단 메뉴의 할당 링크 (또는 사이트)를 클릭하여 전체 메모리 사용량을 확인합니다.

    AHAT 할당

  7. android.webkit.WebView 클래스를 검색합니다. 인스턴스 수를 클릭하여 모든 라이브 인스턴스를 확인합니다. 목록에 여러 인스턴스가 표시됩니다.

    AHAT WebView 인스턴스

  8. 유출된 WebView 인스턴스 중 하나를 클릭합니다. GC 루트의 샘플 경로 섹션까지 아래로 스크롤합니다. com.android.memorylab.WebViewActivitysLeakedWebViews 목록에 의해 보유되고 있음을 확인할 수 있습니다.

    AHAT 경로를 GC 루트로


← 네이티브 | ↑ 위로 | 앱 코드 →