iOS Runtime คือสภาพแวดล้อมรันไทม์ของแอปพลิเคชันบนระบบปฏิบัติการ Apple iOS ซึ่งรวมถึง Objective-C Runtime, Swift Runtime, เฟรมเวิร์ก Cocoa Touch และกลไกการจัดการหน่วยความจำผ่าน Automatic Reference Counting (ARC) iOS Runtime รับผิดชอบการผูกเมธอดแบบไดนามิก (message passing) การโหลดคลาส การจัดการหน่วยความจำ และการโต้ตอบกับฮาร์ดแวร์ผ่านเฟรมเวิร์ก iOS ตาม เอกสารประกอบของนักพัฒนา Apple การทำความเข้าใจ runtime เป็นสิ่งจำเป็นสำหรับการปรับประสิทธิภาพ การดีบัก และการพัฒนาแอป iOS ที่เสถียร
ประเด็นสำคัญ
iOS Runtime คือชุดของส่วนประกอบระบบที่รับประกันการทำงานของแอปพลิเคชันบนอุปกรณ์ Apple ที่ใช้ iOS ซึ่งรวมถึง Objective-C Runtime (ไลบรารี libobjc.A.dylib), Swift Runtime (libswiftCore.dylib), Core Foundation, เฟรมเวิร์ก Cocoa Touch (UIKit, Foundation), ตัวโหลดแบบไดนามิก dyld และสภาพแวดล้อมรันไทม์สำหรับการจัดการหน่วยความจำ เธรด และการสื่อสารระหว่างกระบวนการ
ในเชิงสถาปัตยกรรม iOS Runtime ทำงานสามระดับ ในระดับต่ำสุด — รูปแบบไบนารี Mach-O และ dyld ซึ่งโหลดไฟล์ปฏิบัติการและไลบรารี ระดับกลาง — Objective-C Runtime และ Swift Runtime รับผิดชอบการจัดส่งเมธอดและการจัดการอ็อบเจ็กต์ ระดับบนสุด — เฟรมเวิร์ก Cocoa Touch (UIKit, Foundation, Core Data, Metal) ซึ่งจัดเตรียม API ให้กับนักพัฒนา
การทำความเข้าใจ iOS Runtime ช่วยให้นักพัฒนาแก้ปัญหาที่ซับซ้อนได้: การสลับเมธอด (Method Swizzling) สำหรับการทดสอบ A/B และการวิเคราะห์ การโหลดคลาสแบบไดนามิก การปรับหน่วยความจำให้เหมาะสมผ่านความเข้าใจ ARC การดีบัก retain cycles และการรั่วไหลของหน่วยความจำ การปรับเวลาเริ่มต้นแอปพลิเคชันให้เหมาะสมผ่าน dyld หากไม่มีความรู้เกี่ยวกับ runtime การทำโปรไฟล์และการปรับให้เหมาะสมในระดับระบบเป็นไปไม่ได้
| ส่วนประกอบ | ไลบรารี | วัตถุประสงค์ |
|---|---|---|
| Objective-C Runtime | libobjc.A.dylib | Message passing, คลาสไดนามิก, swizzling |
| Swift Runtime | libswiftCore.dylib | Value types, generics, protocol witnesses |
| Core Foundation | CoreFoundation.framework | CFType, toll-free bridging |
| dyld | dyld (usr/lib/dyld) | การโหลด Mach-O, การเชื่อมโยงไลบรารี |
| libSystem | libSystem.B.dylib | POSIX threads, libc, libdispatch (GCD) |
แอปพลิเคชันสำหรับ iOS จะถูกคอมไพล์เป็นรูปแบบ Mach-O (Mach Object) ไฟล์ Mach-O ประกอบด้วยส่วนหัว คำสั่งโหลด และเซกเมนต์: __TEXT (โค้ด, ค่าคงที่), __DATA (ตัวแปรส่วนกลาง, เมตาดาต้า Objective-C), __LINKEDIT (สัญลักษณ์, ตารางการย้ายตำแหน่ง) dyld วิเคราะห์ Mach-O และโหลด dependencies ก่อนดำเนินการคำสั่งแรก
Objective-C Runtime เป็นส่วนที่ทรงพลังที่สุดของ iOS Runtime ซึ่งแตกต่างจาก C++ ที่มีการผูกก่อน (early binding) Objective-C ใช้การผูกหลัง (late binding) ผ่าน message passing การเรียกเมธอด [receiver message] จะไม่ถูกคอมไพล์เป็นการเรียกฟังก์ชันโดยตรง แต่เป็น objc_msgSend(receiver, @selector(message)) ซึ่งค้นหาการใช้งานเมธอดในคลาสของอ็อบเจ็กต์แบบไดนามิก
อ็อบเจ็กต์ Objective-C แต่ละตัวจะเก็บพอยน์เตอร์ isa ไปยังคลาสของมัน คลาสประกอบด้วยรายการเมธอด แคชเมธอด และพอยน์เตอร์ไปยังซูเปอร์คลาส objc_msgSend จะ遍历ห่วงโซ่การสืบทอด: ตรวจสอบแคชของคลาส จากนั้นรายการเมธอด จากนั้นไปยังซูเปอร์คลาส หากไม่พบเมธอด การส่งต่อจะถูกเรียก: resolveInstanceMethod, forwardingTargetForSelector และ forwardInvocation
Method Swizzling คือเทคนิคการสลับการใช้งานเมธอดทันทีผ่านการแลกเปลี่ยน IMP (พอยน์เตอร์การใช้งาน) ใน runtime ใช้สำหรับการทดสอบ A/B การวิเคราะห์ (การติดตามหน้าจออัตโนมัติ) และการตรวจสอบ ไม่แนะนำสำหรับโปรดักชันโดยไม่จำเป็นอย่างยิ่ง เพราะอาจขัดแย้งกับการอัปเดตระบบปฏิบัติการ
// Method Swizzling สำหรับการติดตาม viewDidLoad
#import
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(swizzled_viewDidLoad);
Method originalMethod = class_getInstanceMethod(
class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(
class, swizzledSelector);
BOOL didAddMethod = class_addMethod(
class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod)
);
if (didAddMethod) {
class_replaceMethod(
class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod)
);
} else {
method_exchangeImplementations(
originalMethod, swizzledMethod);
}
});
}
- (void)swizzled_viewDidLoad {
// การติดตามเหตุการณ์
NSLog(@"View Did Load: %@", self.class);
// เรียกใช้งานดั้งเดิม
[self swizzled_viewDidLoad];
}
@end หมวดหมู่ UIViewController (Tracking) แทนที่ viewDidLoad ด้วย swizzled_viewDidLoad ใน UIViewController ทั้งหมดในแอปพลิเคชัน dispatch_once รับประกันการ swizzling เพียงครั้งเดียว class_addMethod ป้องกันการ swizzling ซ้ำและความขัดแย้งกับซูเปอร์คลาส ใช้สำหรับการติดตามการแสดงหน้าจออัตโนมัติในการวิเคราะห์โดยไม่ต้องแก้ไขซอร์สโค้ดของคอนโทรลเลอร์
ใน iOS สมัยใหม่ (arm64) Apple ได้ปรับพอยน์เตอร์ isa ให้เหมาะสม: มันไม่ใช่แค่ที่อยู่คลาส แต่เป็นฟิลด์บิต (non-pointer isa) ที่มีแฟล็กการจัดการหน่วยความจำและข้อมูลคลาส Tagged pointers เป็นการปรับให้เหมาะสมอีกอย่าง: ค่า NSNumber, NSDate และ NSString ขนาดเล็กจะไม่ถูกเก็บเป็นอ็อบเจ็กต์ในฮีป แต่ถูกเก็บในพอยน์เตอร์โดยตรง ช่วยลดโอเวอร์เฮดของ malloc และ retain/release Tagged pointer ถูกจดจำด้วยบิตที่มีนัยสำคัญน้อยที่สุดของ isa
Swift Runtime แตกต่างจาก Objective-C Runtime โดยพื้นฐาน: Swift โดยค่าเริ่มต้นใช้การจัดส่งแบบคงที่ (static dispatch) ผ่าน vtable สำหรับเมธอดของคลาส และ direct call สำหรับ value types และเมธอดส่วนขยาย การจัดส่งแบบไดนามิก (dynamic dispatch) ใช้เฉพาะกับเมธอดที่ทำเครื่องหมาย @objc หรือ dynamic เท่านั้น ซึ่งให้ประสิทธิภาพดีขึ้นถึง 40% เมื่อเทียบกับ Objective-C
Value types (struct, enum) ใน Swift เป็นความแตกต่างหลักจาก Objective-C พวกมันถูกเก็บไว้บนสแตกหรือภายในอ็อบเจ็กต์อื่น ไม่ใช้ retain/release และไม่มีส่วนร่วมใน ARC สำหรับการนับการอ้างอิง Struct ไม่มีพอยน์เตอร์ isa และไม่สามารถส่งผ่าน objc_msgSend ได้ Protocol witnesses เป็นอะนาล็อกของ vtable สำหรับโปรโตคอล ซึ่งช่วยให้การจัดส่งแบบไดนามิกสำหรับ existential containers
Swift Runtime ยังรวมถึง generics ที่มีการ reification (reified generics ผ่าน mangled symbols) และ COW (Copy-on-Write) สำหรับการปรับ string, array, dictionary, set ให้เหมาะสม เมื่อคัดลอกคอลเล็กชัน การคัดลอกจริงจะเกิดขึ้นเมื่อมีการแก้ไขสำเนาใดสำเนาหนึ่งเท่านั้น ซึ่งช่วยลดโอเวอร์เฮดเมื่อส่งคอลเล็กชันระหว่างฟังก์ชัน
import Foundation
// Swift: การจัดส่งแบบคงที่ (vtable สำหรับคลาส)
class Animal {
func makeSound() { print("...") } // vtable
}
class Dog: Animal {
override func makeSound() { print("Woof") } // vtable override
}
// @objc dynamic: การจัดส่ง Objective-C Runtime
class Cat: Animal {
@objc dynamic override func makeSound() {
print("Meow")
} // objc_msgSend
}
// Struct — ไม่มีการจัดส่ง runtime
struct Cow {
func makeSound() { print("Moo") } // direct call
}
// Protocol with protocol witness
protocol SoundMaker {
func makeSound()
}
struct Duck: SoundMaker {
func makeSound() { print("Quack") }
}
// การใช้ existential container
let soundMakers: [SoundMaker] = [Dog(), Cow(), Duck()]
for maker in soundMakers {
maker.makeSound() // protocol witness dispatch
}
// การทดสอบประสิทธิภาพ
func testDispatch() {
let dog = Dog()
let cat = Cat()
var cow = Cow()
let start = CFAbsoluteTimeGetCurrent()
for _ in 0..<1000000 {
dog.makeSound() // vtable: ~3ns
cat.makeSound() // objc_msgSend: ~15ns
cow.makeSound() // direct: ~1ns
}
let elapsed = CFAbsoluteTimeGetCurrent() - start
print("Elapsed: (elapsed) sec")
}ตัวอย่างแสดงการจัดส่งสามประเภทใน Swift: vtable สำหรับ class (Dog), objc_msgSend สำหรับ @objc dynamic (Cat) และ direct call สำหรับ struct (Cow) Protocol witnesses ใน existential containers ([SoundMaker]) เพิ่มโอเวอร์เฮด ในทางปฏิบัติ Swift เลือกการจัดส่งแบบคงที่ทุกครั้งที่ทำได้ ให้ประสิทธิภาพใกล้เคียงกับ C
Swift Runtime ถูกออกแบบมาเพื่อความเข้ากันได้อย่างสมบูรณ์กับ Objective-C Runtime คลาส Swift ใดๆ ที่สืบทอด NSObject จะถูกลงทะเบียนใน Objective-C Runtime โดยอัตโนมัติและสามารถเรียกผ่าน objc_msgSend ได้ แอตทริบิวต์ @objc ทำให้เมธอด Swift สามารถเข้าถึงได้จาก Objective-C สะพาน String: Swift String จะถูกเชื่อมโยงไปยัง NSString โดยอัตโนมัติเมื่อส่งไปยัง API Objective-C (toll-free bridging)
ARC (Automatic Reference Counting) คือระบบจัดการหน่วยความจำใน iOS ที่ทำงานในเวลาคอมไพล์ คอมไพเลอร์ (Clang) วิเคราะห์อายุของอ็อบเจ็กต์และแทรกการเรียก retain/release/autorelease โดยอัตโนมัติ นักพัฒนาไม่จำเป็นต้องเรียกด้วยตนเอง — แตกต่างจาก Manual Retain-Release (MRR) ก่อน iOS 5 ARC ทำงานในระดับอ็อบเจ็กต์ Objective-C และ Swift แต่ไม่ทำงานสำหรับ value types (struct, enum)
อ็อบเจ็กต์ Objective-C และคลาส Swift แต่ละตัวมีตัวนับการอ้างอิง (retain count) ที่เก็บในฟิลด์ extra_rc ภายใน non-pointer isa เมื่อสร้างอ็อบเจ็กต์ retain count = 1 เมื่อ retain ตัวนับเพิ่มขึ้น เมื่อ release ตัวนับลดลง เมื่อตัวนับถึง 0 อ็อบเจ็กต์จะถูกจัดสรรคืนผ่าน dealloc (Objective-C) หรือ deinit (Swift) ARC ปลอดภัยต่อเธรด: retain/release ใช้การดำเนินการอะตอมมิก (OSAtomicIncrement32/OSAtomicDecrement32)
Retain cycles เป็นปัญหาหลักของ ARC หากอ็อบเจ็กต์ A มีการอ้างอิงแบบ strong ไปยัง B และ B มีการอ้างอิงแบบ strong ไปยัง A อ็อบเจ็กต์ทั้งสองจะไม่ถูกจัดสรรคืนเพราะตัวนับการอ้างอิงจะไม่มีวันถึงศูนย์ ทางออกคือการอ้างอิงแบบ weak (__weak ใน Objective-C, weak ใน Swift) หรือการอ้างอิงแบบ unowned การอ้างอิงแบบ weak ไม่เพิ่ม retain count และถูกตั้งเป็นศูนย์ (nil) โดยอัตโนมัติเมื่ออ็อบเจ็กต์ถูกจัดสรรคืน
import Foundation
// ตัวอย่าง retain cycle
class Parent {
var child: Child?
deinit { print("Parent deallocated") }
}
class Child {
var parent: Parent? // strong — สร้าง retain cycle!
deinit { print("Child deallocated") }
}
var parent: Parent? = Parent()
var child: Child? = Child()
parent?.child = child
child?.parent = parent // วงจร: Parent -> Child -> Parent
parent = nil
child = nil
// deinit ไม่ถูกเรียก — หน่วยความจำรั่ว!
// การแก้ไข: weak
class WeakChild {
weak var parent: Parent? // weak — ไม่เพิ่ม retain count
deinit { print("WeakChild deallocated") }
}
// การแก้ไข: unowned (สำหรับอายุที่รับประกัน)
class UnownedChild {
unowned let parent: Parent
init(parent: Parent) { self.parent = parent }
deinit { print("UnownedChild deallocated") }
}
// การตรวจสอบผ่าน Instruments
func profileMemory() {
// 1. เรียกใช้ Instruments > Leaks
// 2. ดำเนินการที่สร้างอ็อบเจ็กต์
// 3. ตรวจสอบ Leaks เพื่อหารอยรั่ว
// 4. ใน Allocations ค้นหาอ็อบเจ็กต์ที่ไม่มี dealloc
for _ in 0..<1000 {
let p = Parent()
let c = WeakChild()
p.child = c as? Child
// c.parent = p — ไม่เพิ่ม, weak
}
}ตัวอย่าง retain cycle ระหว่าง Parent และ Child: ทั้งคู่มีการอ้างอิงแบบ strong ซึ่งกันและกัน ARC ไม่สามารถทำให้ตัวนับเป็นศูนย์ได้ การแก้ไขคือ weak parent ใน Child weak จะถูกตั้งเป็นศูนย์โดยอัตโนมัติเมื่อ parent ถูกจัดสรรคืน unowned สำหรับกรณีที่อายุของ parent รับประกันว่ายาวนานกว่า child (เช่น viewController และ view) ใช้ Instruments > Leaks เพื่อตรวจจับ retain cycles ตั้งแต่เนิ่นๆ
Autorelease pool คือกลไกการปล่อยที่เลื่อนออกไปสำหรับอ็อบเจ็กต์ที่สร้างขึ้นโดยไม่มีความเป็นเจ้าอย่างชัดเจน @autoreleasepool { } ใน Swift และ Objective-C สร้างพูลที่ถูกระบายเมื่อสิ้นสุดบล็อก โดยส่ง release ไปยังอ็อบเจ็กต์แต่ละตัวในพูล มีความสำคัญอย่างยิ่งในลูป (การสร้างอ็อบเจ็กต์ชั่วคราวหลายพันรายการ) และในเธรดพื้นหลังที่ไม่มี RunLoop UIKit RunLoop จะระบาย autorelease pool หลักโดยอัตโนมัติในทุกการวนซ้ำ
dyld (dynamic link editor) คือตัวโหลดระบบที่รับผิดชอบการโหลดไฟล์ปฏิบัติการ Mach-O และไลบรารีแบบไดนามิกที่เกี่ยวข้อง (dylib) เมื่อเปิดใช้แอปพลิเคชัน iOS dyld อยู่ที่ /usr/lib/dyld และเป็นส่วนหนึ่งของ libSystem กระบวนการโหลดรวมถึงหลายขั้นตอน: การวิเคราะห์ Mach-O, การโหลด dependencies (Library Loader, LC_LOAD_DYLIB), การย้ายตำแหน่งที่อยู่ (ASLR), การเริ่มต้น Objective-C Runtime และการเรียก main()
เวลาเปิดใช้แอปพลิเคชันขึ้นอยู่กับ dyld อย่างยิ่ง: ยิ่งมีไลบรารีแบบไดนามิกและคลาส Objective-C มากเท่าใด pre-main time ก็ยิ่งนานขึ้นเท่านั้น Apple แนะนำให้ลดจำนวนเมธอด +load (ทำงานก่อน main) และแทนที่ด้วย +initialize (การเริ่มต้นแบบขี้เกียจ) ตั้งแต่ปี 2020 Apple ใช้แคช dyld ที่สร้างไว้ล่วงหน้าบน iOS: ไลบรารีระบบถูกเชื่อมโยงล่วงหน้าในแคชเดียว ช่วยเร่งการโหลด
import Foundation
// การวัดเวลาเริ่มต้นผ่าน DYLD_PRINT_STATISTICS
// ใน Xcode: Edit Scheme > Run > Arguments > Environment Variables
// DYLD_PRINT_STATISTICS = 1
// DYLD_PRINT_STATISTICS_DETAILS = 1
// การวัด pre-main time แบบโปรแกรม
@main
struct AppMain {
static func main() {
let launchStart = CFAbsoluteTimeGetCurrent()
// UIApplicationMain เกิดขึ้นที่นี่
AppDelegate.main()
let launchEnd = CFAbsoluteTimeGetCurrent()
let preMainTime = launchEnd - launchStart
print("Pre-main time: (preMainTime) sec")
}
}
// การปรับให้เหมาะสม: แทนที่ +load ด้วย +initialize
class OptimizedClass {
// ❌ +load ทำงานก่อน main
// override class func load() { }
// ✅ +initialize ทำงานเมื่อเข้าถึงครั้งแรก
static let shared = OptimizedClass()
private init() {
// การเริ่มต้นที่นี่
}
}
// การปรับจำนวน dylib ให้เหมาะสม
// การรวมไลบรารีแบบคงที่ลดจำนวน LC_LOAD_DYLIB
// ใช้แฟล็ก -ObjC เพื่อเชื่อมโยงเฉพาะคลาส Objective-C ที่ใช้
// Xcode: Build Settings > Mach-O Type > Static Libraryในการวัด pre-main time ให้ใช้ DYLD_PRINT_STATISTICS ในสคีมา Xcode ผลลัพธ์จะแสดงเวลาทั้งหมด เวลาโหลด dylib เวลา rebase/bind เวลาตั้งค่า Objective-C และเวลาเริ่มต้น ค่าเป้าหมาย: ทั้งหมด < 400ms สำหรับการเริ่มต้นเย็น < 200ms สำหรับการเริ่มต้นอุ่น การปรับให้เหมาะสม: การรวมไลบรารี การแทนที่ +load ด้วย +initialize การลดจำนวนคลาส Objective-C (ใช้ Swift) จำนวนเฟรมเวิร์กแบบไดนามิกขั้นต่ำ
dyld shared cache คือแคชของไลบรารีระบบที่เชื่อมโยงล่วงหน้าบน iOS dylib ของระบบทั้งหมด (UIKit, Foundation, CoreGraphics) ถูกรวมเป็นไฟล์เดียว: /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 ซึ่งช่วยลดความจำเป็นในการโหลดไลบรารีระบบแต่ละรายการแยกกัน — dyld เข้าถึงแคช ซึ่งช่วยเร่งการเริ่มต้นได้อย่างมาก แอปพลิเคชันที่มี 10+ เฟรมเวิร์กแบบไดนามิกประสบความล่าช้ามากที่สุด เนื่องจาก dylib ที่กำหนดเองไม่รวมอยู่ใน dsc
คำถามที่พบบ่อย
iOS Runtime คือสภาพแวดล้อมรันไทม์ของแอปพลิเคชันบน iOS ซึ่งรวมถึง Objective-C Runtime (libobjc.dylib), Swift Runtime (libswiftCore.dylib), เฟรมเวิร์ก Cocoa Touch, dyld (ตัวโหลดแบบไดนามิก) และ ARC (การจัดการหน่วยความจำ) ให้ message passing สำหรับ Objective-C, การจัดส่งแบบคงที่สำหรับ Swift, การโหลดไฟล์ Mach-O และการจัดการหน่วยความจำอัตโนมัติ
Objective-C Runtime ใช้การผูกแบบไดนามิกผ่าน objc_msgSend (message passing) ด้วยการผูกหลัง Swift Runtime ใช้การจัดส่งแบบคงที่ (vtable สำหรับคลาส, direct call สำหรับ struct) เพื่อประสิทธิภาพ @objc dynamic เปิดใช้งาน Objective-C Runtime สำหรับคลาส Swift Swift struct ไม่มีพอยน์เตอร์ isa และไม่ใช้ retain/release
ARC (Automatic Reference Counting) คือการจัดการหน่วยความจำในเวลาคอมไพล์ คอมไพเลอร์ Clang แทรกการเรียก retain/release โดยอัตโนมัติ อ็อบเจ็กต์แต่ละตัวมีตัวนับการอ้างอิง เมื่อถึงศูนย์ dealloc จะถูกเรียก Retain cycles (การอ้างอิง strong ร่วมกัน) ถูกป้องกันด้วยการอ้างอิง weak/unowned ใช้ Instruments > Leaks เพื่อตรวจจับการรั่วไหล
dyld คือตัวโหลดแบบไดนามิกสำหรับไฟล์ Mach-O มันโหลดไฟล์ปฏิบัติการและ dylib ทั้งหมดที่ขึ้นต่อกัน ดำเนินการย้ายตำแหน่ง (ASLR) เริ่มต้น Objective-C Runtime และเรียก main() Pre-main time ขึ้นอยู่กับจำนวน dylib และเมธอด +load ใช้ DYLD_PRINT_STATISTICS สำหรับการวัด การปรับให้เหมาะสม: รวมไลบรารี แทนที่ +load ด้วย +initialize
Method Swizzling คือเทคนิคการสลับ IMP (พอยน์เตอร์การใช้งาน) ของเมธอดทันทีผ่าน class_getInstanceMethod และ method_exchangeImplementations ของ Objective-C Runtime ใช้สำหรับการทดสอบ A/B การวิเคราะห์ (การติดตามหน้าจออัตโนมัติ) และการตรวจสอบ ไม่แนะนำในโปรดักชันโดยไม่จำเป็นอย่างยิ่ง ใน Swift จะถูกแทนที่ด้วย @objc dynamic + Method Swizzling
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม