這篇讀書筆記主要介紹了C語言內存分配、block疑難點、property的深入理解,自己對這三塊做了系統性的總結,希望對你有所幫助。
C語言內存分配
Objective-C從名字來看就可以知道是一門超C語言,所以了解C語言的內存模型對於理解Objective-C的內存管理有很大的幫助。C語言內存模型圖如下:
1-1 C內存分配.png
從圖中可以看出內存被分成了5個區,每個區存儲的內容如下:
棧區(stack):存放函數的參數值、局部變量的值等,由編譯器自動分配釋放,通常在函數執行結束後就釋放了,其操作方式類似數據結構中的棧。棧內存分配運算內置於處理器的指令集,效率很高,但是分配的內存容量有限,比如iOS中棧區的大小是2M。
堆區(heap):就是通過new、malloc、realloc分配的內存塊,它們的釋放編譯器不去管,由我們的應用程序去釋放。如果應用程序沒有釋放掉,操作系統會自動回收。分配方式類似於鏈表。
靜態區:全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。程序結束後,由系統釋放。
常量區:常量存儲在這裡,不允許修改的。
代碼區:存放函數體的二進制代碼。
棧區在什麼時候釋放內存呢?我們通過下面的一個例子來說明下:
- (void)print { int i = 10; int j = 20; NSLog(@"i+j = %d", (i+j)); }
在上面的代碼中當程序執行到 } 的時候,變量i和j的作用域已經結束了,編譯器就會自動釋放掉i和j所占的內存,所以理解好作用域就理解了棧區的內存分配。
棧區和堆區的區別主要為以下幾點:
對於棧來說,內存管理由編譯器自動分配釋放;對於堆來說,釋放工作由程序員控制。
棧的空間大小比堆小許多。
棧是機器系統提供的數據結構,計算機會在底層對棧提供支持,所以分配效率比堆高。
棧中存儲的變量出了作用域就無效了,而堆由於是由程序員進行控制釋放的,變量的生命周期可以延長。
參考文章:
C程序的內存管理
C/C++內存管理詳解
block
聲明block屬性的時候為什麼用copy呢?
在說明為什麼要用copy前,先思考下block是存儲在棧區還是堆區呢?其實block有3種類型:
全局塊(_NSConcreteGlobalBlock)
棧塊(_NSConcreteStackBlock)
堆塊(_NSConcreteMallocBlock)
全局塊存儲在靜態區(也叫全局區),相當於Objective-C中的單例;棧塊存儲在棧區,超出作用域則馬上被銷毀。堆塊存儲在堆區中,是一個帶引用計數的對象,需要自行管理其內存。
怎麼判斷一個block所在的存儲位置呢?
block不訪問外界變量(包括棧中和堆中的變量)
block既不在棧中也不在堆中,此時就為全局塊,ARC和MRC下都是如此。
block訪問外界變量
MRC環境下:訪問外界變量的block默認存儲在棧區。
ARC環境下:訪問外界變量的block默認存放在堆中,實際上是先放在棧區,在ARC情況下自動又拷貝到堆區,自動釋放。
使用copy修飾符的作用就是將block從棧區拷貝到堆區,為什麼要這麼做呢?我們看下Apple官方文檔給出的答案:
1-2 block copy.png
通過官方文檔可以看出,復制到堆區的主要目的就是保存block的狀態,延長其生命周期。因為block如果在棧上的話,其所屬的變量作用域結束,該block就被釋放掉,block中的__block變量也同時被釋放掉。為了解決棧塊在其變量作用域結束之後被釋放掉的問題,我們就需要把block復制到堆中。
不同類型的block使用copy方法的效果也不一樣,如下所示:
block的類型 存儲區域 復制效果
_NSConcreteStackBlock 棧 從棧復制到堆
_NSConcreteGlobalBlock 靜態區(全局區) 什麼也不做
_NSConcreteMallocBlock 堆 引用計數增加
加上__block之後為什麼就可以修改block外面的變量了?
我們先看下例子1:
- (void)testMethod { int anInteger = 42; void (^testBlock)(void) = ^{ NSLog(@"Integer is : %i", anInteger); }; anInteger = 50; testBlock(); }
運行後輸出的結果如下:
Integer is : 42
為什麼不是50呢?這個問題稍後做解釋。我們在看加入__block的情況,例子2如下:
- (void)testMethod { __block int anInteger = 42; void (^testBlock)(void) = ^{ NSLog(@"Integer is : %i", anInteger); }; anInteger = 50; testBlock(); }
運行後輸出的結果如下:
Integer is : 50
兩次運行結果不一樣,中間發生了什麼呢?我們接下來來具體分析下。
在例子1中,block會把anInteger變量復制為自己私有的const變量,也就是說block會捕獲棧上的變量(或指針),將其復制為自己私有的const變量。在例子1中,在進行anInteger = 50的操作的時候,block已經將其復制為自己的私有變量,所以這裡的修改對block裡面的anInteger不會造成任何影響。
在例子2中,anInteger是一個局部變量,存儲在棧區的。給anInteger加入__block修飾符所起到的作用就是只要觀察到該變量被block所持有,就將該變量在棧中的內存地址放到堆中,此時不管block外部還是內部anInterger的內存地址都是一樣的,進而不管在block外部還是內部都可以修改anInterger變量的值,所以anInteger = 50之後,在block輸出的值就是50了。可以通過一個圖簡單來描述一下:
1-3 __block.png
block中循環引用的問題?使用系統的block api是否也考慮循環引用的問題?weak與strong之間的區別?
在使用block的時候,我們要特別注意循環引用的問題,先來看一個循環引用的例子:
@interface XYZBlockKeeper : NSObject @property (nonatomic, copy) void (^block)(void); @end @implementation XYZBlockKeeper - (void)configureBlock { self.block = ^{ [self doSomething]; }; } @end
在上面的代碼中我們聲明了一個block屬性,所以self對block有一個強引用。而在block內部又對self進行了一次強引用,這樣就形成了一個封閉的環,也就是我們經常說的強引用循環。引用關系如圖:
1-4 strong retain cycle.png
在這種情況下,由於其相互引用,內存不能夠進行釋放,就造成了內存洩漏的問題。怎麼解決循環引用的問題呢?我們經常通過聲明一個weakSelf來解決循環引用的問題,更改後的代碼如下:
- (void)configureBlock { __weak typeof(self) weakSelf = self; self.block = ^{ [weakSelf doSomething]; }; }
加入weakSelf之後,block對self就由強引用關系變成了弱引用關系,這樣在屬性所指的對象遭到摧毀時,屬性值也會被清空,就打破了block捕獲的作用域帶來的循環引用。這跟設置self.block = nil是同樣的道理。
在使用系統提供的block api需要考慮循環引用的問題嗎?比如:
[UIView animateWithDuration:0.5 animations:^{ [self doSomething]; }];
在這種情況下是不需要考慮循環引用的,因為這裡只有block對self進行了一次強引用,屬於單向的強引用,沒有形成循環引用。
weak與strong有什麼區別呢?先看一段代碼:
- (void)configureBlock { __weak typeof(self) weakSelf = self; self.block = ^{ [weakSelf doSomething]; // weakSelf != nil // preemption(搶占) weakSelf turned nil [weakSelf doAnotherThing]; }; }
這段代碼看起來很正常呀,但是在並發執行的時候,block的執行是可以搶占的,而且對weakSelf指針的調用時序不同可以導致不同的結果,比如在一個特定的時序下weakSelf可能會變成nil,這個時候在執行doAnotherThing就會造成程序的崩潰。為了避免出現這樣的問題,采用__strong的方式來進行避免,更改後的代碼如下:
- (void)configureBlock { __weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf doSomething]; // strongSelf != nil // 在搶占的時候,strongSelf還是非nil的。 [strongSelf doAnotherThing]; }; }
從代碼中可以看出加入__strong所起的作用就是在搶占的時候strongSelf還是非nil的,避免出現nil的情況。
總結:
在block不是作為一個property的時候,可以在block裡面直接使用self,比如UIView的animation動畫block。
當block被聲明為一個property的時候,需要在block裡面使用weakSelf,來解決循環引用的問題。
當和並發執行相關的時候,當涉及異步的服務的時候,block可以在之後被執行,並且不會發生關於self是否存在的問題。
參考文章:
iOS Block詳解
property
@synthesize和@dynamic分別有什麼作用?
在說兩者分別有什麼作用前,我們先看下@property的本質是什麼:
@property = ivar + getter + setter;
從上面可以看出@property的本質就是ivar(實例變量)加存取方法(getter + setter)。在我們屬性定義完成後,編譯器會自動生成該屬性的getter和setter方法,這個過程就叫做自動合成。除了生成getter與setter方法,編譯器還要自動向類中添加適當類型的實例變量,並且在屬性名前面加下劃線,以此做實例變量的名字。
@synthesize的作用就是如果你沒有手動實現getter與setter方法,那麼編譯器就會自動為你加上這兩個方法。
@dynamic的作用就是告訴編譯器,getter與setter方法由用戶自己實現,不自動生成。當然對於readonly的屬性只需要提供getter即可。
如果都沒有寫@synthesize和@dynamic,那麼默認的就是@synthesize var = _var;
為了加深對@synthesize和@dynamic的理解,我們來看幾個具體的例子,例子1代碼如下:
@interface ViewController () @property (nonatomic, copy) NSString *name; @end @implementation ViewController @dynamic name; - (void)viewDidLoad { [super viewDidLoad]; self.name = @"國士梅花"; NSLog(@"name is : %@", self.name); } @end
我們在進行編譯的時候沒有問題,但是運行的時候就會發生崩潰,崩潰的原因如下:
'NSInvalidArgumentException', reason: '-[ViewController setName:]: unrecognized selector sent to instance 0x7fd28dd06000'
崩潰的原因是不識別setName方法,這也驗證了如果加入了@dynamic的話,編譯系統就不會自己生成getter和setter方法了,需要我們自己來實現。
我們在來看下@synthesize合成實例變量的規則是什麼?例子2代碼如下:
@interface ViewController () @property (nonatomic, copy) NSString *name; @end @implementation ViewController @synthesize name = _myName; - (void)viewDidLoad { [super viewDidLoad]; self.name = @"國士梅花"; NSLog(@"name is : %@", _myName); } @end
從代碼中可以看出,1、當我們指定了成員變量的名稱(指定為帶下劃線的myName),就會生成指定的成員變量。如果代碼中存在帶下劃線的name,就不會在生成了。2、如果是@synthesize name;還會生成一個名稱為帶下劃線的name成員變量,也就是說如果沒有指定成員變量的名稱會自動生成一個屬性同名的成員變量。3、如果是@synthesize name = _name; 就不會生成成員變量了。
在有了自動合成屬性實例變量之後,@synthesize還有哪些使用場景呢?先搞清楚一個問題,什麼時候不會使用自動合成?
同時重寫了setter和getter時。
重寫了只讀屬性的getter時。
使用了@dynamic時。
在@protocol中定義的所有屬性。
在category中定義的所有屬性。
重載的屬性。
注意點:
在category中使用@property也是只會生成getter和setter方法的聲明,如果真的需要給category增加屬性的實現,需要借助於運行時的兩個函數:objc_setAssociatedObject和objc_getAssociatedObject。
在protocol中使用property只會生成setter和getter方法聲明,使用屬性的目的是希望遵守我協議的對象能夠實現該屬性。
weak、copy、strong、assgin分別用在什麼地方?
什麼情況下會使用weak關鍵字?
在ARC中,出現循環引用的時候,會使用weak關鍵字。
自身已經對它進行了一次強引用,沒有必要再強調引用一次。
assgin適用於基本的數據類型,比如NSInteger、BOOL等。
NSString、NSArray、NSDictionary等經常使用copy關鍵字,是因為他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary;
除了上面的三種情況,剩下的就使用strong來進行修飾。
為什麼NSString、NSDictionary、NSArray要使用copy修飾符呢?
要搞清楚這個問題,我們先來弄明白深拷貝與淺拷貝的區別,以非集合類與集合類兩種情況來進行說明下,先看非集合類的情況,代碼如下:
NSString *name = @"國士梅花"; NSString *newName = [name copy]; NSLog(@"name memory address: %p newName memory address: %p", name, newName);
運行之後,輸出的信息如下:
name memory address: 0x10159f758 newName memory address: 0x10159f758
可以看出復制過後,內存地址是一樣的,沒有發生變化,這就是淺復制,只是把指針地址復制了一份。我們改下代碼改成[name mutableCopy],此時日志的輸出信息如下:
name memory address: 0x101b72758 newName memory address: 0x608000263240
我們看到內存地址發生了變化,並且newName的內存地址的偏移量比name的內存地址要大許多,由此可見經過mutableCopy操作之後,復制到堆區了,這就是深復制了,深復制就是內容也就進行了拷貝。
上面的都是不可變對象,在看下可變對象的情況,代碼如下:
NSMutableString *name = [[NSMutableString alloc] initWithString:@"國士梅花"]; NSMutableString *newName = [name copy]; NSLog(@"name memory address: %p newName memory address: %p", name, newName);
運行之後日志輸出信息如下:
name memory address: 0x600000076e40 newName memory address: 0x6000000295e0
從上面可以看出copy之後,內存地址不一樣,且都存儲在堆區了,這是深復制,內容也就進行拷貝。在把代碼改成[name mutableCopy],此時日志的輸出信息如下:
name memory address: 0x600000077380 newName memory address: 0x6000000776c0
可以看出可變對象copy與mutableCopy的效果是一樣的,都是深拷貝。
總結:對於非集合類對象的copy操作如下:
[immutableObject copy]; //淺復制
[immutableObject mutableCopy]; //深復制
[mutableObject copy]; //深復制
[mutableObject mutableCopy]; //深復制
采用同樣的方法可以驗證集合類對象的copy操作如下:
[immutableObject copy]; //淺復制
[immutableObject mutableCopy]; //單層深復制
[mutableObject copy]; //深復制
[mutableObject mutableCopy]; //深復制
對於NSString、NSDictionary、NSArray等經常使用copy關鍵字,是因為它們有對應的可變類型:NSMutableString、NSMutableDictionary、NSMutableArray,它們之間可能進行賦值操作,為確保對象中的字符串值不會無意間變動,應該在設置新屬性時拷貝一份。
參考文章:
招聘一個靠譜的iOS開發者