iOS 內存管理相關面試題

內存管理的一些概念

  • 爲何要使用內存管理?程序員

    1. 嚴格的內存管理,可以是咱們的應用程在性能上有很大的提升
    2. 若是忽略內存管理,可能致使應用佔用內存太高,致使程序崩潰
  • OC的內存管理主要有三種方式:面試

    1. ARC(自動內存計數)
    2. 手動內存計數
    3. 內存池
  • OC中內存管理的基本思想:
    保證任什麼時候候指向對象的指針個數和對象的引用計數相同,多一個指針指向這個對象這個對象的引用計數就加1,少一個指針指向這個對象這個對象的引用計數就減1。沒有指針指向這個對象對象就被釋放了。數組

    1. 每一個對象都有一個引用計數器,每一個新對象的計數器是1,當對象的計數器減爲0時,就會被銷燬
    2. 經過retain可讓對象的計數器+一、release可讓對象的計數器-1
    3. 還能夠經過autorelease pool管理內存
    4. 若是用ARC,編譯器會自動生成管理內存的代碼
  • 蘋果官方基礎內存管理規則:xcode

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

自動內存管理

  • 談談你對 ARC 的認識和理解? ARC 是iOS 5推出的新功能。編譯器在代碼裏適當的地方自動插入 retain / release 完成內存管理(引用計數)。安全

  • ARC機制中,系統判斷對象是否被銷燬的依據是什麼?
    指向對象的強指針是否被銷燬bash

引用計數器

  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會輸出什麼?爲何?
    結果:三、二、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
相關文章
相關標籤/搜索