WebView และหน่วยความจำ

WebView เป็นคอมโพเนนต์ที่มีประสิทธิภาพซึ่งช่วยให้คุณแสดงเนื้อหาเว็บภายในแอปพลิเคชัน Android ได้ อย่างไรก็ตาม เนื่องจากโดยพื้นฐานแล้วเป็นเอนจินเบราว์เซอร์ที่มีฟีเจอร์ครบถ้วน (Chromium) จึงใช้หน่วยความจำที่ใช้จำนวนมากและมีสถาปัตยกรรมแบบหลายกระบวนการที่ซับซ้อน

ข้อมูลเบื้องหลังทางเทคนิค: สถาปัตยกรรมแบบหลายกระบวนการ

ในอุปกรณ์ที่ใช้พลังงานจาก Android รุ่นใหม่ WebView จะใช้โมเดลแบบหลายกระบวนการเพื่อปรับปรุงความปลอดภัยและความเสถียร เมื่อแอปใช้ WebView ระบบจะกระจายหน่วยความจำไปยังกระบวนการต่างๆ ดังนี้

  1. กระบวนการเบราว์เซอร์ (กระบวนการของแอป): นี่คือกระบวนการหลัก ของแอปพลิเคชัน ซึ่งมีออบเจ็กต์ WebView ของ Java และส่วน "เบราว์เซอร์" ของเอนจิน Chromium กระบวนการนี้จะจัดการ UI, คำขอเครือข่าย และการแสดงผล GPU (ผสานรวมกับไปป์ไลน์การแสดงผล HWUI ของ Android โดยตรง) WebView ไม่มีกระบวนการ GPU แยกต่างหากเหมือนกับ Chrome
  2. กระบวนการแสดงผล: กระบวนการนี้มีหน้าที่แยกวิเคราะห์ HTML, เรียกใช้ JavaScript และจัดเลย์เอาต์ โดยจะแยกออกจากระบบส่วนที่เหลือเพื่อความปลอดภัย ปัจจุบันแอปจะได้รับกระบวนการแสดงผลเพียงกระบวนการเดียวสำหรับ WebView ทั้งหมด (ยกเว้นกรณีพิเศษที่พบได้น้อย) ซึ่งแตกต่างจาก Chrome ที่มักจะใช้กระบวนการแสดงผลแยกต่างหากสำหรับเว็บไซต์ต่างๆ

สถาปัตยกรรม WebView

ความสำคัญต่อหน่วยความจำ

เมื่อใช้ dumpsys meminfo <your_package> คุณจะเห็นเฉพาะหน่วยความจำที่ **กระบวนการเบราว์เซอร์** (กระบวนการของแอป) ใช้ ระบบจะพิจารณาหน่วยความจำที่กระบวนการแสดงผล ใช้แยกต่างหาก

หน่วยความจำ WebView จะกระจายอยู่ในกระบวนการเบราว์เซอร์ดังนี้

  • ฮีปของ Java: มี WebViewWrapper ของ Java และออบเจ็กต์ที่เกี่ยวข้อง
  • ฮีปเนทีฟ: มีโครงสร้างข้อมูลภายใน แคช และสถานะของเอนจินเบราว์เซอร์ Chromium โปรดทราบว่าเนื่องจากการใช้ PartitionAlloc การจัดสรรเนทีฟของ WebView บางรายการอาจไม่นับรวมอยู่ใน "Native Heap" ใน dumpsys meminfo และอาจปรากฏในส่วน "Other" หรือ "Unknown" แทน
  • หน่วยความจำที่แชร์: ใช้สำหรับการแชร์บัฟเฟอร์กราฟิกและข้อมูลอื่นๆ dumpsys meminfo อาจไม่จัดหมวดหมู่อย่างชัดเจน

เครื่องมือแก้ปัญหา

เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

เครื่องมือที่มีประสิทธิภาพที่สุดสำหรับการวิเคราะห์หน่วยความจำ ภายใน WebView (กระบวนการแสดงผล) คือเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

  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 ในเอาต์พุตและจำนวนออบเจ็กต์

การทำโปรไฟล์ของ Renderer

เนื่องจาก Renderer ทำงานในกระบวนการแยกต่างหาก คุณจึงทำโปรไฟล์ฮีปเนทีฟไม่ได้เพียงแค่ทำโปรไฟล์แอป คุณต้องระบุ PID ของกระบวนการแสดงผลโดยเฉพาะ

วิธีระบุ PID ของ Renderer ที่ถูกต้องเมื่อ WebView หลายรายการทำงานอยู่

  1. ใช้ dumpsys activity:

    adb shell dumpsys activity processes <your_package_name>
    

    มองหาส่วน mConnections คุณจะเห็น ConnectionRecord ที่ลิงก์แอปกับ SandboxedProcessService PID ของกระบวนการนั้นคือ Renderer ตัวอย่าง

    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% ของกรณี แม้ว่าระบบจัดการหน่วยความจำที่ไม่ใช้แล้วโดยอัตโนมัติจะทำงาน แต่ก็อาจล่าช้าอย่างมาก ซึ่งทำให้แอปเก็บทรัพยากรไว้นานกว่าที่คาดไว้

หากแอปเรียกใช้ WebView.destroy() ในเวลาที่เหมาะสม (เช่น ใน Activity.onDestroy()) การเก็บข้อมูลอ้างอิงถึงออบเจ็กต์ WebView เอง จะไม่ทำให้เกิดการรั่วไหลของทรัพยากรเนทีฟที่สำคัญ คุณไม่จำเป็นต้องตั้งค่าข้อมูลอ้างอิงถึงออบเจ็กต์ WebView ในช่อง Activity เป็น null หลังจากทำลายออบเจ็กต์แล้ว เนื่องจากระบบจะล้างข้อมูลเมื่อมีการเก็บรวบรวม Activity ที่ไม่ได้ใช้แล้ว

แบบฝึกหัด: ลองใช้หน่วยความจำ WebView

แบบฝึกหัดที่ 1: การสังเกตการใช้หน่วยความจำแบบหลายกระบวนการ

  1. เปิด MemoryLab แล้ววัดหน่วยความจำพื้นฐานของแอป

    adb shell dumpsys meminfo com.android.memorylab
    

    ตัวอย่างพื้นฐาน (rango): TOTAL PSS: 18915 KB

  2. แตะเปิด WebView (ปกติ)

  3. ใน WebView ให้แตะจัดสรรหน่วยความจำ JS (1000 DIV) หลายครั้ง

  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. สังเกต PSS ทั้งหมด ที่สูงของกระบวนการแสดงผล ในการเรียกใช้ตัวอย่างของเรา ค่านี้เพิ่มขึ้นเป็น ~55MB หลังจากมีการจัดสรร 2-3 ครั้ง โปรดทราบว่าการจัดสรร JavaScript (จัดการโดยเอนจิน V8) มักจะมีส่วนร่วมในส่วน Private Other หรือ Unknown (mmap) ของ dumpsys meminfo แทนที่จะเป็นฮีป Dalvik

แบบฝึกหัดที่ 2: การรั่วไหลของ WebView ฝั่ง Java

ข้อผิดพลาดที่พบบ่อยคือการเก็บอินสแตนซ์ WebView ไว้ในฟิลด์แบบคงที่หรือในออบเจ็กต์ที่มีอายุการใช้งานยาวนานซึ่งทำให้เกิดการรั่วไหล เนื่องจากออบเจ็กต์ WebView เป็น "จุดยึด" ขนาดใหญ่ที่เก็บทรัพยากรเนทีฟและอาจเก็บกระบวนการแสดงผลทั้งหมดไว้ การรั่วไหลจึงมีค่าใช้จ่ายสูงมาก

ผลกระทบจากการรั่วไหลของ WebView

  1. ใน MemoryLab ให้แตะ เปิด WebView (การรั่วไหลของ Java)
  2. กิจกรรมจะปิดโดยอัตโนมัติหลังจากโหลดหน้าเว็บแล้ว (จำลองการนำทางซ้ำๆ และการสะสมของการรั่วไหล)
  3. แตะปุ่ม 4 ครั้ง
  4. ตรวจสอบจำนวนอินสแตนซ์ WebView ในแอปโดยทำดังนี้

    adb shell dumpsys meminfo com.android.memorylab
    

    มองหาส่วนออบเจ็กต์ ที่ด้านล่าง คุณจะเห็นว่าจำนวน WebViews เพิ่มขึ้นเป็น 4

    ตัวอย่างเอาต์พุต (4 อินสแตนซ์ที่รั่วไหล) ใน rango

     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 คลิกจำนวนอินสแตนซ์ เพื่อดูอินสแตนซ์ที่ใช้งานอยู่ทั้งหมด คุณควรเห็นอินสแตนซ์หลายรายการในรายการ

    อินสแตนซ์ WebView ของ AHAT

  8. คลิกอินสแตนซ์ WebView ที่รั่วไหลรายการใดรายการหนึ่ง เลื่อนลงไปที่ส่วนเส้นทางตัวอย่างจากราก GC คุณจะเห็นว่ารายการ sLeakedWebViews ใน com.android.memorylab.WebViewActivity เก็บอินสแตนซ์นี้ไว้

    เส้นทาง AHAT ไปยังรูทของ GC


← เนทีฟ | ↑ ขึ้น | โค้ดแอป →