1. 程式人生 > >IOS block與property

IOS block與property

這篇讀書筆記主要介紹了C語言記憶體分配、block疑難點、property的深入理解,自己對這三塊做了系統性的總結,希望對你有所幫助。

C語言記憶體分配

Objective-C從名字來看就可以知道是一門超C語言,所以瞭解C語言的記憶體模型對於理解Objective-C的記憶體管理有很大的幫助。C語言記憶體模型圖如下:

416556-5a074ec54c658b80.png

1-1 C記憶體分配.png

從圖中可以看出記憶體被分成了5個區,每個區儲存的內容如下:

  • 棧區(stack):存放函式的引數值、區域性變數的值等,由編譯器自動分配釋放,通常在函式執行結束後就釋放了,其操作方式類似資料結構中的棧。棧記憶體分配運算內置於處理器的指令集,效率很高,但是分配的記憶體容量有限,比如iOS中棧區的大小是2M。

  • 堆區(heap):就是通過new、malloc、realloc分配的記憶體塊,它們的釋放編譯器不去管,由我們的應用程式去釋放。如果應用程式沒有釋放掉,作業系統會自動回收。分配方式類似於連結串列。

  • 靜態區:全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域,未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。程式結束後,由系統釋放。

  • 常量區:常量儲存在這裡,不允許修改的。

  • 程式碼區:存放函式體的二進位制程式碼。

棧區在什麼時候釋放記憶體呢?我們通過下面的一個例子來說明下:

1 2 3 4 5 - (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官方文件給出的答案:

416556-1aaba024873e003e.png

1-2 block copy.png

通過官方文件可以看出,複製到堆區的主要目的就是儲存block的狀態,延長其生命週期。因為block如果在棧上的話,其所屬的變數作用域結束,該block就被釋放掉,block中的__block變數也同時被釋放掉。為了解決棧塊在其變數作用域結束之後被釋放掉的問題,我們就需要把block複製到堆中。

不同型別的block使用copy方法的效果也不一樣,如下所示:

QQ截圖20170503095516.png

block的型別 儲存區域 複製效果

_NSConcreteStackBlock 棧 從棧複製到堆

_NSConcreteGlobalBlock 靜態區(全域性區) 什麼也不做

_NSConcreteMallocBlock 堆 引用計數增加

加上__block之後為什麼就可以修改block外面的變量了?

我們先看下例子1:

1 2 3 4 5 6 7 8 9 10 11 - (void)testMethod {      int anInteger = 42;        void (^testBlock)(void) = ^{          NSLog(@ "Integer is : %i" , anInteger);      };        anInteger = 50;        testBlock(); }

執行後輸出的結果如下:

1 Integer is : 42

為什麼不是50呢?這個問題稍後做解釋。我們在看加入__block的情況,例子2如下:

1 2 3 4 5 6 7 8 9 10 11 - (void)testMethod {      __block int anInteger = 42;        void (^testBlock)(void) = ^{          NSLog(@ "Integer is : %i" , anInteger);      };        anInteger = 50;        testBlock(); }

執行後輸出的結果如下:

1 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了。可以通過一個圖簡單來描述一下:

416556-1216c73271563f9b.png

1-3 __block.png

block中迴圈引用的問題?使用系統的block api是否也考慮迴圈引用的問題?weak與strong之間的區別?

在使用block的時候,我們要特別注意迴圈引用的問題,先來看一個迴圈引用的例子:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @interface XYZBlockKeeper : NSObject   @property (nonatomic, copy) void (^block)(void);   @end   @implementation XYZBlockKeeper   - (void)configureBlock {      self.block = ^{          [self doSomething];          }; }   @end

在上面的程式碼中我們聲明瞭一個block屬性,所以self對block有一個強引用。而在block內部又對self進行了一次強引用,這樣就形成了一個封閉的環,也就是我們經常說的強引用迴圈。引用關係如圖:

416556-32b0b108698f2ba9.png

1-4 strong retain cycle.png

在這種情況下,由於其相互引用,記憶體不能夠進行釋放,就造成了記憶體洩漏的問題。怎麼解決迴圈引用的問題呢?我們經常通過宣告一個weakSelf來解決迴圈引用的問題,更改後的程式碼如下:

1 2 3 4 5 6 - (void)configureBlock {      __weak  typeof (self) weakSelf = self;      self.block = ^{          [weakSelf doSomething];      }; }

加入weakSelf之後,block對self就由強引用關係變成了弱引用關係,這樣在屬性所指的物件遭到摧毀時,屬性值也會被清空,就打破了block捕獲的作用域帶來的迴圈引用。這跟設定self.block = nil是同樣的道理。

在使用系統提供的block api需要考慮迴圈引用的問題嗎?比如:

1 2 3 [UIView animateWithDuration:0.5 animations:^{      [self doSomething]; }];

在這種情況下是不需要考慮迴圈引用的,因為這裡只有block對self進行了一次強引用,屬於單向的強引用,沒有形成迴圈引用。

weak與strong有什麼區別呢?先看一段程式碼:

1 2 3 4 5 6 7 8 - (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的方式來進行避免,更改後的程式碼如下:

1 2 3 4 5 6 7 8 9 - (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的本質是什麼:

1 @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程式碼如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @interface ViewController ()   @property (nonatomic, copy) NSString *name;   @end   @implementation ViewController   @dynamic name;   - (void)viewDidLoad {      [ super  viewDidLoad];        self.name = @ "國士梅花" ;      NSLog(@ "name is : %@" , self.name); }   @end

我們在進行編譯的時候沒有問題,但是執行的時候就會發生崩潰,崩潰的原因如下:

1 'NSInvalidArgumentException' , reason:  '-[ViewController setName:]: unrecognized selector sent to instance 0x7fd28dd06000'

崩潰的原因是不識別setName方法,這也驗證瞭如果加入了@dynamic的話,編譯系統就不會自己生成getter和setter方法了,需要我們自己來實現。

我們在來看下@synthesize合成例項變數的規則是什麼?例子2程式碼如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @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修飾符呢?

要搞清楚這個問題,我們先來弄明白深拷貝與淺拷貝的區別,以非集合類與集合類兩種情況來進行說明下,先看非集合類的情況,程式碼如下:

1 2 3 NSString *name = @ "國士梅花" ; NSString *newName = [name copy]; NSLog(@ "name memory address: %p newName memory address: %p" , name, newName);

執行之後,輸出的資訊如下:

1 name memory address: 0x10159f758 newName memory address: 0x10159f758

可以看出複製過後,記憶體地址是一樣的,沒有發生變化,這就是淺複製,只是把指標地址複製了一份。我們改下程式碼改成[name mutableCopy],此時日誌的輸出資訊如下:

1 name memory address: 0x101b72758 newName memory address: 0x608000263240

我們看到記憶體地址發生了變化,並且newName的記憶體地址的偏移量比name的記憶體地址要大許多,由此可見經過mutableCopy操作之後,複製到堆區了,這就是深複製了,深複製就是內容也就進行了拷貝。

上面的都是不可變物件,在看下可變物件的情況,程式碼如下:

1 2 3 NSMutableString *name = [[NSMutableString alloc] initWithString:@ "國士梅花" ]; NSMutableString *newName = [name copy]; NSLog(@ "name memory address: %p newName memory address: %p" , name, newName);

執行之後日誌輸出資訊如下:

1 name memory address: 0x600000076e40 newName memory address: 0x6000000295e0

從上面可以看出copy之後,記憶體地址不一樣,且都儲存在堆區了,這是深複製,內容也就進行拷貝。在把程式碼改成[name mutableCopy],此時日誌的輸出資訊如下:

1 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,它們之間可能進行賦值操作,為確保物件中的字串值不會無意間變動,應該在設定新屬性時拷貝一份。