iOS 記憶體管理相關面試題

orilme發表於2019-04-14

記憶體管理的一些概念

  • 為什麼要使用記憶體管理?

    1. 嚴格的記憶體管理,能夠是我們的應用程在效能上有很大的提高
    2. 如果忽略記憶體管理,可能導致應用佔用記憶體過高,導致程式崩潰
  • OC的記憶體管理主要有三種方式:

    1. ARC(自動記憶體計數)
    2. 手動記憶體計數
    3. 記憶體池
  • OC中記憶體管理的基本思想:
    保證任何時候指向物件的指標個數和物件的引用計數相同,多一個指標指向這個物件這個物件的引用計數就加1,少一個指標指向這個物件這個物件的引用計數就減1。沒有指標指向這個物件物件就被釋放了。

    1. 每個物件都有一個引用計數器,每個新物件的計數器是1,當物件的計數器減為0時,就會被銷燬
    2. 通過retain可以讓物件的計數器+1、release可以讓物件的計數器-1
    3. 還可以通過autorelease pool管理記憶體
    4. 如果用ARC,編譯器會自動生成管理記憶體的程式碼
  • 蘋果官方基礎記憶體管理規則:

    1. 你擁有你建立的任何物件
    2. 你可以使用retain獲取一個物件的擁有權
    3. 當你不再需要它,你必須放棄你擁有的物件的擁有權
    4. 你一定不能釋放不是你擁有的物件的擁有權

自動記憶體管理

  • 談談你對 ARC 的認識和理解? ARC 是iOS 5推出的新功能。編譯器在程式碼裡適當的地方自動插入 retain / release 完成記憶體管理(引用計數)。

  • ARC機制中,系統判斷物件是否被銷燬的依據是什麼?
    指向物件的強指標是否被銷燬

引用計數器

  1. 給物件傳送一條retain訊息,可以使引用計數器+1(retain方法返回物件本身)
  2. 給物件傳送一條release訊息,可以使引用計數器-1(注意release並不代表銷燬/回收物件,僅僅是計數器-1)
  3. 給物件傳送retainCount訊息,可以獲得當前的引用計數值

自動釋放池

  • 自動釋放池底層怎麼實現?
    (以棧的方式實現的)(系統自動建立,系統自動釋放)棧裡面的(先進後出)
    記憶體裡面有棧,棧裡面有自動釋放池。
    自動釋放池以棧的形式實現:當你建立一個新的自動釋放池時,它將被新增到棧頂。當一個物件收到傳送autorelease訊息時,它被新增到當前執行緒的處於棧頂的自動釋放池中,當自動釋放池被回收時,它們從棧中被刪除,並且會給池子裡面所有的物件都會做一次release操作。

  • 什麼是自動釋放池?
    答:自動釋放池是用來儲存多個物件型別的指標變數

  • 自動釋放池對池內物件的作用? 被存入到自動釋放池內的物件,當自動釋放池被銷燬時,會對池內的物件全部做一次release操作

  • 物件如何放入到自動釋放池中? 當你確定要將物件放入到池中的時候,只需要呼叫物件的 autorelease 物件方法就可以把物件放入到自動釋放池中

  • 多次呼叫物件的autorelease方法會導致什麼問題?
    答:多次將地址存到自動釋放池中,導致野指標異常

  • 自動釋放池作用
    將物件與自動釋放池建立關係,池子內呼叫 autorelease 方法,在自動釋放池銷燬時銷燬物件,延遲 release 銷燬時間

  • 自動釋放池,什麼時候建立?

    1. 程式剛啟動的時候,也會建立一個自動釋放池
    2. 產生事件以後,執行迴圈開始處理事件,就會建立自動釋放池
  • 什麼時候銷燬的?

    1. 程式執行結束之前銷燬
    2. 事件處理結束以後,會銷燬自動釋放池
    3. 還有在池子滿的時候,也會銷燬
  • 自動釋放池使用注意:
    不要把大量迴圈操作放在釋放池下,因為這會導致大量迴圈內的物件沒有被回收,這種情況下應該手動寫 release 程式碼。儘量避免對大記憶體物件使用 autorelease ,否則會延遲大記憶體的回收。

  • autorelease的物件是在什麼時候被release的?
    答:autorelease實際上只是把對release的呼叫延遲了,對於每一個Autorelease,系統只是把該Object放入了當前的 Autoreleasepool中,當該pool被釋放時,該pool中的所有Object會被呼叫Release。對於每一個Runloop,系統會隱式建立一個Autoreleasepool,這樣所有的releasepool會構成一個象CallStack一樣的一個棧式結構,在每一個 Runloop結束時,當前棧頂的Autoreleasepool會被銷燬,這樣這個pool裡的每個Object(就是autorelease的物件)會被release。那什麼是一個Runloop呢?一個UI事件,Timer call,delegate call, 都會是一個新的Runloop。

  • If we don’t create any autorelease pool in our application then is there any autorelease pool already provided to us?
    系統會預設會不定時地建立和銷燬自動釋放池

  • When you will create an autorelease pool in your application?
    當不需要精確地控制物件的釋放時間時,可以手動建立自動釋放池

@property記憶體管理策略的選擇

讀寫屬性:readwritereadonly
setter語意:assignretain / copy
原子性(多執行緒管理):atomicnonatomic
強弱引用:strongweak

  • 讀寫屬性:
    readwrite :同時生成 setget 方法(預設)
    readonly :只會生成 get 方法

  • 控制set方法的記憶體管理:
    retainrelease 舊值,retain 新值。希望獲得源物件的所有權時,對其他 NSObject 和其子類(用於 OC 物件)
    copyrelease 舊值,copy 新值。希望獲得源物件的副本而不改變源物件內容時(一般用於 NSStringblock )
    assign :直接賦值,不做任何記憶體管理(預設屬性),控制需不需生成 set 方法。對基礎資料型別 (NSIntegerCGFloat )和C資料型別(int , float , double , char , 等等)

  • 原子性(多執行緒管理):

    • atomic
      預設屬性,訪問方法都為原子型事務訪問。鎖被加到所屬物件例項級,效能低。原子性就是說一個操作不可以中途被 cpu 暫停然後排程, 即不能被中斷, 要不就執行完, 要不就不執行. 如果一個操作是原子性的,那麼在多執行緒環境下, 就不會出現變數被修改等奇怪的問題。原子操作就是不可再分的操作,在多執行緒程式中原子操作是一個非常重要的概念,它常常用來實現一些同步機制,同時也是一些常見的多執行緒 Bug 的源頭。當然,原子性的變數在執行效率上要低些。
    • nonatomic
      非原子性訪問。不加同步,儘量避免多執行緒搶奪同一塊資源。是直接從記憶體中取數值,因為它是從記憶體中取得資料,它並沒有一個加鎖的保護來用於cpu中的暫存器計算Value,它只是單純的從記憶體地址中,當前的記憶體儲存的資料結果來進行使用。 多執行緒併發訪問會提高效能,但無法保證資料同步。儘量避免多執行緒搶奪同一塊資源,否則儘量將加鎖資源搶奪的業務邏輯交給伺服器處理,減少移動客戶端的壓力。
      當有多個執行緒需要訪問到同一個資料時,OC中,我們可以使用 @synchronized (變數)來對該變數進行加鎖(加鎖的目的常常是為了同步或保證原子操作)。
  • 強指標(strong)、弱指標(weak)

    • strong
      strong 系統一般不會自動釋放,在 oc 中,物件預設為強指標。作用域銷燬時銷燬引用。在實際開放中一般屬性物件一般 strong 來修飾(NSArrayNSDictionary),在使用懶載入定義控制元件的時候,一般也用strong。
    • weak
      weak 所引用物件的計數器不會加一,當物件被釋放時指標會被自動賦值為 nil,系統會立刻釋放物件。
    • __unsafe_unretained 弱引用 當物件被釋放時指標不會被自動賦值為 ni
      在ARC時屬性的修飾符是可以用 assign 的(相當於 __unsafe_unretained
      在ARC時屬性的修飾符是可以用 retain 的 (相當於 __strong)
    • 假定有N個指標指向同一個物件,如果至少有一個是強引用,這個物件只要還在作用域內就不會被釋放。相反,如果這N個指標都是弱引用,這個物件馬上就被釋放
    • 在使用 sb 或者 xib 給控制元件拖線的時候,為什麼拖出來的先屬性都是用 weak 修飾呢?
      由於在向 xib 或者 sb 裡面新增控制元件的時候,新增的子檢視是新增到了跟檢視 View 上面,而 控制器 Controller 對其根檢視 View 預設是強引用的,當我們的子控制元件新增到 view 上面的時候,self.view addSubView: 這個方法會對新增的控制元件進行強引用,如果在用 strong 對新增的子控制元件進行修飾的話,相當於有兩條強指標對子控制元件進行強引用, 為了避免這種情況,所以用 weak 修飾。
      注意:
      (1)addSubView 預設對其 subView 進行了強引用
      (2)在純手碼實現介面佈局時,如果通過懶載入處理介面控制元件,需要使用strong強指標
  • ARC管理記憶體是用 assign 還是用 weak
    assign : 如果由於某些原因代理物件被釋放了,代理指標就變成了野指標。
    weak : 如果由於某些原因代理物件被釋放了,代理指標就變成了空指標,更安全(weak 不能修飾基本資料型別,只能修飾物件)。

記憶體分析

  • 靜態分析(Analyze)
    1. 不執行程式, 直接檢測程式碼中是否有潛在的記憶體問題(不一定百分百準確, 僅僅是提供建議)
    2. 結合實際情況來分析, 是否真的有記憶體問題
  • 動態分析(Profile == Instruments)
    1. 執行程式, 通過使用app,檢視記憶體的分配情況(Allocations):可以檢視做出了某個操作後(比如點選了某個按鈕\顯示了某個控制器),記憶體是否有暴增的情況(突然變化)
    2. 執行程式, 通過使用app, 檢視是否有記憶體洩漏(Leaks):紅色區域代表記憶體洩漏出現的地方

什麼情況下會發生記憶體洩漏和記憶體溢位?

記憶體洩漏:堆裡不再使用的物件沒有被銷燬,依然佔據著記憶體。
記憶體溢位:一次記憶體洩露危害可以忽略,但記憶體洩露多了,記憶體遲早會被佔光,最終會導致記憶體溢位!當程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如資料長度比較小的資料型別 儲存了資料長度比較大的資料。

關於圖片佔用記憶體管理

  • 圖片載入佔用記憶體對比

    1. 使用 imageName: 載入圖片:
      • 載入到記憶體當中後,佔據記憶體空間較大
      • 相同的圖片,圖片不會重複載入
      • 載入記憶體當中之後,會一直停留在記憶體當中,不會隨著物件銷燬而銷燬
      • 載入進去圖片之後,佔用的記憶體歸系統管理,我們無法管理
    2. 使用 imageWithContentsOfFile: 載入圖片
      • 載入到記憶體當中後,佔據記憶體空間較小
      • 相同的圖片會被重複載入記憶體當中
      • 物件銷燬的時候,載入到記憶體中圖片會隨著一起銷燬
    3. 結論:
      1. 圖片較小,並且使用頻繁,使用 imageName: 來載入(按鈕圖示/主頁裡面圖片)
      2. 圖片較大,並且使用較少,使用 imageWithContentsOfFile: 來載入(版本新特性/相簿)
  • 圖片在沙盒中的存在形式

    1. 部署版本在>=iOS8的時候,打包的資源包中的圖片會被放到Assets.car。圖片有被壓縮;
      部署版本在<iOS8的時候,打包的資源包中的圖片會被放在MainBudnle裡面。圖片沒有被壓縮
    2. 沒有放在Images.xcassets裡面的所有圖片會直接暴露在沙盒的資源包(main Bundle), 不會壓縮到Assets.car檔案,會被放到MainBudnle裡面。圖片沒有被壓縮
    3. 結論:
      • 小圖片\使用頻率比較高的圖片放在Images.xcassets裡面
      • 大圖片\使用頻率比較低的圖片(一次性的圖片, 比如版本新特性的圖片)不要放在Images.xcassets裡面

記憶體管理問題

單個物件記憶體管理的問題

  • 關於記憶體我們主要研究的問題是什麼? 野指標:物件的retainCount已經為0,儲存了物件指標地址的變數就是野指標。使用野指標呼叫物件的方法,會導致野指標異常,導致程式直接崩潰
    記憶體洩露:已經不在使用的物件,沒有正確的釋放掉,一直駐留在記憶體中,我們就說是記憶體洩漏
  • 殭屍物件? retainCount = 0的物件被稱之為殭屍物件,也就是不能夠在訪問的物件
    1. 是什麼問題導致,訪問殭屍物件,時而正確時而錯誤?
    2. 如何開始xcode的時時檢測殭屍物件功能?
  • 當物件的retainCount = 0 時 能否呼叫 retain方法使物件復活? 已經被釋放的物件是無法在復活的
  • 如何防止出現野指標操作? 通常在呼叫完release方法後,會把儲存了物件指標地址的變數清空,賦值為nil 在oc中沒有空指標異常,所以使用[nil retain]呼叫方法不會導致異常的發生
  • 記憶體洩漏有幾種情況?
    1. 沒有配對釋放,不符合記憶體管理原則
    2. 物件提前賦值為nil或者清空,導致release方法沒有起作用

多個物件記憶體管理的問題

  • 物件與物件之間存在幾種關係?
    1. 繼承關係
    2. 組合關係
    3. 物件作為方法引數傳遞
  • 物件的組合關係中,如何確保作為成員變數的物件,不會被提前釋放? 重寫set方法,在set方法中,retain該對像,使其retainCount值增加 1
  • 組合關係導致記憶體洩漏的原因是什麼? 在set方法中,retain了該物件,但是並沒有配對釋放
  • 作為成員變數的物件,應該在那裡配對釋放? 在dealloc函式中釋放

記憶體相關的一些資料結構的對比

  • 簡述記憶體分割槽情況

    1. 程式碼區:存放函式二進位制程式碼
    2. 資料區:系統執行時申請記憶體並初始化,系統退出時由系統釋放。存放全域性變數、靜態變數、常量
    3. 堆區:通過malloc等函式或new等操作符動態申請得到,需程式設計師手動申請和釋放
    4. 棧區:函式模組內申請,函式結束時由系統自動釋放。存放區域性變數、函式引數
  • 手機的儲存空間分為記憶體(RAM)和快閃記憶體(Flash)兩種

    1. 記憶體一般較小:1G、2G、3G、4G。快閃記憶體空間相對較大16G、32G、64G;
    2. 記憶體的讀寫速度較快、快閃記憶體的讀寫速度相對較慢;
    3. 記憶體裡的東西掉電後全部丟失、快閃記憶體裡的東西掉電也不丟;
    4. 記憶體相當於電腦的記憶體條、快閃記憶體相當於電腦的硬碟;
  • 堆和棧的區別?

    • 管理方式:
      堆釋放工作由程式設計師控制,容易產生memory leak;
      棧是由編譯器自動管理,無需我們手工控制。
    • 申請大小:
      堆:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。
      棧:在Windows下,棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 Windows下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。
    • 碎片問題:
      堆:頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。
      棧:則不會存在這個問題,因為棧是先進後出的佇列,他們是如此的一一對應,以至於永遠都不可能有一個記憶體塊從棧中間彈出
    • 分配方式:
      堆都是動態分配的,沒有靜態分配的堆。
      棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如區域性變數的分配。動態分配由alloc函式進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。
    • 分配效率:
      棧:是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。
      堆:則是C/C++函式庫提供的,它的機制是很複雜的。
    • 每個App有個記憶體空間,假定是4G,分為堆和棧兩大部分。一般來說每個程式有一個堆(這個程式的所有執行緒共用這個堆),程式中的執行緒有自己棧。
      通過alloc、new或malloc獲得的記憶體在堆中分配,堆中的記憶體需要寫相應的程式碼釋放。如果程式結束了在堆中分配的記憶體會自動釋放。
      區域性變數、函式引數是在棧空間中分配,如果函式返回這個函式中的區域性變數、引數所佔的記憶體系統自動釋放(回收)。
      程式在編譯期對變數和函式分配記憶體都在棧上進行,且程式執行過程中函式呼叫時引數的傳遞也在棧上進行。
  • 佇列和棧有什麼區別:
    佇列和棧是兩種不同的資料容器。從”資料結構”的角度看,它們都是線性結構,即資料元素之間的關係相同。
    佇列是一種先進先出的資料結構,它在兩端進行操作,一端進行入佇列操作,一端進行出列隊操作。
    棧是一種先進後出的資料結構,它只能在棧頂進行操作,入棧和出棧都在棧頂操作。

  • 連結串列和陣列的區別在哪裡?
    二者都屬於一種資料結構。如果需要快速訪問資料,很少或不插入和刪除元素,就應該用陣列;相反, 如果需要經常插入和刪除元素就需要用連結串列資料結構。

    • 從邏輯結構來看
      1. 陣列必須事先定義固定的長度(元素個數),不能適應資料動態地增減的情況。當資料增加時,可能超出原先定義的元素個數;當資料減少時,造成記憶體浪費;陣列可以根據下標直接存取。
      2. 連結串列動態地進行儲存分配,可以適應資料動態地增減的情況,且可以方便地插入、刪除資料項。(陣列中插入、刪除資料項時,需要移動其它資料項,非常繁瑣)連結串列必須根據next指標找到下一個元素
    • 從記憶體儲存來看
      1. 陣列從棧中分配空間,對於程式設計師方便快速,但是自由度小
      2. 連結串列從堆中分配空間, 自由度大但是申請管理比較麻煩

面試題

  • 如何讓程式儘量減少記憶體洩漏

    • 非ARC
      Foundation 物件( OC 物件) : 只要方法中包含了 alloc\new\copy\mutableCopy\retain 等關鍵字,那麼這些方法產生的物件, 就必須在不再使用的時候呼叫1次 release 或者1次 autorelease
      CoreFoundation 物件( C 物件) : 只要函式中包含了 create\new\copy\retain 等關鍵字, 那麼這些方法產生的物件, 就必須在不再使用的時候呼叫1次 CFRelease 或者其他 release 函式。
    • ARC(只自動管理OC物件, 不會自動管理C語言物件)
      CoreFoundation 物件( C 物件) : 只要函式中包含了 create\new\copy\retain 等關鍵字, 那麼這些方法產生的物件, 就必須在不再使用的時候呼叫1次 CFRelease 或者其他 release 函式。
  • block的注意

    // block的記憶體預設在棧裡面(系統自動管理)
    void (^test)() = ^{
        
    };
    // 如果對block進行了Copy操作, block的記憶體會遷移到堆裡面(需要通過程式碼管理記憶體)
    Block_copy(test);
    // 在不需要使用block的時候, 應該做1次release操作
    Block_release(test);
    [test release];
    複製程式碼
  • 野指標舉例
    建了個檢視控制器(ARC時)某個函式裡寫了如下程式碼。當這個函式返回時因為沒有指標指向b所以b會被釋放、但是b.view不會被釋放。如果在b裡有需要操作b的地方(比如代理的方法),就會產生野指標(提前釋放)

    B *b = [[B alloc]init];
    [self.view addSubview:b.view];
    複製程式碼
  • set方法

    1. 在物件的組合關係中,導致記憶體洩漏有幾種情況? 1.set方法中沒有retain物件 2.沒有release掉舊的物件 3.沒有判斷向set方法中傳入的是否是同一個物件
    2. 該如何正確的重寫set方法? 1.先判斷是否是同一個物件 2.release一次舊的物件 3.retain新的物件
      寫一個setter方法用於完成@property (nonatomic,retain)NSString *name,
      寫一個setter方法用於完成@property(nonatomic,copy)NSString *name。
      @property (nonatomic, retain) NSString *name;
      - (void)setName:(NSString *)name {
          if (_name != name) {
              [_name release];
              _name = [name retain];
          }
      }
      
      @property(nonatomic, copy) NSString *name;
      - (void)setName:(NSString *)name {
          if (_name != name) {
              [_name release];
              _name = [name copy];
          }
      }
      
      - (void)dealloc {
      	self.name = nil;
      	// 上邊這句相當於下邊兩句
      	[_name release];
      	_name = nil;
      }
      複製程式碼
  • 引用計數的使用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1
        Person *p = [[Person alloc] init];
        
        p.age = 20;
        
        // 0 (p指向的記憶體已經是壞記憶體, 稱person物件為殭屍物件)
        // p稱為野指標, 野指標: 指向殭屍物件(壞記憶體)的指標
        [p release];
        
        // p稱為空指標
        p = nil;
        
        p.age = 40;
//        [0 setAge:40];
        
        // message sent to deallocated instance 0x100201950
        // 給空指標發訊息不會報錯
        [p release];
    }
    return 0;
}
複製程式碼

堆和棧

#import <Foundation/Foundation.h>
#import "Car.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10; // 棧
        
        int b = 20; // 棧
        
        // c : 棧
        // Car物件(計數器==1) : 堆
        Car *c = [[Car alloc] init];
    }
    
    // 當autoreleasepool執行完後後, 棧裡面的變數a\b\c都會被回收
    // 但是堆裡面的Car物件還會留在記憶體中, 因為它是計數器依然是1
    
    return 0;
}
複製程式碼
  • 看下面的程式,三次NSLog會輸出什麼?為什麼?
    結果:3、2、1

    NSMutableArray* ary = [[NSMutableArray array] retain];  
    NSString *str = [NSString stringWithFormat:@"test"];  // 1 
    [str retain];   // 2
    [ary addObject:str]; // 3  
    NSLog(@"%d", [str retainCount]);  
    [str retain];  // 4
    [str release];   // 3
    [str release];   // 2
    NSLog(@"%d", [str retainCount]);  
    [ary removeAllObjects]; // 1  
    NSLog(@"%d", [str retainCount]);   
    複製程式碼
  • [NSArray arrayWithobject:]後需要對這個陣列做釋放操作嗎?
    答: 不需要,這個物件被放到自動釋放池中

  • 老版本的工程是可以轉換成使用ARC的工程,轉換規則包括:

    1. 去掉所有的retain,release,autorelease
    2. 把NSAutoRelease替換成@autoreleasepool{}塊
    3. 把assign的屬性變為weak使用ARC的一些強制規定
    4. dealloc方法來管理一些資源,但不能用來釋放例項變數,也不能在dealloc方法裡面去掉[super dealloc]方法,在ARC下父類的dealloc同樣由編譯器來自動完成
    5. Core Foundation型別的物件任然可以用CFRetain,CFRelease這些方法
    6. 不能在使用NSAllocateObject和NSDeallocateObject物件
    7. 不能在c結構體中使用物件指標,如果有類似功能可以建立一個Objective-c類來管理這些物件
    8. 在id和void *之間沒有簡便的轉換方法,同樣在Objective-c和core Foundation型別之間的轉換都需要使用編譯器制定的轉換函式
    9. 不能使用記憶體儲存區(不能再使用NSZone)
    10. 不能以new為開頭給一個屬性命名
    11. 宣告outlet時一般應當使用weak,除了對StoryBoard,這樣nib中間的頂層物件要用strong
    12. weak 相當於老版本的assign,strong相當於retain

相關文章