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) Android Low Memory Killer (LMK) จะเริ่มสิ้นสุดกระบวนการทำงานเบื้องหลังเพื่อเรียกคืน หน่วยความจำ ซึ่งจะทำให้การทำงานแบบมัลติทาสก์โดยรวมของอุปกรณ์แย่ลงก่อนที่จะปิดแอปที่ทำงานอยู่เบื้องหน้าในที่สุด
จัดการวงจรของ WebView
การจัดการวงจรที่เหมาะสมเป็นสิ่งสำคัญในการป้องกันหน่วยความจำรั่วไหล ข้อผิดพลาด
ที่พบบ่อยคือการคิดว่าการนำ WebView ออกจากเลย์เอาต์หรือปล่อยให้
Activity ทำงานจนเสร็จโดยอัตโนมัติจะทำให้หน่วยความจำของ ว่าง
เพื่อให้มั่นใจว่าได้ล้างข้อมูลอ้างอิงบริบท Java และทรัพยากรการแสดงผลดั้งเดิมทั้ง 2 อย่างอย่างสมบูรณ์ คุณต้องจัดลำดับการหยุดทำงานอย่างชัดเจนในวงจรของคอมโพเนนต์โฮสต์ (เช่น 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การใช้หน่วยความจำ ให้มุ่งเน้นที่เมตริกต่อไปนี้
ขนาดชุดข้อมูลที่อยู่ในหน่วยความจำ (RSS): RAM จริงทั้งหมดที่แมปกับกระบวนการ รวมถึงโค้ดและไลบรารีที่ใช้ร่วมกัน (ติดป้ายกำกับเป็นทั้งหมดใน Android Studio Profiler)
Anonymous RSS (RssAnon): หน่วยความจำที่จัดสรรโดยกระบวนการโดยตรงซึ่งไม่ได้สำรองข้อมูลไว้ในไฟล์บนดิสก์ (เช่น การจัดสรรฮีปแบบเนทีฟและรันไทม์ JavaScript) ซึ่งแสดงถึงต้นทุนหน่วยความจำหลักของเนื้อหาเว็บ (มีป้ายกำกับเป็นจัดสรรใน Android Studio Profiler)
หน่วยความจำส่วนตัว (PMF): ผลรวมของ RSS แบบไม่ระบุตัวตนและ Swap (zRAM) PMF แสดงภาระด้านหน่วยความจำที่แอปของคุณกำหนดไว้ในระบบ ซึ่งไม่สามารถนำออกได้จริง
PMF ของเบราว์เซอร์เทียบกับ PMF ของโปรแกรมแสดงผล: หน่วยความจำที่กระบวนการหลักของแอปใช้ เทียบกับหน่วยความจำที่กระบวนการแสดงผลที่แยกต่างหากใช้ เนื้อหาเว็บที่มีขนาดใหญ่ ทำให้เกิดการเพิ่มขึ้นอย่างรวดเร็วในกระบวนการแสดงผลเป็นหลัก
จำนวนออบเจ็กต์ที่ใช้งานอยู่ (
WebViews,Activities,Views): จำนวนอินสแตนซ์ UI, Context และWebViewที่ใช้งานอยู่ซึ่งอยู่ในหน่วยความจำ การติดตามสิ่งเหล่านี้จะช่วยระบุว่าการเพิ่มขึ้นของหน่วยความจำเกิดจากการอ้างอิง Java ที่เก็บไว้หรือการจัดสรรเฉพาะเนทีฟฮีปส่วนตัวอื่นๆ และฮีปเนทีฟ: ใน
dumpsys meminfoการจัดสรร C/C++ แบบเนทีฟ และการแมปหน่วยความจำที่กำหนดเอง (เช่นPartitionAllocของ Chromium หรือ ฮีปรันไทม์ JavaScript แบบฝัง) จะปรากฏในส่วนฮีปเนทีฟและส่วนอื่นๆ แบบส่วนตัว แทนที่จะเป็นฮีป Java
ดูข้อมูลเพิ่มเติมเกี่ยวกับตัวนับหน่วยความจำของกระบวนการและหมวดหมู่ได้ที่อภิธานศัพท์หน่วยความจำของกระบวนการ
เวิร์กโฟลว์การวินิจฉัยที่ใช้งานได้จริง
เนื่องจาก WebView ทำงานในหลายกระบวนการและจัดสรรหน่วยความจำดั้งเดิม
ให้ใช้เครื่องมือและเทคนิคต่อไปนี้เพื่อตรวจสอบร่องรอยของ
เครื่องมือสร้างโปรไฟล์และเครื่องมือวินิจฉัย
หากต้องการตรวจสอบการจัดสรรหน่วยความจำและวินิจฉัยการรั่วไหล ให้ใช้เครื่องมือต่อไปนี้
เครื่องมือสร้างโปรไฟล์หน่วยความจำของ Android Studio: ใช้เครื่องมือสร้างโปรไฟล์หน่วยความจำเพื่อ แสดงภาพการจัดสรรแบบเนทีฟ ติดตามหมวดหมู่หน่วยความจำเมื่อเวลาผ่านไป และตรวจหา
Activityการรั่วไหลเมื่อเปลี่ยนหน้าจอการติดตามหน่วยความจำด้วย Perfetto: ใช้ Perfetto เพื่อบันทึกตัวนับหน่วยความจำระดับระบบ (เช่น RSS และ 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เพิ่มขึ้นในแต่ละการนำทางและไม่กลับสู่ค่าพื้นฐาน แสดงว่าแอปของคุณมีอินสแตนซ์ JavaWebViewหรือโฮสต์Activityรั่ว (เช่น เนื่องจากไม่มีViewGroup.removeView()หรือการอ้างอิง Listener ที่เก็บไว้) เนื่องจากActivityที่รั่วไหลจะตรึงทั้งแผนผังมุมมองและแหล่งข้อมูลรูปภาพที่ถอดรหัสแล้วไว้ในหน่วยความจำ การเข้าชมซ้ำๆ จะทำให้ฮีปของ Java หมดลงอย่างรวดเร็วและทำให้OutOfMemoryErrorขัดข้องการรั่วไหลของเนทีฟหรือ DOM: หาก
WebViewsและActivitiesยังคงคงที่ ในขณะที่ RSS ของกระบวนการทั้งหมดและอื่นๆ แบบส่วนตัวยังคงเพิ่มขึ้น การรั่วไหล เกิดจากทรัพยากรเนทีฟ องค์ประกอบ 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:"ตารางต่อไปนี้แสดงแท็กหน่วยความจำแบบไม่ระบุตัวตนที่พบบ่อยและความเกี่ยวข้องกับการเพิ่มขึ้นของหน่วยความจำ
| แท็กความทรงจำ | OOB | ความเกี่ยวข้องกับเนื้อหาแอปและเว็บ | สาเหตุที่พบบ่อยที่ทำให้หน่วยความจำเพิ่มขึ้น |
|---|---|---|---|
[anon:partition_alloc] |
Chromium PartitionAlloc | การจัดสรรสำหรับ 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 Profiler
การจัดลำดับความสำคัญของหน่วยความจำสูงในแอปแบบไฮบริด
เมื่อวินิจฉัยการเพิ่มขึ้นของหน่วยความจำที่อธิบายไม่ได้ในระหว่างWebView
การโต้ตอบที่เกิดซ้ำ (เช่น การเปิดลิงก์เว็บหรือการไปยังส่วนต่างๆ ในฟีดที่ทำงานบนเว็บ) ให้ใช้เวิร์กโฟลว์การคัดแยกต่อไปนี้เพื่อแยกแยะว่าการรั่วไหลมาจากเลเยอร์ Java หรือเครื่องมือดั้งเดิม
แยกประเภทการรั่วไหล (Java กับ Native): เรียกใช้
dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"ก่อนและหลังการเปลี่ยนผู้ใช้ซ้ำๆ (เช่น การเปิดและปิดบทความบนเว็บ หรือการปัดผ่านฟีด)- ข้อสังเกต: หากจำนวน
ActivitiesและWebViewsยังคงคงที่ (เช่น มีอินสแตนซ์ที่ใช้งานอยู่ 1-2 รายการ) แสดงว่าแอปไม่ได้รั่วไหลActivityบริบทหรืออินสแตนซ์WebViewของ Java
- ข้อสังเกต: หากจำนวน
วัดเดลต้าของหน่วยความจำในการโต้ตอบ (การติดตามอนุกรมเวลา): บันทึกสแนปชอต
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ประสิทธิภาพ
ได้จากแหล่งข้อมูลต่อไปนี้