WebView จะเรียกใช้โค้ดแบบเนทีฟในหลายกระบวนการเพื่อแสดงผลเนื้อหาเว็บในแอป Android การปล่อยให้อินสแตนซ์ WebView ไม่ได้รับการจัดการอาจทำให้เกิดหน่วยความจำรั่วไหล, ข้อผิดพลาดหน่วยความจำไม่พอ (OOM) และประสิทธิภาพของแอปแย่ลง
เอกสารนี้อธิบายโมเดลหน่วยความจำแบบหลายกระบวนการของ WebView รวมถึงอธิบายวิธีจัดการวงจรการใช้งานอย่างเหมาะสมเพื่อป้องกันการรั่วไหล และให้เวิร์กโฟลว์ที่ใช้งานได้จริงสำหรับการวินิจฉัยปัญหาหน่วยความจำ
ทำความเข้าใจสถาปัตยกรรมหน่วยความจำของ WebView
หากต้องการจัดการหน่วยความจำของ WebView อย่างมีประสิทธิภาพ คุณต้องทำความเข้าใจวิธีที่ Android จัดสรรทรัพยากรสำหรับเนื้อหาเว็บ ดังนี้
การดำเนินการแบบหลายกระบวนการ: ใน Android 8.0 (ระดับ API 26) ขึ้นไป
WebViewจะแยกเนื้อหาเว็บออกจากฟังก์ชันหลักของแอปในหลายกระบวนการ (ในอุปกรณ์ที่มี RAM น้อย ระบบอาจกลับไปใช้กระบวนการเดียว)- กระบวนการโฮสต์ (เบราว์เซอร์): กระบวนการหลักของแอปที่
Activityและโค้ด Java หรือ Kotlin ทำงาน - กระบวนการแสดงผลแบบแยก: กระบวนการแซนด์บ็อกซ์แยกต่างหาก (
SandboxedProcessService) ที่แยกวิเคราะห์ HTML และ CSS, เรียกใช้ JavaScript และแสดงผลหน้าเว็บ
- กระบวนการโฮสต์ (เบราว์เซอร์): กระบวนการหลักของแอปที่
หน่วยความจำที่ใช้แบบเนทีฟ: หน่วยความจำส่วนใหญ่ของ
WebViewรวมถึงกราฟิกที่แสดงผล, แผนผัง DOM และหน่วยความจำรันไทม์ของ JavaScript จะได้รับการจัดสรรในหน่วยความจำแบบเนทีฟ ไม่ใช่ในฮีป Java ฮีปดัมป์ Java (.hprof) จะแสดงเฉพาะออบเจ็กต์ Wrapper Java แบบเบาและไม่บันทึกหน่วยความจำจริงที่เนื้อหาเว็บใช้ผลกระทบต่อระบบของหน่วยความจำแบบเนทีฟ: หน่วยความจำแบบเนทีฟสามารถเพิ่มขึ้นอย่างเงียบๆ เป็นกิกะไบต์ ซึ่งแตกต่างจากการจัดสรรฮีป Java ที่ถูกจำกัดด้วยขีดจำกัด
maxHeapของแอปและล้มเหลวอย่างรวดเร็วด้วยOutOfMemoryErrorเมื่อหน่วยความจำแบบเนทีฟที่ยังไม่ได้เผยแพร่เต็ม RAM จริงและพื้นที่ Swap (zRAM) Low Memory Killer (LMK) ของ Android จะเริ่มยุติกระบวนการเบื้องหลังเพื่อเรียกคืนหน่วยความจำ ซึ่งจะทำให้การทำงานแบบมัลติทาสก์โดยรวมของอุปกรณ์แย่ลงก่อนที่จะยุติแอปที่ทำงานอยู่เบื้องหน้าในที่สุด
จัดการวงจรการใช้งาน WebView
การจัดการวงจรการใช้งานอย่างเหมาะสมเป็นสิ่งสำคัญอย่างยิ่งในการป้องกันหน่วยความจำรั่วไหล ข้อผิดพลาด
ที่พบบ่อยคือการคิดว่าการนำ WebView ออกจากเลย์เอาต์หรือการปล่อยให้
Activity สิ้นสุดโดยอัตโนมัติจะทำให้หน่วยความจำของ ว่าง
หากต้องการล้างข้อมูลอ้างอิงบริบท Java และทรัพยากรการแสดงผลแบบเนทีฟอย่างสมบูรณ์ คุณต้องจัดลำดับการล้างข้อมูลอย่างชัดเจนในวงจรการใช้งานของคอมโพเนนต์โฮสต์ (เช่น onDestroy()) โดยหยุดการดำเนินการหน้าเว็บที่ใช้งานอยู่ ยกเลิกการแนบมุมมองออกจากคอนเทนเนอร์ และปล่อยการผูกแบบเนทีฟ
ล้างข้อมูลอินสแตนซ์ WebView
หากต้องการให้ระบบปิดอย่างสะอาดและปล่อยทรัพยากรเมื่อ Activity หรือ Fragment ถูกทำลาย ให้ทำดังนี้
- นำ
WebViewออกจากคอนเทนเนอร์ระดับบนสุด (ViewGroup) - หยุดการโหลดที่ใช้งานอยู่และล้างประวัติการนำทาง
- เรียกใช้
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() }
Java
@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 ล้างข้อมูลลำดับชั้นของมุมมอง และหยุดการทำงานเบื้องหลังของเว็บ อย่างไรก็ตาม คุณอาจสังเกตเห็นว่าหน่วยความจำจริง (Resident Set Size) ของกระบวนการไม่ได้ลดลงทันทีเป็นค่าพื้นฐานก่อนใช้ WebView
โดยลักษณะการทำงานนี้เป็นสิ่งที่ปกติ แคชรันไทม์แบบเนทีฟ, ไลบรารีที่แชร์ และหน้าหน่วยความจำที่จัดสรรจะยังคงอยู่ในกระบวนการจนกว่าระบบปฏิบัติการจะเรียกคืนหรือกระบวนการสิ้นสุด เป้าหมายหลักของ destroy() คือการป้องกันหน่วยความจำ Activity รั่วไหลสะสมเมื่อผู้ใช้เข้าและออกจากหน้าจอที่ใช้เว็บ
เมตริกการแก้ไขข้อบกพร่องที่สำคัญ
เมื่อวิเคราะห์การใช้หน่วยความจำของ WebView ให้มุ่งเน้นที่เมตริกต่อไปนี้
Resident Set Size (RSS): RAM จริงทั้งหมดที่แมปกับกระบวนการ รวมถึงโค้ดและไลบรารีที่แชร์ (ระบุเป็นทั้งหมด ในเครื่องมือสร้างโปรไฟล์ของ Android Studio)
Anonymous RSS (RssAnon): หน่วยความจำที่กระบวนการจัดสรรโดยตรงซึ่งไม่ได้สำรองข้อมูลด้วยไฟล์ในดิสก์ (เช่น ฮีปแบบเนทีฟและการจัดสรรรันไทม์ของ JavaScript) ซึ่งแสดงถึงค่าใช้จ่ายหน่วยความจำหลักของเนื้อหาเว็บ (ระบุเป็นจัดสรร ในเครื่องมือสร้างโปรไฟล์ของ Android Studio)
Private Memory Footprint (PMF): ผลรวมของ Anonymous RSS และ Swap (zRAM) PMF แสดงถึงภาระหน่วยความจำจริงที่แอปของคุณกำหนดให้กับระบบ
PMF ของเบราว์เซอร์เทียบกับ PMF ของตัวแสดงผล: หน่วยความจำที่กระบวนการหลักของแอปใช้เทียบกับหน่วยความจำที่กระบวนการแสดงผลแบบแยกใช้ เนื้อหาเว็บขนาดใหญ่ทำให้เกิดการเพิ่มขึ้นอย่างรวดเร็วในกระบวนการแสดงผลเป็นหลัก
จำนวนออบเจ็กต์ที่ใช้งานอยู่ (
WebViews,Activities,Views): จำนวนอินสแตนซ์ UI, บริบท และWebViewที่ ใช้งานอยู่ซึ่งเก็บไว้ในหน่วยความจำ การติดตามจำนวนเหล่านี้จะระบุว่าการเพิ่มขึ้นของหน่วยความจำเกิดจากข้อมูลอ้างอิง Java ที่เก็บไว้หรือการจัดสรรแบบเนทีฟเท่านั้นPrivate Other และ Native Heap: ใน
dumpsys meminfoการจัดสรร C/C++ แบบเนทีฟและการแมปหน่วยความจำที่กำหนดเอง (เช่น ฮีปPartitionAllocของ Chromium หรือฮีปรันไทม์ของ JavaScript แบบฝัง) จะปรากฏในส่วน Native Heap และ Private Other แทนที่จะเป็น Java Heap
ดูข้อมูลเพิ่มเติมเกี่ยวกับตัวนับหน่วยความจำของกระบวนการและหมวดหมู่ของตัวนับได้ที่ อภิธานศัพท์หน่วยความจำของกระบวนการ
เวิร์กโฟลว์การวินิจฉัยที่ใช้งานได้จริง
เนื่องจาก WebView ทำงานในหลายกระบวนการและจัดสรรหน่วยความจำแบบเนทีฟ ให้ใช้เครื่องมือและเทคนิคต่อไปนี้เพื่อตรวจสอบขนาดหน่วยความจำ
เครื่องมือสร้างโปรไฟล์และเครื่องมือวินิจฉัย
หากต้องการตรวจสอบการจัดสรรหน่วยความจำและวินิจฉัยการรั่วไหล ให้ใช้เครื่องมือต่อไปนี้
เครื่องมือสร้างโปรไฟล์หน่วยความจำของ Android Studio: ใช้ เครื่องมือสร้างโปรไฟล์หน่วยความจำ เพื่อ แสดงภาพการจัดสรรแบบเนทีฟ ติดตามหมวดหมู่หน่วยความจำเมื่อเวลาผ่านไป และตรวจหา
Activityที่รั่วไหลในการเปลี่ยนหน้าจอการติดตามหน่วยความจำด้วย Perfetto: ใช้ Perfetto เพื่อบันทึกตัวนับหน่วยความจำระดับระบบ (เช่น RSS และ Anonymous RSS) เพื่อสังเกตการเพิ่มขึ้นของหน่วยความจำ โดยรวม โปรดทราบว่าการจัดสรรเครื่องมือแบบเนทีฟของ
WebViewจะไม่สร้าง Callstack ในเครื่องมือสร้างโปรไฟล์ฮีปของ Perfetto ใช้ เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome เพื่อตรวจสอบสแนปชอตฮีป 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เพิ่มขึ้นในการนำทางแต่ละครั้งและไม่กลับสู่ค่าพื้นฐาน แสดงว่าแอปของคุณกำลังรั่วไหลอินสแตนซ์WebViewJava หรือActivityโฮสต์ (เช่น เนื่องจากไม่มีViewGroup.removeView()หรือข้อมูลอ้างอิง Listener ที่เก็บไว้) เนื่องจากActivityที่รั่วไหลจะตรึง DOM Tree ทั้งหมดและทรัพยากรรูปภาพที่ถอดรหัสไว้ในหน่วยความจำ การเข้าชมซ้ำๆ จะทำให้ฮีป Java หมดลงอย่างรวดเร็วและทำให้เกิดข้อผิดพลาดOutOfMemoryErrorการรั่วไหลแบบเนทีฟหรือ DOM: หาก
WebViewsและActivitiesยังคงที่ในขณะที่ RSS ของกระบวนการทั้งหมดและ Private Other เพิ่มขึ้นเรื่อยๆ แสดงว่าการรั่วไหลเกิดจากทรัพยากรแบบเนทีฟที่ยังไม่ได้เผยแพร่, องค์ประกอบ DOM หรือการผูกเครื่องมือ JavaScript เนื่องจากการจัดสรรเหล่านี้อยู่ในหน่วยความจำแบบเนทีฟและข้ามตัวเก็บขยะ ART จึงทำให้เครื่องมือตรวจหาการรั่วไหลของ Java มาตรฐานมองไม่เห็นและสะสมต่อไปจนกว่าระบบปฏิบัติการจะยุติแอป
สร้างโปรไฟล์กระบวนการแสดงผลแบบแยกโดยใช้ CLI
การเรียกใช้ dumpsys meminfo ด้วยชื่อแพ็กเกจของแอปจะแสดงเฉพาะหน่วยความจำสำหรับกระบวนการโฮสต์หลัก หากต้องการตรวจสอบกระบวนการแสดงผลแบบแยกที่แสดงผลหน้าเว็บ ให้ทำดังนี้
ค้นหารหัสกระบวนการ (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] |
PartitionAlloc ของ Chromium | การจัดสรรสำหรับ DOM Tree, บัฟเฟอร์การแสดงผล, ฮีป JavaScript V8 และการดำเนินการ WebAssembly ใน WebView |
ใช่ (สูง): การโหลดหน้าเว็บขนาดใหญ่, DOM ที่มีสื่อจำนวนมาก หรือการไม่เรียกใช้ destroy() ในอินสแตนซ์ WebView ที่ทิ้งแล้วจะทำให้แท็กนี้เพิ่มขึ้นโดยตรง |
[anon:scudo...] หรือ [anon:libc_malloc] |
ตัวจัดสรรฮีปแบบเนทีฟของ Android (Scudo / jemalloc) | การจัดสรรแบบเนทีฟ C/C++ ทั่วไปที่ไลบรารี NDK, บริดจ์ JNI และไปป์ไลน์กราฟิกแบบเนทีฟใช้ | ใช่ (ปานกลางถึงสูง): การเพิ่มขึ้นจะเกิดขึ้นเมื่อ Wrapper JNI แบบเนทีฟหรือการขึ้นต่อกัน C++ ของบุคคลที่สามเก็บการจัดสรรที่ยังไม่ได้เผยแพร่ไว้ในการนำทาง |
[anon:...] (เช่น [anon:quickjs_heap...]) |
การเขียนสคริปต์ที่กำหนดเองหรือรันไทม์แบบเนทีฟ | เครื่องมือ JavaScript แบบฝัง, รันไทม์ WebAssembly ที่กำหนดเอง หรือพูลบัฟเฟอร์แบบเนทีฟที่กำหนดเอง | ใช่ (ขึ้นอยู่กับบริบท): พบได้ทั่วไปในแอปแบบไฮบริดที่เรียกใช้เครื่องมือเขียนสคริปต์ควบคู่ไปกับมุมมองแบบเนทีฟและล้างข้อมูลการผูกรันไทม์ไม่ได้ |
ข้อจำกัดของ API หน่วยความจำในแอป
API หน่วยความจำในแอป (เช่น Debug.getMemoryInfo หรือ
ActivityManager.getProcessMemoryInfo) จะวัดเฉพาะกระบวนการเรียก
ในโหมดหลายกระบวนการ API เหล่านี้ไม่สามารถบันทึกหน่วยความจำที่กระบวนการแสดงผลแบบแยกใช้ หากต้องการประเมินหน่วยความจำทั้งหมดอย่างถูกต้อง ให้ใช้เครื่องมือระบบ เช่น dumpsys meminfo, Perfetto หรือเครื่องมือสร้างโปรไฟล์ของ Android Studio
การจัดลำดับความสำคัญของหน่วยความจำสูงในแอปแบบไฮบริด
เมื่อวินิจฉัยการเพิ่มขึ้นของหน่วยความจำที่อธิบายไม่ได้ระหว่างการโต้ตอบ WebView ที่เกิดขึ้นซ้ำๆ (เช่น การเปิดลิงก์เว็บหรือการเลื่อนดูฟีดที่ใช้เว็บ) ให้ใช้เวิร์กโฟลว์การจัดลำดับความสำคัญต่อไปนี้เพื่อแยกว่าการรั่วไหลเกิดจากเลเยอร์ Java หรือเครื่องมือแบบเนทีฟ
แยกประเภทการรั่วไหล (Java เทียบกับแบบเนทีฟ): เรียกใช้
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"ก่อนและหลังการเปลี่ยนของผู้ใช้ซ้ำๆ (เช่น การเปิดและปิดบทความเว็บ หรือการปัดดูฟีด)- ข้อสังเกต: หากจำนวน
ActivitiesและWebViewsยังคงที่ (เช่น อินสแตนซ์ที่ใช้งานอยู่ 1-2 รายการ) แสดงว่าแอปไม่ได้รั่วไหลบริบทActivityหรืออินสแตนซ์WebViewJava
- ข้อสังเกต: หากจำนวน
วัดส่วนต่างของหน่วยความจำในการโต้ตอบ (การติดตามแบบอนุกรมเวลา): บันทึกสแนปชอต
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 Bridge ที่กำหนดเองหรือข้อมูลอ้างอิงส่วนกลาง JNI ที่เชื่อมโยงกับมุมมองที่ปิดไป
- ยืนยันว่า
Private Otherและ RSS ของกระบวนการมีเสถียรภาพหลังจากการเปลี่ยนการนำทาง
- ตรวจสอบว่า
แหล่งข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับการแก้ไขข้อบกพร่องและการสร้างโปรไฟล์หน่วยความจำและประสิทธิภาพของ WebView ได้จากแหล่งข้อมูลต่อไปนี้