Objective-C Programming Chapter 16 (Part2)

ARC

 

วิวัฒนาการในการจัดการหน่วยความจำของภาษา Objective-C ได้เริ่มมาตั้งแต่การใช้ reference counting หรือการนับจำนวน release , retain ต่อมาก็มี autorelease pool และ garbage collector (gc) แต่โปรแกรมเมอร์ก็ยังประสบปัญหาในการจัดการหน่วยความจำอยู่ เช่น จำนวนของ release และ retain นั้นไม่เท่ากัน ซึ่งพบได้บ่อยในผู้ที่เริ่มเขียนโปรแกรมด้วยภาษา Objective-C นอกจากปัญหาเหล่านี้แล้ว เมื่อประกาศอ็อบเจ็กยังต้องพิจารณาอีกว่าอ็อบเจ็กควรจะมีการจัดการหน่วยความจำเป็นแบบ autoreleased หรือไม่ เช่น ควรประกาศสตริงด้วย [[NSString alloc] initWith.. ] หรือจะใช้ [NSString stringWith… ] เป็นต้น ตัวเลือกอื่นในการจัดการปัญหาก็คือ garbage collector แต่ผลที่ตามมาก็คือประสิทธิภาพลดลง และ ไม่สามารถใช้กับ iOS ได้ เพื่อแก้ปัญหานี้ Apple ก็ได้ออกเครื่องมือ ช่วยเหลือให้นักพัฒนาได้ใช้ตรวจสอบหน่วยความจำของโปรแกรม เช่น Static Analyzer และ Instrument แต่อย่างไรก็ตามนักพัฒนาโปรแกรมก็ยังต้องจัดการปัญหาหน่วยความจำนี้ ด้วยตัวเองเช่นเดิม แต่ปัญหาเหล่านี้จะหมดไปเพราะ llvm ได้เพิ่มวิธีการจัดการหน่วยความจำแบบใหม่ ที่สะดวกเหมือน garbage collector และยังมีประสิทธิเหมือนการใช้ reference counting นั่นก็คือ Automatic Reference Counting หรือเขียนย่อๆได้ว่า ARC

ARC คือการมอบหมายให้คอมไพลเลอร์เป็นคนจัดการระบบ reference counting แม้ว่าจะเป็นการจัดการแบบอัตโนมัติเช่นเดียวกับ garbage collector แต่การทำงานนั้นแตกต่างกัน เพราะการจัดการหน่วยความจำด้วย garbage collector นั้นจะทำงานในแบบ runtime ทำให้คอมไพลเลอร์ต้องรวมเอา garbage collector เป็นส่วนหนึ่งของโปรแกรม และนั่นเป็นสาเหตุทำให้โปรแกรมทำงานช้า เพราะมี garbage collector ทำงานอยู่อยู่เบื้องหลังตลอดเวลานั่นเอง แต่ในทางกลับกัน การทำงานของอาร์คจะเกิดขึ้นตอน compile time เพราะสิ่งที่อาร์คทำคือวิเคราะห์และจัดการเพิ่มโค้ดส่วนของ retain,release ให้กับอ็อบเจ็กโดยอัตโนมัติ ดังนั้นประสิทธิภาพของโปรแกรมก็ยังเหมือนเดิมเพราะใช้ reference counting เช่นเดิม แต่โปรแกรมเมอร์ทำงานสะดวกมากขึ้นเพราะไม่ต้องกังวลเรื่อง retain , release อีกต่อไป ยกตัวอย่างโค้ดง่ายๆ เช่น เมธอด getNewString ซึ่งจะสร้างอ็อบเจ็กสตริงและส่งกลับมา เมื่อเขียนในแบบ ARC จะได้โค้ดดังนี้

จากโค้ดตัวอย่าง ถ้าหากจัดการหน่วยความจำด้วยตัวเองจะเกิด memory leak อย่างแน่นอน เพราะไม่ได้กำหนดให้อ็อบเจ็กเป็น autorelease แต่เมื่อให้ arc เป็นคนจัดการ ในกระบวนการคอมไพล์ อาร์คจะวิเคราะห์และเพ่ิม autorelease ให้เองโดยอัตโนมัติ ดังนั้นเมธอด retain , release หรือ autorelease จึงไม่อนุญติให้ใช้งาน (ถ้าเรียกใช้เมธอดเหล่านี้จะคอมไพล์ไม่ผ่าน) และผลจากที่ไม่ต้องเขียน retain,release นี้เอง ส่งผลทางอ้อมคือไม่ต้องเขียนโค้ด dealloc เพื่อคืนหน่วยความจำให้กับระบบอีกด้วย

Enable & Disable ARC

โดยปกติเมื่อสร้างโปรเจคด้วย XCode 5 จะถูกกำหนดเป็น ARC ตั้งแต่เริ่มต้น อย่างไรก็ตามเราสามารถที่จะเลือก ปิด/เปิด การใช้ arc ได้ด้วยการเปลี่ยน Build Setting ตามรูป

 

enable_arc

 

Ownership qualifiers

การจัดการหน่วยความจำด้วยตัวเองใช้กฎการนับ retain,release แต่เมื่อเปลี่ยนมาใช้ ARC กฎเหล่านี้ก็ไม่จำเป็น แต่สิ่งต้องพิจารณาเพิ่มเติมก็คือตัวแปรที่ได้ครอบครองอ็อบเจ็กนั้นได้มีลักษณะเป็นแบบใด ซึ่งอาร์คได้แบ่งลักษณะของการครอบครองอ็อบเจ็ก 4 แบบ ด้วยกันคือ strong , weak , unsafe unretained และสุดท้ายคือ autoreleasing

strong
การครอบครองแบบ strong นี้จะถูกกำหนดเป็นค่าเริ่มต้นให้กับตัวแปร ใช้คีย์เวิร์ด __strong (ไม่จำเป็นต้องเขียน __strong) การทำงานซึ่งมีลักษณะคล้ายกับ retain คือหากมีตัวแปรชี้มายังอ็อบเจ็ก ก็จะถือว่าอ็อบเจ็กนั้นยังมีเจ้าของอยู่ แต่เมื่อไหร่ก็ตามที่ไม่มีใครครอบครอง อ็อบเจ็กก็จะถูกทำลายลงทันที เพื่อที่จะทำความเข้าใจความสัมพันธ์แบบ strong ลองพิจารณา โค้ดต่อไปนี้

เมื่อให้ตัวแปร obj ชี้ไปยัง @”App” ก็หมายถึงการกำหนดให้ obj ครอบครองอ็อบเจ็กนั้น และจากโค้ดได้กำหนด ref ให้ชี้ไปยัง obj ก็เปรียบเสมือว่า ref นี้เป็นเจ้าของอ็อบเจ็ก @”App” เช่นเดียวกันเพราะชี้ไปยังอ๊อบเจ็กเดียวกัน เมื่อแสดงภาพลักษณะของ ownership ก็จะได้ดังนี้

owner1

ต่อมาหากเปลี่ยนให้ obj ชี้ไปยังอ็อบเจ็กอื่นเช่น @”Demo” อ็อบเจ็ก @”App” จะยังไม่ถูกทำลาย เพราะมี ref ครอบครองอยู่

owner2

แต่เมื่อไหร่ก็ถามถ้า ref ได้ชี้ไปที่อื่น อ็อบเจ็ก @”App” นี้ก็จะถูกทำลายลง เพราะไม่มีเจ้าของนั่นเอง

owner3

และการประกาศพร็อพเพอร์ตี้ให้เป็นแบบ strong นี้ จะใช้คีย์เวิร์ด strong แทน __strong เช่น

weak
weak pointer คือการที่ตัวแปรชี้ไปยังอ็อบเจ็กนั้น แต่ไม่มีสิทธิครอบครองอ็อบเจ็กนั้น ลองพิจารณาโค้ดตัวอย่าง

ref ถูกกำหนดให้เป็น weak นั่นหมายถึงชี้ไปยัง @”App” และใช้งานได้ แต่ไม่ได้เป็นเจ้าของ เมื่อเขียนแสดงภาพก็จะได้ดังนี้

owner4

ถ้าหากตัวแปร obj เปลี่ยนไปยังอ็อบเจ็กอื่น อ็อบเจ็ก @”App” นี้จะถูกทำลายลงทันที เพราะ ref ไม่มีสิทธิที่จะครอบครองไว้ หลังจากอ็อบเจ็กได้ทำลายลง ตัวแปร ref จะถูกกำหนดให้เป็น nil โดยอัตโนมัติ

owner5

การที่ตัวแปร ref ถูกเปลี่ยนให้เป็น nil นี้เรียกว่า Zeroing Reference Pointer ซึ่งมีข้อดีคือป้องกันปัญหาพร้อยเตอร์ขี้ไปยังอ็อบเจ็กที่ตายไปแล้ว หรือที่เรียกทางเทคนิคว่า draggling pointer หรือเรียกอีกอย่างว่า zombie ส่วนการกำหนดพร็อพเพอร์ตี้ให้เป็นแบบนี้ให้ใช้คีย์เวิร์ด weak แทน __weak

unsafe unretained
ใช้คีย์เวิร์ด __unsafe_unretained มีลักษณะเหมือนกันกับ weak แต่ต่างกันตรงที่เมื่ออ็อบเจ็กถูกทำลาย ตัวแปรที่ชี้ไปยังอ็อบเจ็กนั้นจะไม่ถูกกำหนดให้เป็น nil โดยทั่วๆไปแทบจะไม่ใช้ unsafe unretained เพราะสามารถใช้ weak แทนได้อยู่แล้ว แต่มีบางสถานการณ์ที่จำเป็นต้องใช้ ownership แบบนี้ เช่น struct ของภาษา C เป็นต้นเพราะ ARC ไม่สามารถเข้าไปจัดการได้ และคลาส Cocoa บางคลาสไม่สามารถสร้าง zero link pointer ได้เช่นคลาส NSFont , NSTextView จึงจำเป็นต้องใช้ unsafe unretained

autoreleasing
การใช้ autoreleasing นี้จะใช้เมื่อเมธอดต้องการส่งอ็อบเจ็กที่เป็น autorelease ผ่านออกมาทางพารามิเตอร์ ปกติมักจะใช้กับคลาส NSError เช่น สมมติว่า เขียนเมธอดติดต่อกับ server โดยเมธอดนี้ จะส่งค่า YES เมื่อติดต่อสำเร็จ แต่ถ้าไม่สำเร็จจะส่ง NO พร้อมกับแจ้งปัญหาหากพบข้อผิดพลาดในการติดต่อ

 

เมื่อพิจารณาจากโค้ด ก็จะพบว่าเมธอดส่งค่ากลับมาได้เพียงค่าเดียวคือ BOOL แต่เราต้องการข้อมูลเพิ่มเติมในกรณีที่เกิดการทำงานผิดพลาด ดังนั้นเราจึงใช้พารามิเตอร์เป็นอีกช่องทาง ที่จะนำข้อมูลออกจากเมธอดได้ และสิ่งที่เมธอดนี้ได้สร้างขึ้นก็คืออ็อบเจ็ก NSError ดังนั้นจึงต้องกำหนดให้เป็น autorelease นั่นเอง เมื่อใช้งานก็จะมีลักษระโค้ดดังต่อไปนี้

How do I think about it ?

ARC ช่วยให้ไม่ต้องไปจัดการ retain,release แต่ก็ไม่ได้หมายความว่าเราไม่ต้องทำอะไรเพิ่มเติมเลย สิ่งที่ต้องพิจารณาก็คือการกำหนดความสัมพันธ์ระหว่างอ็อบเจ็กผ่านการกำหนด ownership แม้ว่าจะมี ownership ถึง 4 แบบ แต่การหลักการในการกำหนด ownership นี้ไม่ได้ไม่ได้ซับซ้อนหรือยุ่งยากแต่อย่างใด เพราะเมื่อดูจาก ownership ทั้ง 4 แบบ จะพบว่าโดยส่วนมากแล้วจะใช้เพียงแค่ strong กับ weak เท่านั้น มีเพียงบางกรณีเท่านั้นที่ใช้ autoreleasing หรือ unsafe unretained ดังนั้นในทางปฎิบัติแล้วสิ่งที่ต้องคิดจริงๆก็คือเมื่อไหร่จะใช้ strong เมื่อไหร่จะใช้ weak เท่านั้นเอง

อย่างที่ได้กล่าวไปว่า strong นั้นเป็นค่าพื้นฐาน ฉะนั้นโดยทั่วๆไปก็จะกำหนดให้เป็น strong ส่วนการใช้ weak จะใช้เพื่อหลีกเลี่ยงความสัมพันธ์ของอ็อบเจ็กที่อาจจะก่อให้เกิดการ circle of strong relationships หรือความสัมพันธ์แบบวงกลม ตัวอย่างเช่น A มีสมาชิกที่ครอบครอง B แต่ B กลับมีสมาชิกที่ครอบครอง A เช่นกัน ถ้าหากความสัมพันธ์ทั้งสองเป็นแบบ strong ก็จะเกิด memory leak แน่นอนเพราะต่างฝ่ายก็ครอบครองซึ่งกันและกัน เพื่อให้เข้าใจมากยิ่งขึ้น ลองพิจารณาเหตุการณ์ต่อไปนี้ เขียนโปรแกรมด้วย iOS เพื่อแสดงข้อมูลในตารางโดยใช้ UITableView และโดยทั่วไปแล้ว UITableView ก็จะอ้างอิงไปยัง delegate ซึ่งเป็นอ็อบเจ็กภายนอกที่ทำหน้าที่คอยพิจาณาว่า หากผู้ใช้งานกดที่ตารางจะมีการตอบสนองอย่างไร หรือเป็นคนตัดสินใจว่าข้อมูลอะไรที่ควรแสดงในตาราง ส่วนอ็อบเจ็ก delegate นี้ก็จะมีสมาชิกที่คอยชี้กลับมาตาราง เพื่อที่จะได้รู้ว่าการตัดสินใจต่างๆที่เกิดขึ้นเป็นของ UITableView ใด เมื่อเขียนความสัมพันธ์ ก็จะแสดงได้ดังรูป

 

strong_weak1

จากรูปจะเห็นว่า ความสัมพันธ์ที่เกิดขึ้นนี้ อาจจะก่อให้เกิดปัญหา circle of strong relationships เพราะเมื่อใดก็ตามที่ other objects ( เช่น UITableViewController ) นั้นหยุดความสัมพันธ์นี้ลง UITableView และ DelegateObj ก็จะไม่สามารถตัดขาดออกจากกันได้ ทำให้เกิด memory leak

 

strong_weak2

เพื่อที่จะแก้ปัญหา circle of strong relationships นี้ จึงต้องกำหนด ownership ให้กับ delegate เป็น weak แทน และเมื่อความสัมพันของ other objects กับ table จบลง ก็หมายความว่า delegate ก็จะถูกทำลายไปด้วยเพราะเป็น weak

strong_weak3

และเมื่อ delegate ถูกทำลายลง ความสัมพันธ์แบบ strong ระหว่าง UITableView ก็จะถูกตัดขาดลง ทำให้ UITableView ถูกทำลายตามไปด้วย ดังแล้วก็จะไม่เกิด memory leak

strong_weak4

เราจะลองเขียนโปรแกรมง่ายๆสักโปรแกรมซึ่งเป็นโปรแกรมแสดงรายการสินค้า โดยจะสร้างคลาส Product ง่ายๆขึ้นมาสักหนึ่งคลาส เพื่อให้คุ้นเคยกับการใช้ ARC

Program 16.4
Product.h

Product.m

ปกติเมื่อประกาศพร๊อพเพอร์ที่เป็นอ็อบเจ็กอย่างเช่น NSString มักกำหนด attribute เป็น retain , copy แต่เมื่อเปลี่ยนมาใช้ arc จึงเปลี่ยนให้เป็น strong ส่วนพร็อพเพอร์ตี้ price ยังใช้ assign อยู่ก็เพราะว่า int เป็นค่าแบบ scalar ไม่ใช่อ็อบเจ็ก และในส่วนของ implementation ไม่ต้องเขียน dealloc เหมือนแต่ก่อน

main.m

โค้ดของโปรแกรมหลัก ก็ยังประกาศอ็อบเจ็กโดยใช้ alloc และ init เช่นเดิม แต่ไม่ต้องเขียน release แต่อย่างใด เพราะ arc เป็นคนจัดการให้ หลังจากคอมไพล์และให้โปรแกรมทำงานก็จะได้ผลตามนี้

Program 16.3 Output
(
    “iPhone5S – 25000”,
    “iPad – 15000”,
    “iMac – 45000”
)

Block with ARC

ทั่วไปแล้ว block สามารถใช้งานกับ arc ได้ทันที และหากบล็อกนั้นอยู่บน Stack เมื่อส่งผ่านบล๊อกระหว่างเมธอด ไม่ต้องเขียน Block Copy ยกตัวอย่าง เช่น โปรแกรม 15.8 เมธอด demoBlock มีโค้ดดังนี้

จะเห็นว่าเมื่อส่งผ่าน block จะต้องเรียก copy ไม่เช่นนั้นจะเกิด error ดังที่เคยได้อธิบายไปในบทที่ 15 แต่หลังจากที่เปลี่ยนไปใช้ ARC ก็ไม่ต้องเรียก copy อีกต่อไป

อย่างไรก็ตามมีข้อยกเว้นคือหากเก็บบล็อกไว้ใน collection class เช่นอาร์เรย์ก็ยังคงต้องเรียกใช้ copy เช่น

ในการเขียนพร็อพเพอร์ตี้ที่เป็น block ก็ยังคงใช้ copy อยู่ ไม่ได้เปลี่ยนไปใช้ strong แต่อย่างใด ดังเช่นตัวอย่าง

และจากบทที่แล้วที่ค้างไว้ ปัญหาอย่างหนึ่งของการใช้บล๊อกคือ Strong Reference Cycle ซึ่งในการแก้ปัญหาแบบ non arc ได้เขียนโค้ด createBlock ดังนี้

เมื่อเปลี่ยนมาใช้ ARC จะใช้ __weak แทน __block ดังเช่นตัวอย่าง

Convert non ARC object to ARC

โปรแกรมที่ไม่ได้เขียนแบบ ARC มาตั้งแต่เริ่มต้น เมื่อเปลี่ยนมาใช้ ARC ก็จะเกิดปัญหาโค้ดของโปรแกรมเดิมจะไม่สามารถใช้งานได้เพราะไม่สามารถเขียน release,retain ได้ หากคอมไพล์โปรแกรมก็จะได้รับแจ้ง error

arc_error

แต่ไม่ต้องกังวลเพราะ XCode ได้เตรียมเครื่องมือในการแปลงคลาสจาก Non ARC ไปยัง ARC ไว้ให้เรียบร้อยแล้ว ซึ่งสามารถใช้เครื่องมือนี้ได้จากเมนู Edit > Refactor > Convert to Objective-C Arc… เราจะใช้โปรแกรมที่ 10.3 เป็นตัวอย่างในการเปลี่ยนโปรเจคและคลาสต่างๆให้เป็น ARC โดยเริ่มต้นด้วยการปรับ Build Setting และกำหนด Objective-C Automatic Reference Counting ให้เป็น YES จากนั้นก็เลือกเมนู Convert to Objective-C ก็จะพบกับหน้าต่างดังรูป

convert_arc

หน้าต่างนี้จะบอกรายละเอียดว่าไฟล์ใดบ้างที่ต้องการจะเปลี่ยนให้เป็น ARC จากนั้นกด Check โปรแกรมจะทำการวิเคราะห์คลาสต่างๆ และบอกรายละเอียดของสิ่งจะเปลี่ยนไปดังรูป

preview_arc

จากรูปจะเห็นว่าสิ่งที่เปลี่ยนสำหรับไฟล์ BarGraph.h คือ การเปลี่ยนพร็อพเพอร์ตี้ให้เป็น __unsafe_unretained จากเดิมที่เป็น assign ส่วนการเปลี่ยนแปลงของไฟล์อื่นๆก็สามารถกดดูได้ และเมื่อตรวจดูสิ่งที่จะเปลี่ยนไปเรียบร้อยแล้วก็กด Save โปรแกรมก็จะถามว่า ต้องการจะใช้ Snap Shot ไว้หรือไม่

snap

 

ถ้าหากเลือก Enable โปรแกรมก็จะสร้าง Snapshot เพื่อให้เราสามารถย้อนทุกสิ่งทุกอย่างให้กลับมายังจุดเริ่มต้นก่อนการเปลี่ยนแปลงได้ แนะนำให้เลือก Enable เพราะในกรณีที่เกิดการผิดพลาด เราสามารถที่จะย้อนกลับไป Snap shot ที่เคยสร้างไว้ จากเมนู File > Restore Snapshot

restore_snap

แต่ถ้ามั่นใจว่าจะไม่เกิดปัญหาภายหลังก็เลือก Disable แทนก็ได้

 

ARC & Non ARC

ถ้าหากมี source code ของโปรแกรม การเปลี่ยนโปรเจคให้เป็น arc คงไม่ใช่ปัญหาอะไร เพราะสามารถใช้เครื่องมือช่วยในการเปลี่ยนให้เป็นอาร์คได้ แต่ในกรณีที่ไม่มีโค้ดของโปรแกรม เช่น Library หรือ Framework นั้นจะเกิดปัญหา เพราะเนื่องจาก library หรือ framework เหล่านี้มีการจัดการหน่วยความจำแบบเก่า โชคดีที่ llvm ได้ออกแบบให้ arc และ non arc นั้นทำงานร่วมกันได้ โดยการกำหนด compiler flag ด้วยค่า -fno-objc-arc ให้กับไฟล์ หรือ library ที่เป็น non arc
เราจะใช้โปรเจค 10.3 ที่ผ่านมา (หลังจากได้เปลี่ยนเป็น arc) เพื่อทดสอบว่าสามารถคอมไพล์ไฟล์ที่เป็น non arc ร่วมกันได้ โดยเพิ่มคลาส NonArch เข้าไปในโปรเจค ซึ่งมีโค้ดง่ายๆดังนี้

NonArch.h

NonArch.m

เมื่อคลาสที่เพิ่มเข้าไปเรียบร้อย และลองคอมไพล์จะพบแจ้งเตือน error บอกว่า ‘release’ is unavailable: not available in automatic reference counting mode ก็เพราะว่าโค้ดที่เขียนไปยังเป็นการจัดการหน่วยความจำแบบเก่า ดังนั้นเราจึงต้องบอกให้คอมไพลเลอร์รู้ว่าไฟล์นี้ต้องคอมไพล์เป็นแบบ non arc ซึ่งสามารถทำได้โดย การกำหนด flag ที่ Build Phase ดังรูป

set_flag

 

เมื่อกำหนดเรียบร้อยแล้ว และถ้าหากลอง compile ดูอีกครั้งก็จะพบว่า error นั้นหายไปแล้ว ในกรณีที่เป็น library หรือ framework ก็ใช้วิธีการเดียวกัน

Summary

LLVM ได้กลายเป็นคอมไพลเลอร์ที่มีประสิทธิภาพทรงพลังเป็นอย่างมาก เพราะได้เพิ่มความสามารถให้กับ Objective-C หลายๆอย่าง เช่น class extension และ block นอกจากนี้แล้ว llvm ยังเพิ่มการจัดการหน่วยความจำแบบใหม่นั่นคือ ARC และเราได้เห็นความสะดวกสบายจากการเปลี่ยนมาใช้ ARC กันไปแล้ว  อาจจะสงสัยว่าทำไมให้เขียนโปรแกรมแบบ non ARC อยู่ตั้งนาน นั่นเป็นเพราะตัวส่วนตัวผมคิดว่า การที่ให้เรียนรู้การจัดการบริหารหน่วยความจำแบบ manual นั้นจะทำให้ผู้อ่านเข้าใจเรื่องหน่วยความจำได้อย่างลึกซึ้ง และทำให้การเขียนโปรแกรมมีประสิทธิภาพมากกว่า เปรียบได้กับผู้ที่หัดขับรถเกียร์ธรรมดามาก่อนเมื่อเปลี่ยนไปใช้เกียร์อัตโนมัติก็ไม่ใช่เรื่องยากอะไร แต่ผู้ที่หัดขับแบบเกียร์อัตโนมัติอย่างเดียว ย่อมไม่สามารถจะไปขับเกียร์ธรรมดาได้ อีกทั้ง Library ต่างๆที่มีอยู่ในอินเทอร์เน็ต ก็ไม่ได้เป็นแบบ ARC ไปทั้งหมด เมื่อไปนำมาใช้งานอาจจะเกิดความสับสนและไม่เข้าใจการทำงาน จนกระทั่งไม่สามารถนำมาใช้งานได้

 

โหลด PDF ไปอ่านได้ครับ Chapter 16

ส่วน Source code ก็ download ได้ที่ github

Leave a Reply