1. 程式人生 > >iOS程式設計基礎-OC(六)-專家級技巧:使用ARC

iOS程式設計基礎-OC(六)-專家級技巧:使用ARC

     第6章 專家級技巧:使用ARC

     本章是第一部分的最後一章;

         本章介紹ARC記憶體管理中的細微之處;

         如直接橋接物件使用ARC的方法;

     6.1 ARC和物件所有權

     我們已經知道OC程式碼建立的物件儲存在以動態方式分配的記憶體(堆記憶體)中;需要記憶體管理;

         確保將不再使用的物件從該記憶體區域中移除;

         確保不錯誤地移除仍在使用中的物件;

     OC通過引用計數模式實現記憶體管理,該模式通過計算物件的被引用數量判定物件是否在使用中;

     ARC通過在編譯時,插入程式碼,使向物件傳送的retain和release訊息達到平衡,從而自動化該任務;

         ARC禁止程式設計師手動控制物件的生命週期;

         也不能通過傳送retain和release訊息控制物件的所有權;

     瞭解ARC記憶體管理中的物件所有權規則很重要;

     6.1.1 宣告物件的所有權

     OC能夠通過獲得由名稱以alloc new copy mutableCopy開頭的方法建立的任何物件的所有權;

         A * a = [[A alloc] init];//這裡使用alloc方法建立了A物件,也就聲明瞭物件的所有權;

         在編寫的程式碼中,如果使用copy訊息建立塊物件,也可以獲得這類物件的所有權;

     注:

         塊是指閉包的實現程式碼,這類函式允許訪問它們作用範圍之外的變數;後續會詳細介紹;

     6.1.2 釋放物件的所有權

     ARC禁止以手動方式向物件傳送release、autorelease、dealloc或其他相關的訊息;

     當你編寫的程式碼執行下列操作時,會放棄物件的所有權:

         1)重新分配變數;

         2)將nil賦予變數;

         3)釋放物件的所有者;

     1.重新分配變數:

         變數中指向以動態方式建立的物件更改為指向另一個物件,那麼原來物件就會失去一個所有者;

         在編譯時,ARC會插入向這個物件傳送release訊息的程式碼;此時如果該物件沒有其他的所有者,那麼它就會被釋放;

     2.將nil賦予變數:

         如果是以動態的方式建立的物件的變數設定為nil,該物件也會失去一個所有者;

         ARC會在將變數設定為nil的語句後面,插入向這個物件傳送release訊息的程式碼;

     3.釋放物件的所有者:

         釋放物件的所有者既與物件的所有權有關;又與ARC管理物件(包括物件圖和物件集)生命週期的方式有關;

     物件圖:(Object graph)

         面向物件的程式由相互關聯的物件網路組成;這些物件通過OOP繼承或合成法連線到一起,統稱為物件圖;

     在物件圖中,物件通過合成法規則引用其他(子)物件;

         如果某個(父)物件建立另一個(子)物件,那麼這個父物件就聲明瞭它擁有孩子物件的所有權;

         我們定義了C6OrderEntry類及C6OrderItem、C6Address類,通過物件圖建立了一段程式;

(注:類實現程式碼附在文末,建議先瀏覽一下程式碼)

    C6OrderEntry * entry = [[C6OrderEntry alloc] initWithId:@"001" name:@"AD"];
    NSLog(@"OrderEntry:%@ \nOrderItem:%@ name:%@",entry.orderId,entry.item,entry.item.name);

      如上述程式碼所示:

          當程式建立並初始化了一個OrderEntry物件後,其初始化方法還會建立其他兩個子類的例項;

          因此,這個OrderEntry物件會宣告它擁有這些子物件的所有權;

          當程式釋放OrderEntry物件時,ARC會自動向其子類物件傳送release訊息;

      值得注意的是:

          在編譯時ARC還會為這個OrderEntry物件插入dealloc方法(沒有的話);

          併為它的每個子類物件插入一條release訊息;

          然後向OrderEntry物件的父類傳送一條dealloc訊息;

      上述程式碼的log如下:

      2017-11-17 15:30:09.834033+0800 精通Objective-C[36322:2311019] Initial orderEntry object with ID 001

      2017-11-17 15:30:09.834217+0800 精通Objective-C[36322:2311019] Initial OrderItem object AD

      2017-11-17 15:30:09.834369+0800 精通Objective-C[36322:2311019] OrderEntry:001

      OrderItem:<C6OrderItem: 0x60400000f790> name:AD

      2017-11-17 15:30:09.834463+0800 精通Objective-C[36322:2311019] Dealloc orderEntry object with ID 001

      2017-11-17 15:30:09.834546+0800 精通Objective-C[36322:2311019] Dealloc OrderItem object AD

      這樣ARC就實現了物件圖生命週期管理的自動化;

      Foundation框架中還有各種集合類,當一個物件儲存在集合類的一個例項中時,該集合類會宣告該物件的所有權;

          當這個集合類被釋放時,ARC會自動向該集合類中的每個物件傳送一條release訊息;

     6.2 擴充套件訂單條目工程

      (Code)

      類C6OrderEntry;

      類C6OrderItem;

      基於上述類測試宣告物件所有權,以及釋放物件所有權的幾種方式,我們會發現:ARC以正確的方式管理了這些物件圖的生命週期;

      我們來寫一段程式測試一下:

 NSLog(@"Entering autoreleasepool block");
    @autoreleasepool{
        C6OrderEntry * entry1 = [[C6OrderEntry alloc] initWithId:@"A-1" name:@"Hot dog"];
        C6OrderEntry * entry2 = [[C6OrderEntry alloc] initWithId:@"A-2" name:@"Potato"];

        NSArray * entrys = [[NSArray alloc] initWithObjects:entry1,entry2, nil];
        NSLog(@"Setting entry2 to nil");
        entry2 = nil;
        NSLog(@"Setting entrys to nil");
        entrys = nil;
        NSLog(@"Setting entry1 to nil");
        entry1 = nil;
        
        NSLog(@"Leaving autoreleasepool block");
    }

       log:

       2017-11-17 16:43:10.115083+0800 精通Objective-C[36981:2402627] Entering autoreleasepool block

       2017-11-17 16:43:10.115430+0800 精通Objective-C[36981:2402627] Initial orderEntry object with ID A-1

       2017-11-17 16:43:10.115565+0800 精通Objective-C[36981:2402627] Initial OrderItem object Hot dog

       2017-11-17 16:43:10.115658+0800 精通Objective-C[36981:2402627] Initial orderEntry object with ID A-2

       2017-11-17 16:43:10.115736+0800 精通Objective-C[36981:2402627] Initial OrderItem object Potato

       2017-11-17 16:43:10.115820+0800 精通Objective-C[36981:2402627] Setting entry2 to nil

       2017-11-17 16:43:10.115905+0800 精通Objective-C[36981:2402627] Setting entrys to nil

       2017-11-17 16:43:10.116067+0800 精通Objective-C[36981:2402627] Dealloc orderEntry object with ID A-2

       2017-11-17 16:43:10.116966+0800 精通Objective-C[36981:2402627] Dealloc OrderItem object Potato

       2017-11-17 16:43:10.117205+0800 精通Objective-C[36981:2402627] Setting entry1 to nil

       2017-11-17 16:43:10.118076+0800 精通Objective-C[36981:2402627] Dealloc orderEntry object with ID A-1

       2017-11-17 16:43:10.118704+0800 精通Objective-C[36981:2402627] Dealloc OrderItem object Hot dog

       2017-11-17 16:43:10.118909+0800 精通Objective-C[36981:2402627] Leaving autoreleasepool block

  NSLog(@"Entering autoreleasepool block");
    @autoreleasepool{
        C6OrderEntry * entry3 = [[C6OrderEntry alloc] initWithId:@"A-3" name:@"Hot dogs"];
        printf("entry3 retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(entry3)));
        NSLog(@"%@",entry3.item.name);
        printf("entry3 retain count = %ld\n",CFGetRetainCount((__bridge CFTypeRef)(entry3)));

        C6OrderEntry * entry4 = [[C6OrderEntry alloc] initWithId:@"A-4" name:@"Potatos"];
        NSLog(@"%@",entry4.item.name);
        
        NSArray * entrys = [[NSArray alloc] initWithObjects:entry3,entry4, nil];
        NSLog(@"Setting entry3 to nil");
        entry3 = nil;
        NSLog(@"Setting entrys to nil");
        entrys = nil;
        NSLog(@"Setting entry4 to nil");
        entry4 = nil;
        
        NSLog(@"Leaving autoreleasepool block");
    }

     log:

     2017-11-17 16:48:27.110462+0800 精通Objective-C[37036:2409143] Entering autoreleasepool block

     2017-11-17 16:48:27.110675+0800 精通Objective-C[37036:2409143] Initial orderEntry object with ID A-3

     2017-11-17 16:48:27.110892+0800 精通Objective-C[37036:2409143] Initial OrderItem object Hot dogs

     entry3 retain count = 1

     2017-11-17 16:48:27.111098+0800 精通Objective-C[37036:2409143] Hot dogs

     entry3 retain count = 1

     2017-11-17 16:48:27.111325+0800 精通Objective-C[37036:2409143] Initial orderEntry object with ID A-4

     2017-11-17 16:48:27.112664+0800 精通Objective-C[37036:2409143] Initial OrderItem object Potatos

     2017-11-17 16:48:27.112762+0800 精通Objective-C[37036:2409143] Potatos

     2017-11-17 16:48:27.112848+0800 精通Objective-C[37036:2409143] Setting entry3 to nil

     2017-11-17 16:48:27.112929+0800 精通Objective-C[37036:2409143] Setting entrys to nil

     2017-11-17 16:48:27.113455+0800 精通Objective-C[37036:2409143] Dealloc orderEntry object with ID A-3

     2017-11-17 16:48:27.114057+0800 精通Objective-C[37036:2409143] Dealloc OrderItem object Hot dogs

     2017-11-17 16:48:27.115738+0800 精通Objective-C[37036:2409143] Setting entry4 to nil

     2017-11-17 16:48:27.115833+0800 精通Objective-C[37036:2409143] Dealloc orderEntry object with ID A-4

     2017-11-17 16:48:27.115927+0800 精通Objective-C[37036:2409143] Dealloc OrderItem object Potatos

     2017-11-17 16:48:27.116019+0800 精通Objective-C[37036:2409143] Leaving autoreleasepool block

     我們實現了兩段程式碼:區別在於後一種的orderItem物件通過entry物件的屬性讀取器進行了獲取;

         當前所學書籍中描述:通過屬性讀取器讀取之後,ARC會自動插入向物件傳送retain和autorelease訊息的程式碼,從而避免物件被過早的釋放;以此描述,OrderItem物件將在自動釋放池末尾被釋放;

         但如上我們所做的測試,在進行物件屬性讀取前後,OrderItem的引用計數併為發生變化,這個可能是ARC在後續版本中進行了修改;關鍵的是我們理解了ARC為管理物件記憶體所做的事;

     6.3 將ARC與蘋果公司提供的框架和服務一同使用

     蘋果公司提供的常用框架和服務:

         應用框架:AppKit、UIKit;

         應用服務:音訊&視訊、圖形&動畫、資料管理、網路&因特網、使用者應用、次級框架;

         核心服務:啟動服務、Carbon Core、Core Foundation、Foundation;

     這些軟體庫,有些是用OC編寫的,因而可以直接使用;而大多數軟體庫的API是使用ANSC C編寫的,要在OC程式中直接使用,還需要做一些事;

         ARC能夠自動管理OC物件和塊物件的記憶體,蘋果提供的基於C語言的API軟體庫沒有與ARC整合;

         因此,當你通過動態的方式為這些基於C語言的API分配記憶體時,必須手動管理記憶體;

     實際上,ARC不允許在OC物件指標和其他資料型別指標(蘋果提供的基於C的API中的)之間進行直接轉換;

         為了方便在OC中使用基於C語言的API,蘋果提供了多種機制,如 橋接 和 ARC橋接轉換;

     我們來分別看一下這兩種機制;

     6.4 OC直接橋接

     Core Foundation框架是基於C語言的;

     Foundation是基於OC的;

     兩者之中的許多資料型別之間蘋果為之提供了互通性——這種功能成為“直接橋接”(toll free bridging);

         通過直接橋接你可以在Core Foundation函式中呼叫和OC訊息的接收器中使用資料型別相同的引數;

         你可以通過將一種資料型別轉換為另一種資料型別,防止編譯器報警;

     以下是一些較為常用的直接橋接資料型別,其中包括Core Foundation資料型別和對應的Foundation框架資料型別;

         Core Foundation資料型別                     Foundation框架資料型別

         CFArrayRef                                  NSArray

         CFDataRef                                   NSData

         CFDateRef                                   NSDate

         CFDictionaryRef                             NSDictionary

         CFMutableArrayRef                           NSMutableArray

         CFMutableDataRef                            NSMutableData

         CFMutableDictionaryRef                      NSMutableDictionary

         CFMutableSetRef                             NSMutableSet

         CFMutableStringRef                          NSMutableString

         CFNumberRef                                 BSNumber

         CFReadStreamRef                             NSInputStream

         CFSetRef                                    NSSet

         CFStringReg                                 NSString

         CFWriteStreamRef                            NSOutputStream

     通過直接橋接,編譯器能夠將資料在Core Foundation和Foundation框架之間進行轉換;

         示例:

    CFStringRef cstr = CFStringCreateWithCString(nil, "Hello Flower!", kCFStringEncodingASCII);
    NSArray * array = [NSArray arrayWithObject:(__bridge id _Nonnull)(cstr)];
    NSArray * array1 = [NSArray arrayWithObject:(__bridge NSString *)(cstr)];

    NSLog(@"array:%@",array);
    NSLog(@"array1:%@",array1);

     這裡的__bridge稍後會介紹,先看其他的;

     我們初始化陣列的方法需要一個OC物件指標,我們傳入的是直接橋接的CFStringRef資料型別,所以引數會被隱式裝換為NSString物件;當然你也可以顯示轉換;

     通過上一節,我們已經知道:

         OC編譯器不會自動管理Core Foundation框架資料型別物件的生命週期;

         因此,要在使用ARC的OC程式中,使用Core Foundation框架直接橋接資料型別,就必須手動管理這些物件的記憶體;

         這也是上邊ARC環境下出現__bridge的原因;

     在編寫使用ARC的OC程式時,必須設定直接橋接資料是由ARC管理記憶體還是手動管理記憶體;

         通過ARC橋接轉換可以做到這一點;

     我們先簡單介紹一下ARC橋接轉換:

         在使用ARC時,通過ARC橋接轉換 可以使用 直接橋接資料型別;

         這些操作必須使用一些特殊標記作為字首:

             __bridge、__bridge_retained和__bridge_transfer;

     __bridge:

         使用__bridge標記 可以在不改變所有權的情況下,將物件從Core Foundation框架資料型別轉換為Foundation框架資料型別(反之亦然);

         1)ARC下,Foundation框架物件,通過直接橋接將它的資料型別轉換為Core Foundation框架下的;此時,

             通過__bridge標記可以使編譯器知道這個物件的生命週期仍舊由ARC管理;

         2)Core Foundation框架物件,直接橋接為Foundation框架的資料型別;此時,

             通過__bridge標記可以告訴編譯器這個物件的生命週期仍舊是以手動方式管理的(不是ARC);

         注意:使用該標記可以使編譯器不報錯,但是不會改變物件的所有權,因此在使用它解決記憶體洩漏和懸掛指標問題時應多加小心;

     __bridge_retained:

         使用__bridge_retained標記可以將Foundation框架資料型別物件 轉換為Core Foundation框架資料型別物件;

         並從ARC接管物件所有權;這樣你就可以手動管理直接橋接資料的生命週期;

     __bridge_transfer:

         使用__bridge_transfer標記可以將Core Foundation框架資料型別物件 轉換為Foundation框架資料型別物件;

         並且會將物件的所有權交給ARC管理;這樣會由ARC管理物件的宣告週期;

     使用橋接轉換標記的語法:

         (橋接轉換標記 目的資料型別)變數名

     瞭解了ARC橋接轉換的三中標記的含義,相信可以看出之前示例程式碼中的問題了,在下一節我們詳細分析下,我們修改之後的程式碼如下:   

    CFStringRef cstr1 = CFStringCreateWithCString(nil, "Hello Flower!", kCFStringEncodingASCII);
     NSArray * array3 = [NSArray arrayWithObject:(__bridge_transfer NSString *)(cstr1)];

     這裡__bridge_transfer標記會將橋接物件的所有權交給ARC,這樣該物件的生命週期就會由ARC管理;

      瞭解了概念之後,讓我們來看看ARC橋接轉換的用法;

     6.5 使用ARC橋接轉換

      示例1:

      還是上邊的程式碼示例(使用__bridge標記的那一段);

      我們使用Analyze選項分析該程式,就會發現變數cstr中存在潛在的記憶體洩漏問題;


      (Pic1)

      出現這個問題的原因是使用__bridge標記無法改變直接橋接物件的所有權,因此,必須手動管理變數cstr的生命週期;

          這段程式碼建立了該CFStringRef物件,所以他擁有這個以動態方式建立的物件,並且可以向該物件傳送release訊息;

          例如:CFRelease(cstr);

      這是一種解決方式;

      更好的選擇:使用__bridge_transfer才是更好的選擇;(上一節中__bridge_transfer標記的那一段)

          這樣就不會再檢查到記憶體洩漏了;

      示例2:

   NSNumber * counting = [[NSNumber alloc]initWithFloat:5];
    CFNumberRef cscounting = (__bridge CFNumberRef)counting;
    CFShow(cscounting);
    NSLog(@"greeting:%@",counting);

     這段程式碼編譯是沒有問題的,但存在潛在的懸掛指標問題;

         因為在執行橋接轉換時,ARC會立即向NSNumber物件傳送一個release訊息;

         解決這個問題,可以這樣修改程式碼;

   NSNumber * counting1 = [[NSNumber alloc]initWithFloat:5];
    CFNumberRef cscounting1 = (__bridge_retained CFNumberRef)counting1;
    CFShow(cscounting1);
    CFRelease(cscounting1);

     使用__bridge_retained標記可以從ARC接管該物件,從而避免ARC向該物件傳送release訊息;

     亦因此,你必須手動管理這個物件的生命週期;

         現在已經轉換為CFStringRef型別;

         在使用結束之後呼叫CFRelease方法;

     可以看到:

     __bridge:並不能修改物件所有權,之前是手動管理/ARC的,使用它標記之後,仍然需要手動管理/ARC;

     __bridge_retained:可以修改物件所有權,而且不是簡單的修改,而是接管,適用於手動管理從ARC的接管物件所有權;

     __bridge_transfer:也可以修改物件所有權,而且不是簡單的修改,而是將所有權交給ARC,適用於手動管理交給ARC物件所有權;

     6.6 小結

     本章深入研究了ARC記憶體管理方式;ARC也是蘋果推薦的記憶體管理方式;

     要點:

     1)ARC禁止通過手動方式,向物件傳送release、autorelease、dealloc訊息,以及與物件有關的其他訊息;

         當執行下列操作的時候,程式會放棄物件的所有權:

             a.重新分配變數;-編譯時,ARC會插入向該物件傳送一條release訊息的程式碼;

             b.將nil賦予變數;-編譯時,ARC會插入向該物件傳送一條release訊息的程式碼;

             c.釋放物件的擁有者;-使用集合類例項時會出現釋放所有者的情況,集合類例項被釋放時,ARC會自動向該集合中的每一個物件傳送一條release訊息;

     2)蘋果公司提供的基於C語言的API軟體庫沒有與ARC整合,因此,當您在程式中以動態的方式為這些API分配記憶體時,必須手動管理記憶體;

     3)ARC禁止在OC物件指標和其他型別指標之間進行標準轉換;為此提供了相應的機制:直接橋接 和 ARC橋接轉換;

         以方便在OC中使用基於C的API;

     4)Core Foundation和Foundation框架中有很多非常有用的直接橋接資料型別,從而使你編寫的程式 能夠在呼叫Core Foundation框架和 接收OC訊息時 使用相同的資料型別;

         資料型別轉換可以消除編譯器警告;但在ARC下,無法直接轉換 直接橋接的資料型別,而必須使用特殊的ARC橋接轉換標記才能進行轉換;

     注:直接橋接 橋接的是資料型別;

         ARC橋接轉換 轉換的是物件所有權;

     5)通過ARC橋接裝換,可以使用ARC的同時,使用直接橋接資料型別;這些轉換操作以特殊的轉換標記開始(__bridge、__bridge_retained、__bridge_transfer);

     到本章為止,OC基礎知識就介紹完了,接下來我們開始介紹OC執行時系統;

程式碼實現:

C6OrderEntry


#import <Foundation/Foundation.h>

#import "C6OrderItem.h"
#import "C6Address.h"

@interface C6OrderEntry : NSObject{
    C6Address * shippingAddress;
}

@property (readonly) NSString * orderId;
@property (readonly) C6OrderItem * item;

-(id)initWithId:(NSString *)oid name:(NSString *)order;

@end
#import "C6OrderEntry.h"

@implementation C6OrderEntry

-(id)initWithId:(NSString *)oid name:(NSString *)order{
    if (self = [super init]) {
        NSLog(@"Initial orderEntry object with ID %@",oid);

        _orderId = oid;
        shippingAddress = [[C6Address alloc] init];
        _item = [[C6OrderItem alloc]initWithName:order];
    }
    return self;
}
-(void)dealloc{
    NSLog(@"Dealloc orderEntry object with ID %@",self.orderId);
}
@end

C6OrderItem

#import <Foundation/Foundation.h>

@interface C6OrderItem : NSObject

@property (readonly) NSString * name;

-(id)initWithName:(NSString *)itemName;

@end

#import "C6OrderItem.h"

@implementation C6OrderItem
-(id)initWithName:(NSString *)itemName{
    if (self = [super init]) {
        NSLog(@"Initial OrderItem object %@",itemName);
        _name = itemName;//對於自動補全屬性來說,例項變數的標準命名約定為在屬性名稱中加下劃線字首
    }
    return self;
}
-(void)dealloc{
    NSLog(@"Dealloc OrderItem object %@",self.name);
}

@end

C6Address