iOS通過ARC管理記憶體(內容根據iOS編程編寫),iosarc
當程式執行某個方法(或函數)時,會從記憶體中一個叫棧的地區分配一塊記憶體空間,這塊記憶體空間我們叫幀。幀負責保護程式在方法內聲明的變數的值。在方法內聲明的變數我們稱之為局部變數。
當我們的程式開始啟動,作為程式的入口main函數,他的幀會被儲存在棧的地步。當main調用另一個方法時,這個方法會被壓入棧的頂部。被調用的方法還會調用其他的方法,這樣一直調用,就會形成一個幀序列。當調用的方法執行結束的時候,程式會將其幀從棧頂“彈出”並釋放響應的記憶體。
所以棧的記憶體形式是先進後出。
堆是值記憶體中的另一塊地區,是和棧分開的。堆中包含了大量無序的使用中的物件,需要通過指標來儲存這些對象在堆中的地址。當應用向某個類發送 alloc 訊息時,系統會從堆中分配出一塊記憶體,其大小為對象的全部的執行個體變數大小。
iOS應用在啟動和運行時會持續建立需要的對象,如果堆的空間是無限的,則可以隨意建立所需的對象。但是可惜,我們可用應用支配的記憶體空間是很有限的。因此,當應用不再需要某些對象時,就要將其釋放掉。釋放掉的對象可以將其佔用的記憶體歸還給堆,使之能夠重新使用。最終要的是,我們要時刻避免釋放應用正在使用的對象。
指標變數暗含了對其所指向的對象的所有權。
那些情況是使對象失去擁有者
只要指標變數指向某個對象,那麼相應的對象就會多一個擁有者,並且不會被程式釋放。這種指標特性稱為強引用。
程式也可以選擇讓指標變數不影響其指向對象的擁有者個數。這種不會改變對象擁有者個數的指標特性稱之為弱引用。
弱引用非常適合解決一種稱為強引用迴圈的記憶體管理問題。當兩個或者以上的對象相互之間有強引用特性的指標關聯的時候,就會產生強引用迴圈。這種迴圈會導致記憶體流失。因為這種迴圈中程式無法通過ARC機制來釋放記憶體。
在 RandomItems 添加一處強引用迴圈,來解釋如何解決此類問題。
#import <Foundation/Foundation.h>@interface JXItem : NSObject{ NSString *_itemName; NSString *_serialNumber; int _valueInDollars; NSDate *_dateCreated; // 這裡添加讓JXItem對象能夠儲存另一個JXItem對象。 JXItem *_containedItem; JXItem *_container;}// 初始化方法- (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber;- (instancetype)initWithItemName:(NSString *)name;- (void)setContainedItem:(JXItem *)item;- (JXItem *)containedItem;- (void)setContainer:(JXItem *)item;- (JXItem *)container;// 存方法- (void)setItemName:(NSString *)str;// 取方法- (NSString *)itemName;- (void)setSerialNumber:(NSString *)str;- (NSString *)serialNumber;- (void)setValueInDollars:(int)v;- (int)valueInDollars;- (NSDate *)dateCreated;@end
實現方法中:
#import "JXItem.h"@implementation JXItem- (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber { // 調用父類的初始化方法 self = [super init]; // 判斷父類的指定初始化方法是否是成功建立 if (self) { // 為執行個體變數設定初始值 _itemName = name; _serialNumber = sNumber; _valueInDollars = value; // 設定 _dateCreated 的值為系統目前時間 // 因為我們沒有為該執行個體變數設定 set 方法來從外部擷取 (只是一個唯讀屬性) _dateCreated = [NSDate date]; } // 返回初始化後的對象的新地址 return self; }- (void)setContainedItem:(JXItem *)item { _containedItem = item; // 將item加入容納他的JXItem對象時,會將它的container執行個體變數指向容納它的對象 item.container = self;}- (JXItem *)containedItem { return _containedItem;}- (void)setContainer:(JXItem *)item { _container = item;}- (JXItem *)container { return _container;}- (instancetype)initWithItemName:(NSString *)name { return [self initWithItemName:name valueInDollars:0 serialNumber:@""];}// 預設的初始化方法,調用自訂的初始化方法,並將之出入一個預設值- (instancetype)init { return [self initWithItemName:@"Item"];}- (void)setItemName:(NSString *)str { _itemName = str;}- (NSString *)itemName { return _itemName;}- (void)setSerialNumber:(NSString *)str { _serialNumber = str;}- (NSString *)serialNumber { return _serialNumber;}- (void)setValueInDollars:(int)v { _valueInDollars = v;}- (int)valueInDollars { return _valueInDollars;}- (NSDate *)dateCreated { return _dateCreated;}- (NSString *)description { NSString * descriptionString = [[NSString alloc] initWithFormat:@"%@ (%@): Worth $%d,recorded on %@",self.itemName,self.serialNumber,self.valueInDollars,self.dateCreated]; return descriptionString;}@end
在 main.m 實現方法
#import <Foundation/Foundation.h>#import "JXItem.h"int main(int argc, const char * argv[]) { @autoreleasepool { // 建立一個NSMutableArray對象,並用items變數儲存該對象的地址 NSMutableArray * items = [[NSMutableArray alloc] init]; JXItem * backpack = [[JXItem alloc] initWithItemName:@"Backpack"]; [items addObject:backpack]; JXItem * calculator = [[JXItem alloc] initWithItemName:@"Calculator"]; [items addObject:calculator]; backpack.containedItem = calculator; backpack = nil; calculator = nil; // 使用快速枚舉法來遍曆 for (NSString * item in items) { NSLog(@"%@",item); } } return 0;}
列印結果:
2016-09-09 00:47:04.537 RandomItems[27531:2815858] Backpack (): Worth $0,recorded on 2016-09-08 16:47:04 +00002016-09-09 00:47:04.537 RandomItems[27531:2815858] Calculator (): Worth $0,recorded on 2016-09-08 16:47:04 +0000Program ended with exit code: 0
可見,並沒有列印出釋放的資訊。對此我們可以這麼理解:當執行 backpack.containedItem = calculator; 其實就是執行了 JXItem 中的set方法。也就是 [backpack setContainer:calculator]; 我們可以查看在類中我們自訂的方法,可以看到這時候 backpack 就是self,此時的self擁有指向 calculator 的指標,也就是 calculator 的擁有方;同時,我們在實現方法中 item.container = self; 此時的 item 就是 calculator ,所有就造成了循環參考。
要解決這種循環參考問題,我們就需要在新建立的兩個對象之間的任意一個指標改為弱引用特性。在決定哪個指標改為弱引用之前,我們可以先為存在強引用迴圈問題的多個對象決定響應的父-子關係。我們可以讓父物件擁有子物件,並確保子物件不會擁有父物件。定義形式為: __weak JXItem *_container;
屬性是用來簡化聲明變數和儲存方法。申明方式: @property NSString * itemName;
#import <Foundation/Foundation.h>@interface JXItem : NSObject@property NSString * itemName;@property NSString * serialNumber;@property int valueInDollars;@property NSDate * dateCreated;@property JXItem * containedItem;@property JXItem * container;// 初始化方法- (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber;- (instancetype)initWithItemName:(NSString *)name;- (void)setContainedItem:(JXItem *)item;- (JXItem *)containedItem;- (void)setContainer:(JXItem *)item;- (JXItem *)container;// 存方法- (void)setItemName:(NSString *)str;// 取方法- (NSString *)itemName;- (void)setSerialNumber:(NSString *)str;- (NSString *)serialNumber;- (void)setValueInDollars:(int)v;- (int)valueInDollars;- (NSDate *)dateCreated;@end
實現方法:屬性的名字是執行個體變數的名字去掉底線,編譯器會根據屬性產生執行個體變數時會自動在變數名前加上底線,同時還能自動產生相應的存取方法。
#import "JXItem.h"@implementation JXItem- (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber { // 調用父類的初始化方法 self = [super init]; // 判斷父類的指定初始化方法是否是成功建立 if (self) { // 為執行個體變數設定初始值 _itemName = name; _serialNumber = sNumber; _valueInDollars = value; // 設定 _dateCreated 的值為系統目前時間 // 因為我們沒有為該執行個體變數設定 set 方法來從外部擷取 (只是一個唯讀屬性) _dateCreated = [NSDate date]; } // 返回初始化後的對象的新地址 return self; }- (void)setContainedItem:(JXItem *)item { _containedItem = item; // 將item加入容納他的JXItem對象時,會將它的container執行個體變數指向容納它的對象 item.container = self;}- (JXItem *)containedItem { return _containedItem;}- (void)setContainer:(JXItem *)item { _container = item;}- (JXItem *)container { return _container;}- (instancetype)initWithItemName:(NSString *)name { return [self initWithItemName:name valueInDollars:0 serialNumber:@""];}// 預設的初始化方法,調用自訂的初始化方法,並將之出入一個預設值- (instancetype)init { return [self initWithItemName:@"Item"];}- (void)setItemName:(NSString *)str { _itemName = str;}- (NSString *)itemName { return _itemName;}- (void)setSerialNumber:(NSString *)str { _serialNumber = str;}- (NSString *)serialNumber { return _serialNumber;}- (void)setValueInDollars:(int)v { _valueInDollars = v;}- (int)valueInDollars { return _valueInDollars;}- (NSDate *)dateCreated { return _dateCreated;}- (NSString *)description { NSString * descriptionString = [[NSString alloc] initWithFormat:@"%@ (%@): Worth $%d,recorded on %@",self.itemName,self.serialNumber,self.valueInDollars,self.dateCreated]; return descriptionString;}@end
屬性的特性
任何屬性都可以有一組特性,用於描述響應存取方法的行為。這些特性需要寫在小括弧裡,並跟在 @property 指令之後。例如: @property (nonatomic,copy) NSString * itemName; 任何屬性都有三個特性,每個特性都有多種不同的可選類型。
屬性的特性-多線程特性
此特性有兩種可選類型: nonatomic 和 atomic 。前一種是非原子訪問,不會加線程鎖,後一種相反,但是線程鎖雖然是絕對安全的,但是效率很低,一般不推薦使用。預設是 原子訪問。
#import <Foundation/Foundation.h>@interface JXItem : NSObject@property (nonatomic) NSString * itemName;@property (nonatomic) NSString * serialNumber;@property (nonatomic) int valueInDollars;@property (nonatomic) NSDate * dateCreated;@property (nonatomic) JXItem * containedItem;@property (nonatomic) JXItem * container;// 初始化方法- (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber;- (instancetype)initWithItemName:(NSString *)name;@end
屬性的特性-讀/寫特性
此特性也有兩種可選類型: readwrite 和 readonly 。編譯器預設會是 readwrite 特性的屬性產生存取方法。但是如果是 readonly 屬性,只會產生取方法。
#import <Foundation/Foundation.h>@interface JXItem : NSObject@property (nonatomic) NSString * itemName;@property (nonatomic) NSString * serialNumber;@property (nonatomic) int valueInDollars;@property (nonatomic,readonly) NSDate * dateCreated;@property (nonatomic) JXItem * containedItem;@property (nonatomic) JXItem * container;// 初始化方法- (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber;- (instancetype)initWithItemName:(NSString *)name;@end
屬性的特性-記憶體管理特性
此特性有四種可選類型: strong (預設屬性), weak , copy , unsafe_unretained 。對於不指向任何對象的屬性,也就是基礎資料型別 (Elementary Data Type),我們不需要做記憶體管理,這時候我們應該選用 unsafe_unretained ,他表示存取方法會直接為執行個體變數賦值。在 ARC 之前使用的是 assign ,現在我們仍舊可以使用這個屬性。
通常情況下,但給某個屬性是指向其他對象的指標,而且這個屬性的類有可修改的子類( NSString/NSMutableString , NSArray/NSMutableArray )這時我們應該將其屬性記憶體管理設定為 copy 。
#import <Foundation/Foundation.h>@interface JXItem : NSObject@property (nonatomic,copy) NSString * itemName;@property (nonatomic,copy) NSString * serialNumber;@property (nonatomic) int valueInDollars;@property (nonatomic,readonly,strong) NSDate * dateCreated;@property (nonatomic,strong) JXItem * containedItem;@property (nonatomic,weak) JXItem * container;// 初始化方法- (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber;- (instancetype)initWithItemName:(NSString *)name;@end
更改為 copy 特性之後,其屬性的存方法可能類似於如下代碼:
- (void)setItemName:(NSString *)itemName { _itemName = [itemName copy];}
這段代碼沒有將傳入的值 itemName 直接賦值給執行個體變數 _itemName ,而是先向 itemName 發送了 copy 資訊。該對象的 copy 方法會返回一個新的 NSString 對象。我們這麼做的原因就是:如果屬性指向的對象的類有可修改的子類,那麼該屬性可能會指向可修改的子類對象,同時,該對象可能會被其他擁有者修改。因此,我們在操作的時候最好先複製該對象,然後再將屬性指向複製後的對象。但是在 copy 方法中, NSString 對象是不會發生任何變化的,所以我們一般只有對可變對象設定為 copy ,複製不可變對象就是浪費空間而已。
自訂屬性的存取方法
預設情況下回自動產生存取方法,而且非常簡單;
- (void)setContainedItem:(JXItem *)item { _containedItem = item;}- (JXItem *)containedItem { return _containedItem;}
屬性自訂添加的存取方法我們可以直接拿來用,同時我們也可以自訂存取方法;當我們自訂的存取方法之後,編譯器就不會為我們建立預設的存取方法了。
- (void)setContainedItem:(JXItem *)containedItem { _containedItem = containedItem; self.containedItem.container = self;}