Objective-C Programming Chapter 5

Memory Management

ถ้าหากเราเขียนโปรแกรมด้วยภาษาที่เป็น interpreter เช่นภาษาสคริปต่างๆอย่างภาษา php , perl เราไม่จำเป็นต้องคำนึงถึงหน่วยความจำให้ปวดหัว จะประกาศตัวแปรก็ประกาศได้ไม่ต้องคิดเรื่องขนาดของหน่วยความจำหรือชนิดของตัวแปรต่างๆให้ยุ่งยากเพราะทุกๆอย่างถูกจัดการให้หมดแล้ว แต่ภาษาที่เป็น compiler อย่าง Objective-C , C++ กลับยุ่งยากกว่ามาก เราต้องกำหนดชนิดของตัวแปรต่างๆให้กับคอมไพลเลอร์และยังต้องคำนึงถึงขนาดของหน่วยความที่ใช้ในโปรแกรมเสมอ การบริหารหน่วยความจำจึงเป็นเรื่องจำเป็นอย่างยิ่งสำหรับการเขียนโปรแกรมด้วยภาษา Objective-C ในบทนี้เราจะได้เรียนรู้และเข้าใจเกี่ยวกับหน่วยความจำ รวมถึงการบริหารจัดการหน่วยความจำของโปรแกรม

 

Virtual address space

ก่อนที่จะไปเจาะลึกเกี่ยวกับ memory เราควรจะรู้เรื่อง Virtual address space เสียก่อนเพื่อจะได้เข้าใจเนื้อหาของบทนี้ได้ง่ายมากขึ้น รูปภาพ 1 เป็นภาพแสดง Vitual address space เมื่อโปรแกรมเริ่มทำงาน

 

 

หลังจากที่โปรแกรมได้ถูกโหลดเข้าสู่หน่วยความจำ ระบบปฎิบัติการจะแบ่งหน่วยความจำให้กับโปรแกรมเป็น 4 ส่วนใหญ่ๆ

Code Segment

หรือ Text Segment คือส่วนของพื้นที่หน่วยคำความจำที่เป็นส่วนของโค้ดโปรแกรม ( executable instruction ) รวมถึงข้อมูลที่ไม่สามารถแก้ไขได้เช่นข้อความต่างๆที่ใช้ในโปรแกรม ปกติพื้นที่ส่วนนี้จะไม่อนุญาติให้โปรแกรมแก้ไขค่าใดๆ

Data Segment

เป็นส่วนที่ไว้เก็บตัวแปรแบบ Global และแบบ Static ที่ได้ประกาศไว้ในโปรแกรม พื้นที่ส่วนนี้อนุญาติให้เข้าแก้ไขค่าต่างๆได้ แต่ขนาดของพื้นจะมีขนาดคงที่ เพราะถูกกำหนดมาตั้งแต่ตอนคอมไพล์แล้ว

Stack

คือพื้นที่ของหน่วยความจำที่มีการทำงานเป็นแบบ เข้าทีหลัง-ออกก่อน ( Last in – First Out ) เหมือนการเอาสิ่งของลงในกล่อง ของที่ถูกใส่เข้ามาทีหลังจะอยู่บนสุดและจะถูกนำออกก่อนของที่อยู่ด้านล่าง พื้นที่ส่วนนี้เป็นส่วนสำหรับการใช้งานของ thread และ function เมื่อฟังชั่นเริ่มทำงานโปรแกรมจะสร้าง Stack Frame ไว้ให้เป็นพื้นที่ไว้สำหรับการทำงานของ function ตัวแปรและค่าต่างๆที่จำเป็นต้องใช้สำหรับฟังชั่นจะถูกใส่เข้ามายัง Stack Frame นี้และหลังจากนั้น Stack Frame จะถูกใส่เข้ามายัง Stack ( Call Stack ) เมื่อฟังชั่นทำงานจบลงข้อมูลก็จะถูกถอนออกจาก Stack ยกตัวอย่างเช่น ฟังชั่นวาดสี่เหลี่ยมระบบจะสร้าง Stack Frame ของฟังชั่นใส่เข้าไปยัง Call Stack ภายในฟังชั่นนี้ได้เรียกฟังชั่นวาดเส้นทั้งสี่ด้าน Stack Frame ของฟังชั่นการวาดเส้นก็จะถูกใส่เข้าไปทับด้านบนของฟังชั่นวาดสี่เหลี่ยม เมื่อวาดเส้นเสร็จ Stack Frame ของการวาดเส้นจะถูกนำออกก่อนและหลังจากนั้นฟังชั่นวาดสี่เหลี่มก็จะถูกนำออกมาทีหลัง ข้อดีของ Stack คือการทำงานที่รวดเร็วกว่าการทำงานของ heap และมั่นใจได้ว่าข้อมูลที่ใช้ใน stack จะถูกลบออกโดยอัตโนมัติ โปรแกรมเมอร์จึงไม่ต้องไปจัดการอะไรกับหน่วยความจำตรงส่วนนี้ และเนื่องจากว่าข้อมูลต่างๆจะถูกลบหลังจากที่ฟังชั่นทำงานเสร็จจึงต้องคัดลอกข้อมูลไปเก็บไว้ที่อื่นเสียก่อน ดังนั้น Stack จึงเหมาะกับข้อมูลที่เป็นลักษณะชั่วคราว ข้อเสียของ Stack คือพื้นที่มีจำกัดมาก ถ้าจองหน่วยความจำตรงส่วนนี้มากเกินไปจะเกิดปัญหาโปรแกรมปิดตัวเนื่องจากเกิด Stack Overflow ได้

Heap

คือพื้นที่ว่างส่วนที่เหลือของหน่วยความจำ ถ้าหากโปรแกรมต้องการหน่วยความจำเพิ่ม ระบบสามารถเพิ่มหน่วยความจำส่วนนี้ให้กับโปรแกรมได้ ( Dyanamic allocation ) เมื่อเราต้องการหน่วยจำความเพิ่มก็จะขอให้ระบบจองพื้นที่ให้กับเรา ระบบจะทำการหาพื้นที่ว่างและจัดสรรพื้นที่ให้ หลังจากเลิกใช้หน่วยความจำตรงส่วนนี้แล้วก็ต้องบอกให้ระบบทำการลบหน่วยความจำด้วย ถ้าไม่อย่างน้ันพื้นที่ส่วนนี้จะยังคงอยู่จนกว่าจะปิดโปรแกรม เว้นแต่จะมี Garbage Collection ที่เป็นผู้ช่วยคอยตรวจสอบส่วนที่ไม่ได้ใช้และคืนให้กับระบบโดยอัตโนมัติ

 

Storing data in memory

ข้อมูลต่างๆของโปรแกรมที่อยู่ในหน่วยความจำจะมีหมายเลขไว้อ้างถึงตำแหน่งของฮาร์ดแวร์จริง ( Physical Address ) ซึ่งจะเป็นเลขฐาน 16 เช่น 0xFFFFF เป็นต้น การเข้าถึงข้อมูลที่อยู่ในหน่วยความจำสามารถทำได้สองวิธีคือ การอ้างอิง Physical Address โดยตรง เราสามารถหาค่าของ Physical Adress ของตัวแปรได้โดยการเติมเครื่องหมาย & นำหน้าตัวแปรนั้นและสามารถแสดงค่าออกมาด้วย NSLog ได้ด้วย Format String คือ %p ดังตัวอย่าง

ที่ผ่านมาเราเคยอ้างถึง physical address ไปบ้างแล้วจากการใช้คำสั่ง scanf และวิธีที่สองคือใช้ตัวแปร Pointer ซึ่งเป็นตัวแปรเก็บที่ค่า Physical Address และเราจะได้ทำความเข้าใจเกี่ยวกับ Pointer มากขึ้นในบทนี้

 

Local Variable

ตัวแปรที่ประกาศในฟังชั่นจะเรียกว่า automatic variable หรือ local variable ตัวแปรแบบนี้จะทำงานบน Stack เช่นตัวอย่าง

เมื่อเรียกใช้งานก็จะมีโค้ดดังเช่น

จากโค้ดได้ประกาศตัวแปร integer ชื่อ area ตัวแปรชนิดนี้เป็น local variable อากิวเม้น heigh และ width ก็เป็น local variable เช่นกัน ตัวแปรทั้งสามนี้จะถูกสร้างขึ้นเฉพาะตอนเรียกคำสั่งคำสั่งนี้เท่านั้นและหลังจากจบคำสั่งตัวแปรเหล่านี้จะถูกทำลายอัตโนมัติ ดังนั้นถ้าหากเราต้องการจะเก็บค่า area นี้ไว้ก็ต้องหาตัวแปรอื่นรับไว้ซึ่งนั่นก็คือ rectArea นั่นเอง

 

Scope for local variable

เมื่อประกาศตัวแปรแบบ local variable ขอบเขตของตัวแปรจะเริ่มจากบรรทัดที่ประกาศ เราไม่สามารถที่จะใช้งานก่อนการประกาศตัวแปรได้ และหลังจากจบฟังชั่นนั้นเราก็ไม่สามารถที่จะเข้าถึงตัวแปรนั้นได้ ขอบเขตของตัวแปรแบบ local vairable ยังสามารถกำหนดได้ด้วยเครื่องหมาย { } ถ้าเราประกาศตัวแปรอยู่ภายใน { } จะไม่สามารถนำออกไปใช้งานนอก { } นั้นได้ ตัวอย่างเช่น

External Variable

ตัวแปรแบบ external คือตัวแปรที่ทุกๆฟังชั่นหรือเมธอดสามารถเข้าถึงได้ หรือเรียกว่า Global Variable ตัวแปรแบบนี้คอมไพเลอร์จะนำไปไว้ในส่วนของ data segment เมื่อโปรแกรมทำงานตัวแปรนี้จะคงอยู่ในหน่วยความจำจนกว่าโปรแกรมจะปิดตัว

 

Scope for external variable

ลองดูตัวอย่างโปรแกรมเล็กๆที่เขียนขึ้นมาทดสอบขอบเขตของตัวแปร external กันสักโปรแกรม โดยที่โปรแกรมนี้มีคลาสชื่อ Demo และมีโค้ดของโปรแกรมดังนี้

Program 5.1

Program 5.1 Output

Global 12

โค้ดของโปรแกรม 5.1 เป็นโค้ดที่ออกแบบไม่ดี เพราะเขียน main program , interface และ implement ไว้ไฟล์เดียวกัน แต่จุดประสงค์ของโปรแกรมก็เพื่อจะทดสอบการใช้ Global Variable ถ้าดูจากโปรแกรมตัวแปร gVariable นั้นประกาศอยู่บนสุดของโปรแกรม ไม่ได้เป็นตัวแปรของคลาส Demo หรือ main เลย แต่ตัวแปรนี้ไม่ว่าจะเป็นส่วนใดๆของโปรแกรมก็สามารถเข้าถึงตัวแปรนี้ได้

 

Storage Class Specifiers

เราเคยใช้ storage class specifier มาแล้วโดยที่ไม่รู้ตัว นั่นคือ const เพื่อกำหนดว่าตัวแปรนี้ไม่สามารถแก้ไขได้ ในภาษา Objective-C ยังมี storage class specifiers อื่นๆเพื่อกำหนดคุณลักษณะของตัวแปรได้ด้วย keyword ต่อไปนี้

 

auto

การใช้ auto นำหน้าตัวแปรก็เพื่อประกาศว่าตัวแปรนี้เป็นตัวแปรแบบ automatic การประกาศตัวแปรต่างๆในฟังชั่นหรือเมธอดตัวแปรจะถูกตั้งเป็น auto โดยอัตโนมัติ เพียงแต่ไม่มีคำว่า auto นำหน้าให้เห็นเท่านั้น ดังนั้นเราแทบจะไม่เห็นการประกาศตัวแปรแบบ auto นี้เลย

const

เราได้ใช้ const ไปตั้งแต่บทแรกๆแล้ว การประกาศ const ก็เพื่อบอกว่าตัวแปรนี้เป็นค่าคงที่ ไม่สามารถแก้ไขได้ ในระหว่างที่คอมไพล์หากเจอว่าเราได้แก้ไขค่าของตัวแปร ก็จะแจ้ง error ให้ทราบทันที

เมื่อประกาศตัวแปร const ต้องทำการ initialize ค่าตั้งแต่ประกาศตัวแปร เพราะว่าค่าของ const ไม่สามารถแก้ไขได้

 

volatile

ตัวแปรแบบนี้ตรงข้ามกับ const เพราะการประกาศตัวแปรแบบ volatile เป็นการบอกกับคอมไพเลอร์ว่าตัวแปรนี้เป็นตัวแปรที่เปลี่ยนแปลงค่าได้ อาจจะสงสัยว่าทำไมต้องระบุว่าเปลี่ยนแปลงค่าได้เพราะตัวแปรอื่นๆก็เปลี่ยนแปลงได้อยู่แล้ว แต่ที่จำเป็นต้องระบุว่าตัวแปรนี้เปลี่ยนแปลงได้ ก็เพราะว่าบางครั้งคอมไพลเลอร์เมื่อคอมไพล์โปรแกรมจะมีการ Optimize โค้ดให้ทำงานได้เร็วขึ้น ทำให้อาจจะตัดโค้ดบางส่วนออกไป ยกตัวอย่างเช่น สมมติว่าเราเขียนโปรแแกรมให้แสดงข้อความไปยังจอ LED ที่เชื่อมต่อยังพอร์ท USB เพื่อที่จะส่งข้อความออกไปยังจอได้จำเป็นต้องมีตัวแปรที่ไว้เก็บตำแหน่งของ Hardware เช่นสมมติชื่อ usbLED ถ้าอยากจะแสดงผลคำว่า Hello หลังจากนั้น 1 นาทีให้แสดงผลคำว่า Hi ก็อาจจะมีโค้ดดังนี้

ตั้งแต่ประกาศตัวแปร usbLED และให้ค่าเป็นคำว่า Hello โค้ดส่วนการรอ 1 นาทีไม่ได้มีโค้ดส่วนใดที่ใช้ค่าหรือแก้ไขค่านี้เลย จนกระทั่งให้ค่าใหม่เป็น Hi คอมไพลเลอร์ก็อาจจะเข้าใจว่า การให้ค่า Hello นั้นไม่มีประโยชน์จึงได้ให้ usbLED เป็นค่าคำว่า Hi ตั้งแต่แรกและตัดบรรทัดสุดท้ายทิ้งไป กรณีแบบนี้โปรแกรมย่อมทำงานผิดไปจากที่เราต้องการ ดังนั้นเราจึงต้องบอกให้คอมไพลเลอร์รู้ว่า ตัวแปรนี้เป็นตัวแปรที่มีการเปลี่ยนแปลงเสมอ

 

static

ปกติเมื่อเราใช้ตัวแปรแบบ local variable เมื่อเราเรียกเมธอดเดิมหลายๆครั้ง ทุกครั้งๆที่เรียกค่าของตัวแปรจะถูกตั้งค่าใหม่เสมอ แต่ถ้าเราอยากให้ตัวแปรนี้สามารถใช้ค่าเดิมจากการเรียกเมธอดหรือฟังชั่นของครั้งที่แล้ว เราสามารถทำได้โดยใช้ static

ตัวอย่างเช่น เพิ่มเมธอด printStatic ในคลาส Demo ในโปรแรกม 5.1 ที่ผ่านมา ซึ่งมีโค้ดดังนี้

และเพิ่มคำสั่งให้เรียก printStatic ใน main สองครั้งดังเช่นตัวอย่าง

เมื่อเรียกคำสั่ง printStatic ครั้งแรกตัวแปร demoStatic จะมีค่าเป็น 0 และหลังจากจบฟังช่ันจะมีค่าเป็น 1 แต่ถ้าเราเรียก printStatic ครั้งที่สองตัวแปร demoStatic จะใช้ค่าเดิมของครั้งที่แล้วนั่นคือค่า 1 ไม่ใช่ค่า 0 ดังนั้นเมื่อจบฟังช่ันก็จะมีค่าเป็น 2

Program 5.1 Output ( Print Static )

Static 1
Static 2

 

ตัวแปรแบบ static นี้คอมไพลเลอร์จะนำตัวแปรไปไว้ในส่วนของ data segment และถ้าเราไม่ได้ตั้งค่าเริ่มต้นให้กับตัวแปร คอมไพลเลอร์จะให้ค่าเป็น 0 โดยอัตโนมัติ

 

Extern

ใช้สำหรับการอ้างถึงตัวแปรแบบ external ( Global ) ที่อยู่คนละไฟล์ จากโปรแกรม 5.1 ถ้าหากเราประกาศคลาส Demo แยกไฟล์กับ main เราจะไม่สามารถใช้ตัวแปร gVariable ได้ เพราะอยู่คนละไฟล์ แต่ถ้าเราใช้ extern นำหน้าตัวแปรคอมไพลเลอร์จะรู้ว่าตัวแปรนี้ได้มีการประกาศไว้แล้วที่ไฟล์อื่นและจะเชื่อมตัวแปรนี้เข้าไว้ด้วยกันเป็นตัวเดียว

 

Program 5.2 Main Program

 

Program 5.2 Interface

Program 5.2 Implement

Program 5.2 Output

Global 18
Static 1
Static 2

Register

ใช้สำหรับตัวแปรที่มีการใช้งานหรืออ้างอิงบ่อยมากๆ คอมไพลเลอร์อาจจะนำเอาตัวแปรแบบนี้ไปไว้ใน register ของ CPU เพื่อให้ทำงานได้เร็วขึ้น

 

Dynamic allocation

การประกาศตัวแปรแบบต่างที่ผ่านมาเช่น int x , float y เราเรียกการประกาศแบบนี้ว่า static allocation ( คนละอย่างกับตัวแปร static ) เพราะว่าเราได้บอกคอมไพลเลอร์ถึงชนิดและจำนวนหน่วยความจำของตัวแปรเหล่านี้ตั้งแต่ตอนคอมไพล์ สมมติเขียนโปรแกรมเก็บค่าคะแนนของนักเรียน ถ้าเรารู้ว่าผู้ใช้งานต้องการป้อนข้อมูลของนักเรียนทั้งหมดสิบคน เราก็คงสามารถบอกกับคอมไพลเลอร์ได้ว่าเราต้องการจองหน่วยสำหรับนักเรียนสิบคน แต่ถ้าเราไม่รู้ว่าจำนวนนักเรียนที่ผู้ใช้ต้องการจะป้อนเข้ามาในโปรแกรมละจะทำอย่างไร ? ถ้าหากเรากำหนดให้โปรแกรมรับได้สูงสุดคือ 5 คน ก็อาจจะน้อยไป แต่ถ้ากำหนดให้มากกว่านั้นเช่น 1000 คน ก็มากไปหน่วยความจำอาจจะไม่พอ จะเห็นได้ว่ากำหนดน้อยไปหรือมากไปก็มากไปก็เกิดปัญหา วิธีการแก้ปัญหานี้ก็คือเราต้องให้โปรแกรมของเราสามารถขยายหรือลดขนาดของหน่วยจำเองได้ นั่นก็คือ dynamic allocation นั่นเอง การประกาศ Instance ของ Class เป็นการประกาศตัวแปรแบบ dynamic allocation เราเคยได้เห็นไปแล้วดังตัวอย่าง

จากโค้ดตัวอย่างโปรแกรมจะขอหน่วยความจำไปยังระบบ หลังจากนั้นระบบจะจองหน่วยความจำในส่วน heap อย่างที่ได้ทราบกันไปแล้วว่าการใช้พื้นที่ตรงส่วนนี้หลังจากใช้งานเราต้องเป็นคนบอกให้ระบบลบออก ดังนั้นเราจึงต้องใช้คำสั่ง release เพื่อบอกว่าเราไม่ได้ใช้มันแล้ว

จะเห็นว่า Object ทุกตัวอยู่ในส่วนพื้นที่ของ heap อย่างไรก็ตามในระบบปฎิบัติการรุ่นใหม่ตั้งแต่ 10.6 ขึ้นไป Object บางตัวสามารถอยู่ในส่วนของ Stack ได้ โดยการใช้ Block ซึ่งเราจะพูดถึง Block กันในบทหลังๆ

 

Reference counting

เมื่อ alloc ก็ต้อง release อาจจะเข้าใจว่า release คือคำสั่งให้ลบ Object นั้นออกจากความจำ แต่ความหมายจริงๆของการเรียก release คือคำสั่งที่บอกกับระบบว่า เราไม่ได้ใช้ Object นี้อีกต่อไป หลังจากนั้นระบบจะไปจัดการเองโดยการตรวจสอบว่ามีใครใช้อยู่อีกหรือเปล่าถ้าไม่มีก็จะทำการลบข้อมูลตรงส่วนนั้นออก แล้วระบบรู้ได้อย่างไรว่าไม่มีคนใช้ ? เบื้องหลังการทำงานทั้งหมดคือ reference counting นั่นเอง การทำงานของระบบนี้คือ Object แต่ละตัวจะมีค่าที่เรียกว่า retain count ซึ่งเป็นค่าที่ใช้นับผู้ที่ใช้งาน Object นี้ เมื่อมีการเรียก alloc ตัวแปรนั้นจะถูกเพิ่มค่า retain count โดยอัตโนมัติ และหลังจากที่เราไม่ได้ใช้ตัวแปรนั้นอีกต่อไปแล้วเราก็จะใช้ release เพื่อเป็นการลดค่า retain count ถ้าหาก retain count มีค่าเป็น 0 ก็หมายความว่าไม่มีใครใช้แล้ว ระบบก็จะทำการเรียกเมธอด dealloc ของ Object ตัวนั้นเพื่อลบข้อมูลของตัวแปรนี้ออกจาก heap นั่นเอง นอกจากนี้เราสามารถเพิ่มค่า retain count ได้ด้วยคำสั่ง retain โดยตรงก็ได้ แต่ยังไม่ต้องงงว่าทำไมต้องเพิ่มค่า retain count เพราะเดี๋ยวจะได้ใช้ในบทต่อไป

ดูแผนภาพประกอบ

เราสามารถสั่งให้แสดงค่า retain count ได้ด้วยเมธอด retainCount ดังตัวอย่าง

นอกจากนี้ยังมีเมธอดที่เพิ่มค่า retain count ให้กับ Object คือเมธอด copy และ mutableCopy ซึ่งเราจะได้ทำความเข้าใจกับ copy ในบทต่อไป ฉะนั้นแล้ว 4 คำสั่งดังต่อไปนี้เป็นการเพิ่มค่า retain count คือ alloc , retain , copy , mutableCopy ทุกครั้งที่ใช้คำสั่งเหล่านี้ต้องจำไว้เสมอว่า ต้อง release ให้ตรงกับจำนวนที่ใช้ เพราะถ้าหาก release ไม่ครบจำนวน ก็จะกลายเป็น memory leak แต่ถ้าเรา release มากเกินไปก็จะกลายเป็น error ดังตัวอย่าง

Program 5.3

Program 5.3 Output
malloc: *** error for object 0x10010cba0: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug

 

จาก error log ได้แจ้งเราว่า object ตำแหน่งที่ 0x10010cba0 ไม่อนุญาติให้ลบหรือลบไม่ได้ ก็เพราะว่าระบบได้ลบหน่วยความจำส่วนนั้นไปแล้ว เมื่อมาลบซ้ำก็หา Object นั้นไม่เจอ จึงเกิด error ขึ้น และเมื่อดูที่ Editor โปรแกรมจะแจ้งบรรทัดที่เกิดปัญหาให้ด้วยดังรูป

 

 

ยังมีอีกกรณีคือถ้าหาก Object นั้นถูกลบไปแล้ว แต่ยังมีอ๊อบเจ๊กอื่นบางตัวเข้าใจว่า อ๊อบเจ๊กนี้ยังมีอยู่ในระบบ เมื่อเรียกใช้ Object ที่ถูกลบไปแล้วก็จะเกิด error ขึ้นเช่นกันหรือเรียกว่า dangling pointer ซึ่งจะมีตัวอย่างในบทต่อไป

 

Autorelease

สมมติว่าเราเขียนเมธอดที่สร้าง instance ใหม่ให้เราสักตัว ดังเช่นตัวอย่างโค้ดต่อไปนี้

เมธอดนี้จองหน่วยความจำให้กับอ๊อบเจ็กใหม่และส่งอ๊อบเจ็กนั้นกลับคืนมาให้ เราก็สามารถใช้ Object นั้นได้ตามปกติ โค้ดนี้ทำงานได้แทบจะสมบูรณ์ แต่เมื่อจองหน่วยความแล้วก็ต้องคืน ดังนั้นแล้วโค้ดนี้จึงก่อให้เกิด memory leak เพราะไม่ได้ release นั่นเอง งั้นเราก็แก้ปัญหานี้โดยการเพิ่ม release ดังตัวอย่าง

โค้ดตัวอย่างทำงานได้ครบถ้วนคือ alloc แล้วก็ต้อง release แต่เมื่อสั่งให้ release ค่า retain count ก็เป็น 0 ระบบก็จะลบออกจาก heap ถ้าฝืนใช้ก็ต้องเกิด freed object แน่นอน แล้วเราจะใช้ Object นั้นได้อย่างไร ?

เราสามารถแก้ปัญหานี้ได้ด้วยการโยนไว้กับคนอื่นจัดการแทน ซึ่งพระเอกผู้รับหน้าที่ของเราก็คือ Autorelease Pool ทีนี้ก็เห็นแล้วใช่ไหมว่าโค้ด

ที่เราได้ใช้มาตั้งแต่บทแรกเนี่ยกลายเป็นพระเอกของเราในทันที การฝาก Object ให้ autorelease pool เป็นคนจัดการให้เราง่ายนิดเดียว เราเพียงแค่เรียกเมธอด autorelase เท่านั้น ดังตัวอย่าง

เมื่อเราส่ง message autorelase บอกกับ Object นั้น เปรียบเสมือนโยน Object ลงสระน้ำที่ชื่อ autorelease pool อ๊อบเจ็กต่างๆก็จะลอยอยู่ในสระ ซึ่งเค้าจะเป็นคนทำหน้าจัดการดูแลวัตถุเหล่านี้ให้เราเอง วิธีการที่สระแห่งนี้จัดการกับอ๊อบเจ็กคือเมื่อ autorelease pool ได้รับแมสแซก drain หรือ release จากนั้นสระน้ำแห่งนี้จะส่ง release หาอ๊อบเจ็กทุกตัวที่อยู่ใน pool เหมือนเปิดท่อระบายน้ำและให้น้ำพัดสิ่งต่างๆออกไปจากสระนั่นเอง เมื่อดูภาพประกอบจะเข้าใจการทำงานของ autorelease pool มากขึ้น

 

 

 

การใช้ autorelease นั้นมีประโยชน์กับการสร้าง Object ที่ไว้ใช้ชั่วคราว ( Temporary ) เป็นอย่างมาก และใน Cocoa Framework มี method ที่เป็นลักษณะแบบ autoreleased object มากมาย ยกตัวอย่างเช่นโค้ดต่อไปนี้

Program 5.4

Program 5.4 Output
<Demo: 0x1001145d0>

 

จากโค้ด NSLog(@”%@”, [demo description]) จะเห็นว่าเราไม่ได้จัดรูปแบบการแสดงผลอะไรเลย แต่โปรแกรมจะแสดงผลลัพธ์อธิบายว่าตัวแปร sample นี้คือคลาส Demo อยู่ตำแหน่ง 0x1001145d0 แล้ว NSLog แสดงผลแบบนี้ได้อย่างไร ? ถ้ากลับไปดูตารางการใช้ NSLog จะเห็นว่า %@ ไว้แสดง Object เราก็พอจะเดาได้ว่า [demo description] ต้องมีการสร้าง Object อะไรสักอย่างให้เรา ถูกต้องแล้วนะครับ !!! เมธอด description นี้ได้สร้างอ๊อบเจ็กชนิด NSString ให้กับเรา ซึ่ง Object ตัวนี้เป็น autoreleased object นั่นเอง เพราะเราไม่ต้องการเอาวัตถุนี้ไปทำประโยชน์อย่างอื่น นอกจากใช้กับ NSLog บรรทัดนี้บรรทัดเดียว

เราไม่ควรจะให้ทุกๆ Object อยู่ใน Autorelease Pool เพราะเราไม่อาจจะรู้ได้ว่าเมื่อไหร่ Object นั้นจะถูก release และการที่ให้วัตถุทุกอย่างอยู่ใน autorelease จะทำให้ประสิทธิภาพของโปรแกรมลดลง และข้อเสียสำคัญคือ Object ที่อยู่ใน autorelease pool นั้น debug ยากกว่า reference counting ลองดูโปรแกรมตัวอย่าง

Program 5.5

Program 5.5 Output
malloc: *** error for object 0x1001131b0: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug

 

จากโปรแกรมเราได้สั่งให้อ็อบเจ็กนั้นไปอยู่ใน autorelease pool และทำการ release object ด้วย โปรแกรมจึงเกิดข้อผิดพลาดและแสดงผล error บอกเหมือนโปรแกรม 5.3 แต่ถ้าดูจาก Editor จะเห็นการแสดงผลดังนี้

 

 

จะเห็นว่าโปรแกรมหยุดที่บรรทัด [pool drain] พูดอีกอย่างคือจากโค้ดของโปรแกรมเมื่อทำงานไปเรื่อยๆ โดยที่ไม่เกิดปัญหา จนกระทั่ง pool ได้พยายาม release วัตถุแต่ทำไม่ได้เพราะได้ถูก release ไปก่อนหน้านี้แล้ว ลองนึกภาพว่ามีโค้ดก่อนหน้า [pool drain] สักหมื่นบรรทัดสิครับแล้วโปรแแกรมแจ้ง error ที่บรรทัดนี้ เราจะรู้ได้อย่างไรว่าวัตถุตัวไหนเกิดข้อผิดพลาด

แต่อย่างไรก็ตามยังมีวิธีที่จะหาข้อผิดพลาดได้อยู่โดยการปลุกผีดิบ NSZombie กันในบทของเทคนิคการ debug ครับ

 

Analyzer

เพื่อป้องกันความผิดพลาดของการใช้ alloc และ release ไม่สมดุลกัน Xcode มีเครื่องมือตรวจสอบโค้ดของเราว่ามีจุดเสี่ยงต่อการเกิดปัญหาที่ชื่อว่า Analyzer ความสามารถพิเศษในการค้นหาว่าจุดใดมีความเสี่ยง ตัวอย่างเช่นโปรแกรมต่อไปนี้

Program 5.6

 

โปรแกรมนี้มีข้อผิดพลาดเต็มไปหมด เราจะให้ Static Analyzer นี้ช่วยเราค้นหาจุดเสี่ยงต่อได้ด้วยการเลือกที่เมนู Product > Analyze ดังรูป

 

หลังจากน้ัน Xcode จะหาจุดเสี่ยงให้เราดังรูป

 

 

เราจะเห็นสีเหลี่ยมสีฟ้าเล็ก พร้อมกับคำอธิบายว่า Reference-counted object is used after it is released ก็แปรว่าจุดนี้ได้มีการเรียกใช้งาน วัตถุที่ได้ถูก release ไปแล้ว และถ้าเรากดคลิ้กเข้าไปดูเพิ่มเติม (ตรงสี่เหลี่ยมหน้าคำอธิบาย ) Xcode จะอธิบายลำดับของการปัญหาให้อีกด้วยดังรูป

การใช้ analyze นี้ช่วยให้เราสามารถเห็นข้อผิดพลาดได้ง่ายขึ้นมาก แต่อย่างไรก็ตามการใช้ analyze เป็นเพียงแค่การหาข้อผิดพลาดเบื้องต้นเท่านั้น ดังนั้นเราควรจะระมัดระวัง retain count ด้วยตัวเองเสียก่อน

 

Garbage Collector

ถ้าหากว่าเรารู้สึกว่าการใช้ระบบ retain count มันยุ่งยากมากมาย ก็สามารถให้ระบบจัดการให้เราก็ได้ โดยการใช้ Garbage Collector ซึ่งสามารถทำได้โดยการเปิดการใช้งานจาก Setting ดังภาพ

ถ้าหากเลือกเป็น unsupported ก็จะเป็นการปิดการใช้ garbage collector แต่ถ้าเลือก supported หรือ required ก็จะเปิดการเปิดใช้งาน ให้เลือก supported ในกรณีเปิด application แต่ถ้าเราสร้าง library ให้เลือก required

เมื่อเปิดการใช้งานแล้วให้เพิ่มอ๊อบเจ็ก NSGarbageCollector เข้าไปยังท้ายโปรแกรม ดังเช่นตัวอย่าง

 

 

และหลังจากนั้นเราก็ไม่ต้องไปสนใจ retain หรือ release ทั้งสิ้น เมื่อเลิกใช้ Object นั้นก็บอกให้อ๊อบเจ็กนั้นเป็นค่า nil แทน ดังเช่นตัวอย่าง

 

 

การใช้ Garbage Collector มีข้อเสียคือใช้หน่วยความจำมากกว่าปกติและเราไม่สามารถรู้ได้ว่าเมื่อไหร่จะทำงาน และการใช้ Garbage Collector นี้ไม่สามารถใช้กับ iOS ได้ ฉะนั้นแล้วถ้าหากต้องเขียนโปรแกรมบน iOS ไม่ว่าจะเป็น iPhone หรือ iPad เราไม่สามารถหลีกเลี่ยงการใช้ reference counting ได้เลย

 

Automatic Reference Counting

การจัดการบริหารหน่วยความจำสองอย่างที่เราได้รู้ไปก่อนหน้านี้ก็คือ Reference Counting ที่ใช้การนับ retain count เมื่อไหร่ที่มีการ retain ก็ต้อง release และ autorelease pool ก็เป็นส่วนหนึ่งของ retain count ส่วนระบบที่สองคือการใช้ Garbage Collector แต่อย่างไรก็ตามการใช้ Garbage Collector ก็มีข้อจำกัดหลายประการด้วยกัน ที่เห็นได้ชัดเจนก็คือ ไม่มีใน iOS นอกจากสองระบบนี้แล้วก็ยังมีระบบบริหารหน่วยความจำอีกอย่างนั่นก็คือ Automatic Reference counting หรือเรียกสั้นๆว่า ARC ซึ่งเป็นสิ่งใหม่ที่เพิ่งจะนำมาใส่ไว้เป็นส่วนหนึ่งของ Objective-C เมื่อไม่นานมานี้ ซึ่งเราจะได้เรียนรู้การใช้ ARC ในบทหลังๆ

 


interpreter คือตัวแปรคำสั่ง ทำงานตามชุดคำสั่งในทันที ไม่เหมือนกับ compiler ที่ต้องแปลงจากภาษาหนึ่งไปยังอีกภาษาหนึ่ง

Analyzer คือการนำเอาเครื่องมือ CLang Static Analyzer มารวมเข้ากับ Xcode

โหลดไฟล์ PDF – Chapter 5

17 thoughts on “Objective-C Programming Chapter 5”

  1. สุดยอด ครับ หาภาษาไทย ที่อธิบาย แบบนี้ ไม่ได้เลยครับ

    อ่าน 20 นาที แก้ข้อสงสัย ไปได้เยอะเลยครับ

    ขอบคุณครับ

  2. ตรงตัวอย่างที่ 5.2 ตรง file Demo.m
    ตรง extern int gVariable = 0;
    รู้สึกว่าตรงจะจะ error ว่า ‘extern’ variable cannot have an initializer อ่ะครับ

    รบกวนเช็คหน่อยครับ

  3. ขอบคุณค่ะ ช่วยให้เข้าใจการทำงานของ obj-c ได้เยอะเลยค่ะ

  4. เนื้อหาน่าสนใจมากครับ อ่านสนุกเลยหละ แต่ Chapter นี้ไม่มีเป็น PDF หรอครับ

  5. เข้ามาอ่านแล้ว อธิบายได้ดีมาก ๆ ครับ
    แล้วจะส่งต่อความรู้ด้วยการออกโปรแกรมดี ๆ ต่อไครับ

Leave a Reply