從兩道面試題說起

Bug開發者發表於2019-03-03

iOS 中是否存在野指標的情況?

野指標

野指標指向一個已刪除的物件或未申請訪問受限記憶體區域的指標。特別要指出的是野指標不是空指標。

Block

一提到 Block 大家肯定都知道要說的是迴圈引用。在 ARC 中,如果兩個物件相互持有對方,就會造成迴圈引用,導致記憶體無法釋放。在 Block 中,最常用的場景則是,self 持有 blockblock 中又持有了 self 。例如下方一段程式碼:

@property (nonatmaic, copy) Block dataChanged;

- (void)setUpModel{
  XYModel *model = [XYModel new];
  model.dataChanged = ^(NSString *title) {
      self.titleLabel.text = title;                
  };
  self.model = model;
}
複製程式碼

上面的這段程式碼就會造成迴圈引用。那我們怎麼破除呢?通常的做法都是使用 weakSelf 來處理,即:

- (void)setUpModel {
  XYModel *model = [XYModel new];
  __weak typeof(self) weakSelf = self;
  model.dataChanged = ^(NSString *title) {
      weakSelf.titleLabel.text = title;   
  };
  self.model = model;
}
複製程式碼

或許你還看到另外一種不是很一樣的版本:

- (void)setUpModel {
  XYModel *model = [XYModel new];
  __weak typeof(self) weakSelf = self;
  model.dataChanged = ^(NSString *title) {
      __strong typeof(self) strongSelf = weakSelf;
      strongSelf.titleLabel.text = title;   
  };
  self.model = model;
}
複製程式碼

對比一下,多了一個 strongSelf 。那為什麼又要多加一個 strongSelf 呢?

考慮一下下面的程式碼,

__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^{
  [weakSelf doSomething];
  [weakSelf doSomethingElse];
});
複製程式碼

doSomething 時, weakSelf 不會被釋放,但是在 doSomethingElse 時,weakSelf 有可能被釋放。

這個時候就遇到了野指標問題,回答了一開始的題目。

在這裡就需要用到 strongSelf ,使用 __strong 確保在 Block 內, strongSelf 不會被釋放。

小結

  • 在使用 Block 時,如遇到迴圈引用問題,可以使用 __weak 來破除迴圈引用。

  • 如果在 Block 內需要多次訪問 __weak 變數,則需要使用 __strong 來保持變數不會被釋放。

SDWebImage 中為什麼要解碼圖片

要說明這麼問題我們需要先了解一下在 iOS 中,圖片顯示的流程。

概括來說,從磁碟中載入一張圖片,並將它顯示到螢幕上,中間的主要工作流如下:

假設我們使用 imageWithContentsOfFile:方法從磁碟中載入一張圖片,這個時候的圖片並沒有解壓縮;

然後將生成的 UIImage 賦值給 UIImageView

接著一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化;

在主執行緒的下一個 run loop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進行 copy 操作,而受圖片是否位元組對齊等因素的影響,這個 copy 操作可能會涉及以下部分或全部步驟:

  • 分配記憶體緩衝區用於管理檔案 IO 和解壓縮操作;
  • 將檔案資料從磁碟讀到記憶體中;
  • 將壓縮的圖片資料解碼成未壓縮的點陣圖形式,這是一個非常耗時的 CPU 操作;
  • 最後 Core Animation 使用未壓縮的點陣圖資料渲染 UIImageView 的圖層。

在上面的步驟中,我們提到了圖片的解壓縮是一個非常耗時的 CPU 操作,並且它預設是在主執行緒中執行的。那麼當需要載入的圖片比較多時,就會對我們應用的響應性造成嚴重的影響,尤其是在快速滑動的列表上,這個問題會表現得更加突出。

這裡順便提一下 imageNamed:imageWithContentsOfFile: 的區別,這兩個 API 都需要解碼,並且工作流程都是一致的。不過imageNamed:會做快取處理,在下一次用到相同的資源時,就會從快取裡面讀取。而 imageWithContentsOfFile: 則不會。所以網上大多文章都會告訴你,多次使用的小圖片使用 imageNamed: 載入,一次性使用的大圖片使用 imageWithContentsOfFile: 載入。

對於上面引用的流程中最後提到,當有大量圖片滑動時就會造成主執行緒的卡頓,原因就是解碼圖片在主執行緒中操作的。那有什麼辦法避免呢? 我在查詢關於這個問題的相關資料時,發現有些部落格給出了2種方案:

  1. 我們不使用imageNamed:載入圖片,使用其他的方法,比如imageWithContentsOfFile:

  2. 我們自己解碼圖片,可以把這個解碼過程放到子執行緒

其實第一種方式沒法避免卡頓。這就引出了為什麼 SDWebImage中需要自己解碼圖片。

在我們使用 UIImage 的時候,建立的圖片通常不會直接載入到記憶體,而是在渲染的時候再進行解壓並載入到記憶體。這就會導致 UIImage 在渲染的時候效率上不是那麼高效。為了提高效率通過 decodedImageWithImage 方法把圖片提前解壓載入到記憶體,這樣這張新圖片就不再需要重複解壓了,提高了渲染效率。這是一種空間換時間的做法。

參考文章:

  1. 到底什麼時候才需要在ObjC的Block中使用weakSelf/strongSelf 浮生獵趣
  2. 談談 iOS 中圖片的解壓縮 雷純鋒的技術部落格
  3. SDWebImage原始碼解析(三)——SDWebImage圖片解碼/壓縮模組 SHY圓圓圈圈圓圓

你也可以關注我的公眾號,獲取更多文章。

在這裡插入圖片描述

相關文章