רשימת תפוצה

חפש באתר:

Loading

פייסבוק
5to9-תמונות מ
שיחות בפורום

 

 

« טיסת מבחן | Main | מה חדש למפתחים במערכת הפעלה 5.0? »
שבת
אוק222011

המאמר האחרון על ניהול זכרון – עד המאמר הבא

אחת המכשלות העיקריות בפיתוח עבור אייפון/אייפד הינה הצורך של המפתח לטפל בניהול הזכרון באופן ידני. למרות שב–Cocoa יש כלי עזר חשוב לזה, ספירת התיחסויות, ניהול הזכרון הוא עדיין קונספט שאינו מוכר למפתחים הבאים משפות כמו Java או C# והוא כנראה הגורם מספר אחד לקריסת אפליקציות. אפל הציגו עם SDK 5.0 גישה חדשה לניהול זכרון באובג'קטיב–סי – ספירת התיחסויות אוטומטית (ARC). שבאה לפתור את המפתח מלחשוב על ניהול זכרון ובמידה רבה מצליחה לעשות זאת. במאמר זה אציג מהו ARC, כיצד להסב פרויקט לשימוש ב–ARC וממה להיזהר כאשר משתמשים ב–ARC. אפל כל כך מאמינים ב–ARC שהם הפכו זאת לברירת המחדל לפרויקטים חדשים.

נתחיל בסקירה קצרה.

עד עתה כאשר תכנתנו עבור Cocoa למדנו שכל עצם סופר את מספר ההתיחסויות אליו, כלומר מספר העצמים האחרים ששומרים התיחסות אל עצם זה וכאשר מספר ההתיחסויות יורד לאפס, כלומר אין שום מצביע בזכרון שמתיחס לעצם הנתון, העצם משחרר את עצמו מהזכרון. אנחנו מעדכנים את ספירת ההתיחסויות על ידי שליחת הודעות retain ו–release לעצם וההודעה המיוחדת autorelease שהינה למעשה release דחוי שמטרתה לאפשר למתודה להחזיר עצם שישוחרר מאוחר יותר. Cocoa גם הכתיבה לנו חוקים לשמור בבעלות על עצמים. מתודות בשם init או copy או create החזירו עצם עם ספירת התיחסות 1 שהקורא קיבל עליהם בעלות, כלומר הקורא אחראי לקרוא ל–release כאשר אינו זקוק לעצם יותר, ויתר המתודות שמחזירות עצם, מחזירות עצם שהקורא אינו מקבל בעלות עליהם ואינו צריך לשחרר אותם.

כל זה לא השתנה.

כל מה שידענו עדיין נכון. אפל אבל גילו שהמוסכמות של Cocoa כל כך חזקות שניתן ללמד את הקומפיילר אותם ולתת לקומפיילר להוסיף קריאות ל–retain/release/autorelease עבורינו באופן אוטומטי. וזה מה ש–ARC עושה.

בשפות כמו Java או c# יש אוסף אשפה (יש גם אוסף אשפה לאובגקטיב סי על לאופרד על המק אבל הוא לא קיים על האייפון). אוסף אשפה הוא תהליך שרץ ברקע וברגעים מסויימים שהוא מחליט (על פי מצב עומס הזכרון במערכת ומצּב הכוכבים בשמים) הוא מנתח את הזכרון של האפליקציה לגלות לאלו עצמים לא מתיחסים יותר ומשחרר אותם מהזכרון. אפל לא אוהבים את האקראיות שיש באוסף האשפה, על פי אפל קשה לשמור על קצב אנימציה של 60 תמונות בשניה אם פתאום אוסף האשפה מחליט לעשות את עבודתו ברגע הלא מתאים ולכן קשה לבנות אפליקציות מאד responsive עם שיטה זו. אפל גם לא אוהבים את התהלי הנוסף שרץ ברקע לנתח את הזכרון ועל ידי כך מקטין את חיי הסוללה והנימוק האחרון של אפל נגד אוספי אשפה שהם לא יעילים וגורמים לבזבוז יותר זכרון דבר שלא נורא על מחשבים ביתיים שיש בהם הרבה זכרון אבל קריטי במכשירים כמו אייפון בהם הזכרון משאב יותר לחוץ.

ARC מונע את כל זה. ARC משחרר עצמים ברגע שהם לא נחוצים יותר, כמו ניהול זכרון ידני ואולי אפילו יעיל יותר אם התכנת לא דאג להוריד התיחסות מייד. ARC לכן הוא דטרמיניסטי, אין באפליקציה תהליכים שלא בשליטתנו ויכולים להעמיס את המעבד ברגעים הלא נכונים ו–ARC קורה כולו בזמן הקומפילציה, אין שום תהליך נוסף שרץ ברקע וצורך מהסוללה.

בואו נראה דוגמא:

נגיד שאנו רוצים לכתוב מתודה שמחזירה לנו את העצם האחרון ב–NSArray וגם מסירה עצם זה מהמערך (למשל כחלק מניהול מחסנית). באופן טבעי הינו רוצים לכתוב כך:

- (id) pop {

    id obj = [array lastObject]; // 1

    [array removeLastObjec]; // 2

    return obj;

}

 

קוד זה לא יעבוד בהתיחסות ידנית (MRR-Manual Retain Release שזה מה שאנו מכירים עד ל–ARC) משום שבשורה 2 נשלחת לעצם הודעת release על ידי המערך כאשר הוא מוסר מהמערך ולכן המצביע השמור ב–obj כבר לא תקף. ב–MRR היינו צריכים לכתוב את המתודה ככה:

- (id) pop {

    id obj = [array lastObject];

    [obj retain]; // 1

    [array removeLastObjec]; // 2

    return [obj autorelease]; // 3

}

בשורה 1 אנו שולחים הודעת retain לעצם על מנת שלא ישוחרר בשורה 2 ובשורה 3 אנו שולחים הודעת autorelease כדי לאזן את ה–retain ששלחנו קודם.

זה מסורבל, לא? וזה גם ’מלכלך' את הקוד וקשה יותר לקרוא אותו. עם ARC הקוד הראשון למעלה פשוט עובד. הקומפיילר יודע להוסיף את הקריאות ל–retain ו–autorelease בעצמו.

השתכנעתי, איך עוברים ל–ARC?

ראשית צריך את Xcode 4.2 שכן ARC רק קיים בגירסא זו. כאשר מתחילים פרויקט חדש, ניתן לבחור אם להשתמש ב–ARC בפרויקט החדש, וזו ברירת המחדל ב Xcode 4.2. אם יש לנו פרויקט קיים ניתן להסב אותו ל–ARC על ידי Edit->Refactor->Convert to Objective C ARC. כאשר בוחרים פונקציה זו Xcode בוחן את הקוד שלנו ומבקש שנתקן דברים מסוימים שאינו יכול לשנות בעצמו וכאשר הכל מוכן Xcode  ישכתב לנו את הקוד ויסיר ממנו את כל הקריאות ל retain/release וישנה את הדגלים של הקומפיילר להשתמש ב–ARC.

אמנם צריכים SDK 5.0 על מנת לכתוב בARC אבל קוד הכתוב ב–ARC יכול לרוץ על כל מערכת הפעלה מגירסה 4.0 ומעלה.

 

 

אז מה צריך לדעת לגבי כתיבת קוד עם ARC? 

דבר ראשון הוא שתחת ARC לא ניתן לקרוא ל retain/release/autorelease שכן הקומפיילר קורא להם לבד.

אם בצענו מימוש שלנו ל–retain או release זה לא יעבוד תחת ARC ונידרש להסיר את המימוש שלנו.

לתכונות באובג'קטיב סי (@property) יש עכשיו מאפיינים חדשים. בעבר ניתן היה להגדיר תכונה כ–retain או assign. תחת ARC ניתן להגדיר תכונה כ–:

strong פירושו שהבעלות על המשתנה היא חזקה, כלומר נדרשת לאורך כל חיי האוביקט. הדבר מקביל ל–retain ללא ARC.

weak פירושו שאין בעלות על העצם, כלומר כמו assign אבל בניגוד ל–assign אם העצם שמצביעים עליו משוחרר מהזכרון המצביע שסומן כ–weak מתאפס באופן אוטומטי כך שמונע מאיתנו לנסות להשתמש במצביע פג–תוקף.

ניתן גם להגדיר תכונה כ–unsafe_unretained ופירושו שעושים הצבה פשוטה על המצביע ואין שום הגנה נגד שימוש במצביעים פגי תוקף. הדבר מקביל ל–assign של MRR.

לפני ARC היינו לפעמים ממשים NSAutoreleasePool כשרצינו לשחרר זכרון מהר יותר. תחת ARC זה הפך לחלק מהשפה ואנו רושמים זאת כ– @autorelease {…} . זה גם מימוש יותר יעיל מה–NSAutoreleasePool.

תחת ARC לא ניתן לשים אוביקט של אובגקטיב סי בתוך struct של סי. כלומר הקוד הבא לא חוקי תחת ARC:

struct {

    id obj;

    int i;

} s;

זאת משום שהקומפיילר לא יודע מתי אוביקט סי חי או מת ולכן לא יודע מתי יש לקרוא ל–retain/release. במקום זאת אנחנו יכולים להשתמש ב–NSObject במקום להשתמש ב–struct. או ניתן להגיד לקומפיילר שאנו מנהלים את הזכרון בעצמינו. זאת עושים כך:

struct {

    __unsafe_unretained id obj;

    int i;

} s;

ואז הקומפיילר יודע שאין ניהול זכרון ל–obj.

תחת ARC אנחנו חייבים לשמור על המוסכמות של Cocoa לשמות של מתודות. כלומר מתודות שמתחילות ב init…, copy…, create מחזירות עצם בעל ספירת התיחסות 1 כלומר הקורא מקבל בעלות על העצם. זה אומר שמתודות כמו: copyrightString או copyPasteView אינן חוקיות ונזדקק לתת להם שם חדש.

חלק ניכר מה–API של האייפון אינו ב–Cocoa אלה ב–Core Foundation שהיא ספריית סי לכל דבר. ARC רק מטפל באובג'קטיב סי ולא בסי ולכן כאשר ממירים מצביע מ–Cocoa לסי יש להגיד לקומפיילר אם יש ניהול זכרון של Core Foundation.

  • ברוב המקרים אין ניהול זכרון של CF ולכן ניתן פשוט להוסיף את המילה __bridged כאשר מסבים את המצביע. למשל:

CFStringRef str = (__bridged CFStringRef)[array objectAtIndex:...];

 CFShow(str);

 NSString *str2 = (__bridged NSString *)CFArrayGetValueAtIndex(...);

 NSLog(@”%@”, str2);

  • במקרים בהם יש ניהול זכרון, כלומר העצם נוצר תחת CF אבל משוחרר תחת Cocoa או ההיפך יש להוסיף CFBridgingRetain או CFBridgingRelease כדי לאזן את היצירה או השיחרור של העצם בדומה למה שהיינו רגילים ב–MRR לדוגמה:
  • -(NSString *)firstName {

         NSString *result =

             CFBridgingRelease(ABRecordCopyCompositeName(...));

         return result;

    }

    או

    CFStringRef str = CFBridgingRetain([myNSString copy]);

     CFRelease(str);

    תחת MRR היינו יכולים לממש משתנים בתוך switch. כך:

    switch (i) {

        case 1:

            NSString *str = @"ohMy";

            break;

        case 2:

            UIView *myView = [[UIView alloc] initWithFrame:CGRectZero];

            break;

    }

    תחת ARC זה אסור משום שבהתאם לערך של המשתנה i חלקים שונים של הקוד מורצים. הפתרון פשוט, יש להקיף כל case בסוגריים מסולסלות, כך:

    switch (i) {

        case 1: {

            NSString *str = @"ohMy";

            } break;

        case 2: {

            UIView *myView = [[UIView alloc] initWithFrame:CGRectZero];

            } break;

    }

    ממה להיזהר כאשר כותבים ל–ARC

    ARC נחמד ומשחרר אותנו מדאגות לגבי ניהול זכרון, כמעט. ARC לא פותר הכל ואינו פותר אותנו מהצורך לחשוב בכלל על זכרון.

    לולאות.

    לולאות של התיחסויות הם כאשר עצם א' מתיחס לעצם ב' ועצם ב' מתיחס לעצם א'. הלולאה לא חייבת להיות בין שתי עצמים היא יכולה לערב עצמים רבים וקוראת כאשר יש התיחסות לעצם מאחד העצמים שניתן להגיע אליהם על ידי מעקב אחר שרשרת המצביעים היוצאים מהעצם.

    הבעיה היא שאם יש לנו עצם ג' שמתיחס לעצם א' בדוגמא למעלה, כאשר עצם ג' מסיר את ההתיחסות שלו לעצם א', הספירה של עצם א' לא יורדת לאפס משום שעצם ב' מתיחס אליו והספירה של עצם ב' לא יורדת לאפס משום שעצם א' מתיחס אליו ולמעשה נוצרת לנו דליפת זכרון. כלי גילוי הדליפות Leaks באינסטרומנטס קיבל מוד חדש שמאפשר לנו לגלות לולאות כאלו. ומומלץ לבדוק את האפליקציה לגבי לולאות.

    ניתן לפתור לולאות על ידי שינוי אחד המצביעים למצביע חלש (weak). כמו שהזכרנו קודם מצביע חלש אינו מוסיף לספירת ההתיחסויות של עצם ומתאפס ברגע שהעצם אליו הוא מצביע משוחרר. אם בדוגמא למעלה עצם ב' היה מצביע על עצם א' עם מצביע חלש, אז כאשר עצם ג' מסיר את ההתיחסות שלו לעצם א', הספירה של עצם א' תרד לאפס משום שעצם ב' לא מוסיף לספירה ועצם א' ישוחרר דבר שיוריד את הספירה של עצם ב' לאפס וגם הוא ישוחרר.

    קריאת מתודות דינאמית

    אובג'קטיב–סי הינה שפה דינאמית המאפשרת לעשות דברים כגון קריאה למתודה ששמה נתון במחרוזת, למשל:

    p = [obj performSelector:NSSelectorFromString(myNSString) withObject:arg];

    תחת ARC נקבל שגיאה משום שהקומפיילר לא יודע את שם המתודה שיהיה במחרוזת בזמן הריצה ועל כן לא יודע אם האובייקט שמוחזר ממנה p צריך להיות מנוהל על ידי הקורא או הנקרא. יש לעשות דברים שונים עם myNSString מכיל את המחרוזת myMethod או מכיל את המחרוזת copyMyObject.

    ניתן להגיד לקומפיילר להתעלם משורה זו ולהניח לנו, וזאת בתנאי שבאמת אין ניהול זכרון מעורב בקריאה ואנחנו יכולים להבטיח זאת מתוך הכרותינו עם הנתונים של התוכנה ואיזה שמות של מתודות היא יכולה לקבל. ניתן להורות לקומפיילר להתעלם מהשורה כך:

    #pragma clang diagnostic push

    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"

           p = [obj performSelector:NSSelectorFromString(myNSString) withObject:arg];

    #pragma clang diagnostic pop

    עירבוב סי ואובגקטיב–סי

    הרבה מהספריות של המערכת הם ספריות סי ולא אובג'קטיב סי. הדבר כך לפעמים מסיבות היסטוריות ולפעמים מסיבות של ביצועים. ARC מנהל זכרון רק באובג'קטיב–סי ו–Cocoa ולא עבור סי והעירבוב בינהם יכול ליצור באגים מאוד לא ברורים מאליהם.

    למשל, נניח שיש לנו קטע קוד שמציר בעזרת Quartz שהיא ספריה בסי. ל–Cocoa יש מספר פונקציות שעוזרות לנו ב–Quartz, למשל קבלת CGColor הדרוש לקוורץ מתוך UIColor. נסתכל על הקוד הבא:

    CGColorRef fillColor = [UIColor yellowColor].CGColor; // 1

    CGContextSetFillColorWithColor(context, fillColor); // 2

    נראה תמים ותחת MRR זה גם יעבוד, אולם תחת ARC זה טעות שתגרום לאפליקציה לקרוס!

    בואו נבין למה. בשורה 1 הקומפיילר מייצר עבורינו משתנה זמני מסוג UIColor שהוא התוצאה מהקריאה ל [UIColor yellowColor]. באותה שורה אנו מציבים במשתנה fillColor מצביע לתוכן של המשתנה הזמני שיצרנו כסוג CGColor. הקומפיילר רואה שהמשתנה הזמני אינו נחוץ עוד אחרי שורה 1 לכן הוא מייד משחרר אותו (מפעיל עליו release) זאת משום שהמשתנה fillColor הוא משתנה מסוג CGColor זהוא משתנה סי לא אובג'קטיב סי והקומפיילר לא יודע להתחשב במשתנים כאלה. התוצאה היא שהמשתנה fillColor נותר מצביע אל זכרון ששוחרר. לכן בשורה 2 כאשר אנחנו ניגשים לזכרון שהוא מצביע, אנו ניגשים לזכרון ששוחרר דבר שיכול לגרום לקריסה.

    איך מונעים זאת? דרך אחת היא לדאוג שיהיה משתנה של Cocoa בתוקף בשורה 2, כלומר:

    UIColor yellowColor = [UIColor yellowColor]; // 0

    CGColorRef fillColor = yellowColor.CGColor; // 1

    CGContextSetFillColorWithColor(context,fillColor); // 2

    כעת בשורה 2 קיים משתנה, yellowColor שלא משוחרר עד שלא נצא מקטע הקוד הנתון ולכן הזכרון שאליוּ fillColor מצביע עדיין בתוקף. דרך פשוטה יותר הוא להימנע משמירת מצביעים במשתנים בסי שמקורם ב–Cocoa. במקרה זה הקוד יראה כך:

    UIColor fillColor = [UIColor yellowColor]; // 1

    CGContextSetFillColorWithColor(context, fillColor.CGColor); // 2

    ספריות

    אמרנו שקוד הכתוב ב–ARC יכול לרוץ על מערכת הפעלה מגירסה 4.0 ומעלה (10.6 ומעלה אם מדובר במק). זה אפשרי משום ש–Xcode מוסיף לאפליקציה שלנו סיפריה המאפשרת תאימות עם מערכות אלו. אבל אם הפרויקט שלנו הוא בעצמו ספריה ואנו רוצים לתת אותה בצורה בינארית (בניגוד לקוד) לתכנתים אחרים, אזי אם הפרויקט שלהם אינו משתמש ב–ARC והספריה שלנו נכתבה עם, xcode לא יוסיף את ספרית התאימות והאפליקציה תקרוס. לכן אם אנו כותבים ספריה שבכוונתנו לתת באופן בינארי לאחרים כניראה עוד לא כדאי לעבור ל–ARC שכן אם נעבור אנו מכריחים את כלֹ המשתמשים בספריה שלנו גם לעבור ל–ARC.

    מה לגבי המקרה ההפוך? נגיד אנו משתמשים בספריה של צד שלישי שנתונה כקוד, אבל לא הועברה עדיין ל–ARC. מכיוון ש–ARC ו–MRR הם בסופו של דבר אותו המנגנון לניהול זכרון עם ההבדל שבאחד אנו אחראים על הניהול, בשני הקומפיילר אחראי לזה. ניתן להוסיף את הספריה לפרוייקט שלנו ולהגיד לקומפיילר שאת הקבצים האלה יקמפל בעזרת MRR ולא ARC. זה נעשה על ידי קביעת האופציה -fno-objc-arc ליד הקובץ במסך שלבי הבניה של הפרויקט כך:

    זהו, בעזרת ידע זה אני מקווה שתוכלו לעבור ל–ARC ולהנות מתכנות פשוט ומהיר יותר ולסבול מפחות באגים.

    PrintView Printer Friendly Version

    EmailEmail Article to Friend

    Reader Comments (2)

    סקירה מצויינת, תודה.

    א, אוקטובר 30, 2011 | Unregistered Commenterא

    תודה גיא, מאמר מצויין. ממליץ גם להסתכל במאמרים הבאים בנושא ARC ולולאות התיחסות (retain-cycles)

    http://www.mikeash.com/pyblog/friday-qa-2011-09-30-automatic-reference-counting.html
    http://www.mikeash.com/pyblog/friday-qa-2010-04-30-dealing-with-retain-cycles.html
    http://cocoawithlove.com/2009/07/rules-to-avoid-retain-cycles.html

    ה, נובמבר 10, 2011 | Unregistered Commenterגבי מירו

    PostPost a New Comment

    Enter your information below to add a new comment.

    My response is on my own website »
    Author Email (optional):
    Author URL (optional):
    Post:
     
    Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>