內存管理說明白點

 

寫在前面html


  

      下面的內容,《Obcject-C 高級編程 iOS與OS X 多線程和內存管理》一書是去年看的。那時想總結的,忘記了,趁着最近有時間,再把這本書回爐從新理解再看一遍,對比本身的理解,以及一些Swift內存管理的知識總結的內容,可能文章內容會比較長,就是但願本身能把內存管理這方面的知識真正的仔細總結一下,也方便本身之後回顧:編程

      到底什麼是ARC?安全

      在書中一句話總結成了「ARC(Automatic Reference Counting)表明的是自動引用計數,自動引用計數是指內存管理中對引用採起自動計數的技術」。多線程

     

理解ARC先清楚這個「引用計數」和內存管理的思考方式app


 

      書中關於理解「引用計數」這個概念引入的「開關房間的燈」的例子也是挺經典的,這裏只是一個簡單的說明:框架

      在OC中,咱們辦公室的照明設備用來比喻咱們的「對象」,「對象的使用環境」至關於咱們上班進入辦公室的人,「對象的使用環境」多一次就等因而上班進來一我的,這樣咱們就能夠這樣理解「引用計數」:源碼分析

      一、第一我的進入辦公室,辦公室的照明設備(對象)使用的人多了一個,加1,計數值就從0變成了1學習

      二、以後每當上班的人多一個(對象的使用環境多一個),那咱們的引用計數就+1spa

      三、下班後上班的人少一個( 對象的使用環境少一個),那咱們的引用計數就-1線程

      四、最後一我的也走了,引用計數再-1就變成了0,沒人再去使用對象,這時候引用計數變成0,咱們就廢棄對象

      要結合它說的這個例子去理解引用計數相信仍是比較容易的,下面再說說書中總結的內存管理的思考方式,羅列出這四點思考方式以後你們先彆着急往下面看,好好的想一下說的這四點思考方式,理解一下咱們能怎樣去對應着四點作相應的操做!

      一、本身生成的對象本身擁有

      二、非本身持有的對象本身也能持有

      三、不在須要本身持有的對象時候釋放這個對象

      四、非本身持有的對象本身是不能去釋放的

      上面你也能看到提到了「生成」、「釋放」、「持有」等詞,這些對象操做在對應Object-C的方法中是下面這樣一個對應關係,一張表總結一些,先有個印象,後面在數對它的理解以及一些須要注意的點:

 

 

      在書中是對這四點的思考方式作了一一的說明的,我這裏就再也不去一一的說明這幾點,說說須要咱們理解記住的幾個地方:

      第一:注意一下上面說的「生成並持有對象」對應的幾個方法,它並非只有這四個方法才能讓「生成並持有對象」!而是使用那四個名稱「開頭」(切記是開頭)的方法都意味着本身生成並持有對象,要理解這個開頭的意思,命名要符合「駝峯法」(這個不理解的本身去查查)的系統方法纔算是用它們開頭,像我隨便寫一個allocWithUser(),或者allocreat()等等的方法是不行的,這個相信都是能理解並做出正確的判斷的。

      第二:注意最後一點,沒法釋放非本身持有的對象,也就是經過前面說的使用alloc/new/copy/mutableCopy方法生成並持有的對象或使用retain方法持有的對象,因爲持有者是本身,因此在不須要該對象時須要將其釋放。除了這些方法以外的對象,本身是沒法釋放的,還有就像書中寫的例子同樣,已經realese掉的對象你在執行其餘操做,就是釋放非本身持有的對象,就會形成程序崩潰。        

     

從代碼中看看alloc/retain/release/dealloc       


 

      下面的源碼是從蘋果公開的代碼中咱們查看的,你能夠點擊這裏查看 runtime 源碼!裏面就有NSObject.mm源碼供咱們學習。

      後面的具體關於源碼的解析這裏就不總結了,由於這一塊的內容單獨寫出來都能寫幾篇文章,幾句是說不清它的實現過程的,但咱們不說並不表明就無法好好看一下這部分的內容了,既然NSObject.mm源碼部分以及公開了,有時間你就能夠好好看看這部分的內容。

      關於源碼解讀我找了幾篇很是不錯的文章收錄在這裏,尤爲推薦第一篇,但就是篇幅有點長,須要耐心去讀。

 

       一、Objc 對象的此生今世

      二、iOS NSObject.mm源碼解析

     三、iOS Copy解析以及源碼分析

       

   

 循環引用


       

      在理解這個循環引用以前在書中總結了一下幾個全部權的修飾符  __strong 和 __weak ,那這些修飾符是用在哪裏的呢,固然是對象類型。

      所謂的對象類型就是指向NSObject這樣的OC類的指針,例如 NSObject * , id類型用於隱藏對象類型的類名部分,至關於C語言經常使用的 void * 

      __strong 修飾符是id類型和全部對象類型默認的修飾符,這點咱們知道就能夠,它標識對對象的「強引用」,持有強引用的變量在超出其做用域時候被廢棄,隨着強引用的失效,引用的對象會隨之釋放。

     下面經過這張圖咱們看一下「強引用」的概念,而後結合簡單的代碼讓咱們掌握什麼到底什麼是「強引用」!

       看下面的代碼:

@interface TestObject:NSObject
{
        id __strong _obj;
}
-(void)setObject:(id __strong)obj;
@end

@implementation TestObject
- (instancetype)init{
        self = [super init];
        if (self) {
        }
        return self;
}
-(void)setObject:(id __strong)obj{
        _obj = obj;
}
@end

 

      要是咱們有下面這樣的引用關係就會致使循環引用:

      id  test0 = [TestObject alloc]init];

      id  test1 = [TestObject alloc]init];

      [test0 setObject test1];

      [test1 setObject test0];

       

      上面這段代碼咱們通常確定不會這麼寫,咱們在這裏只是簡單的說明一下什麼是「循環引用」,上面這段代碼相信能明白什麼是「循環引用」,在看這本書Block內容的時候有一個比較好的例子,準便也給你們看看:

typedef void(^Block)();
@interface TestObject:NSObject
{
        Block _block;
}
@end

@implementation TestObject
- (instancetype)init
{
        self = [super init];
        if (self) {
                __block id tmp = self;
                _block = ^{
                        NSLog(@"self = %@",tmp);
                        tmp = nil;
                };
        }
        return self;
}

-(void)execBlock{
        _block();
}

-(void)dealloc{
        NSLog(@"dealloc");
}

@end

int main(){
        
        id testObject = [[TestObject alloc]init];
        [testObject execBlock];
        return 0;
}

      你們分析一下這段代碼有沒有「循環引用」! 

      答案是:上面這種寫法沒有引發「循環引用」,關鍵點就是咱們用testObject 這個對象調用了execBlock 這個方法,而這個方式是執行了一下Block,那執行一下爲何就沒有循環引用呢,咱們這樣解釋!

      假如:咱們沒有  [testObject execBlock] 這句代碼,那就有循環引用而且會形成內存泄漏。(內存泄漏的緣由:應當廢棄的對象在超出其做用域的以後任然存在,這就會形成內存泄漏

      上面咱們假如沒調用以後說有「循環引用」,那這個引用關係又是什麼樣子的?分析一下:

      一、初始化咱們的 TestObject 以後,咱們的TestObject就持有了 Block (這個Block就是賦值給_block變量的Block表達式)

      二、咱們的Block是持有__block對象的,這個沒必要多說

      三、  __block id tmp = self  這句代碼就讓tmp變量持有了 TestObject,在進入下面將Block表達式賦值給_block變量以後,因爲Block表達式有截獲局部變量的屬性,因此tmp被Block表達式截獲,是有了tmp,這樣咱們的_block變量也就持有了tmp,也就是是有了TestObject對象。

      這樣就有了以下圖的一個持有關係:

     

      這就是上面的持有關係,這樣就造成了「循環引用」!

      經過調用 execBlock 這個方法,也就是執行了一下咱們的Block表達式以後爲何就不會有「循環引用」呢?關鍵的一點就是這句:  tmp = nil;

      這樣以後咱們的_block變量中的tmp就被賦值成nil,這樣tmp對TestObject的強引用就失效,咱們的_block也就再也不持有TestObject對象!這樣就沒有了循環引用!就沒有了內存泄漏,調用以後的持有關係以下:

      上面說的其實就是利用block來改變「循環引用」,那__weak呢?它又是怎樣做用的?

      __weak 修飾符它是弱引用,只指向不會持有對象,也就避免了對象之間的相互持有形成的「循環引用」,__weak 還有一個優勢就是,在持有某對象的弱引用時,要是這個對象被廢棄,則該弱引用將自動失效且處於nil被賦值的狀態,也就是所謂的「空弱引用」!因此,經過檢查被__weak修飾的變量是否爲nil,來判斷被賦值的對象是否已經被廢棄!

 

 這些得注意


     

      這一塊的東西咱們主要說說下面的這幾個內容:

      一、一些關於內存管理的規則

      二、@autoreleasepool

      三、OC 對象和 Core Foundation對象之間的轉換

  

      第一點:一些關於內存管理的規則

      (1)、在ARC中因爲內存管理是編譯器的工做,所以沒有必要使用內存管理的方法。也就是在設置了ARC後,就無需再去使用retain或者是release代碼。

      (2)、不管ARC是否有效,只要對象的全部者不在持有對象的時候該對象就會被廢棄,對象被廢棄時,無論ARC是否有效,都會調用對象的dealloc方法,在ARC有效的時候就不在顯式的調用dealloc方法。

      (3)、內存管理的方法命名規則

 

      第二點:@autoreleasepool

      顧名思義,autorelease就是自動釋放,那也就能理解autoreleasepool就是自動釋放池,要了解autoreleasepool就須要咱們瞭解autorelease,理解了autorelease也就理解了autoreleasepool。

      autorelease會像C語言的自動變量那樣來對待對象實例,當超出其做用域時候,對象實例的release實例方法就會被調用。可是在大量生成autorelease對象時,只要不廢棄,也就形成內存不足,有一個典型的處理方式,咱們一塊兒瞭解一下:

      在讀入大量圖片的同時改變尺寸,大概過程是圖像文件讀入到NSData對象,並從中生成UIImage,改變該對象的尺寸以後生成新的UIImage對象,這種狀況下就會產生大量的autorelease對象。下面這段僞代碼表達的就是這種狀況。

for (int i=0; i<圖片數; i++) {
             
        /*
           讀入圖片
           大量產生autorelease對象
           因爲沒有廢棄NSAutoreleasepool對象
           最後致使內存不足
       */          
}

      那面對這種狀況,咱們該怎麼處理呢?下面的這段僞代碼有給了咱們答案:

for (int i=0; i<圖片數; i++) {              
      /*
        在此狀況下,有必要在適當的地方生成、持有或者廢棄NSAutoreleasePool對象
        讀入圖像
        大量產生autorelease對象
       */
        NSAutoreleasePool * pool =[[NSAutoreleasePool alloc]init];
                
       /*
        經過 drain 方法
        autorelease對象被一塊兒release
       */
        [pool drain];
}

      那咱們說了這麼多,好像沒有說到 @autoreleasepool ,其實在ARC有效的狀況下咱們就直接使用 @autoreleasepool{} 代替了NSAutoreleasePool,但它們作的事以及其中的原理確實相同的,明白了NSAutoreleasePool也就明白了@autoreleasepool 。

      最後,在Cocoa框架中,也有許多的類方法用於返回 autorelease  對象,好比 NSMutableArray 類的 arrayWithCapacity 類方法,好比下面兩個方法是同樣的,只不過在ARC環境中不須要咱們本身再去寫 autorelease

NSMutableArray * array = [NSMutableArray arrayWithCapacity:1];
NSMutableArray * array = [[NSMutableArray arrayWithCapacity:1] autorelease];

 

      第三點:OC 對象和 Core Foundation對象之間的轉換

      id 類型就是咱們的OC對象   void * 類型就是C類型對象 ,咱們上面說的 Core Foundation 框架就是C語言編寫的,它們二者之間就存在着一個相互轉換的關係。其實Core Foundation對象和OC的對象區別很小,區別之處就是是使用Core Foundation框架仍是Foundation框架生成的區別,因此在ARC無效的時候,只用簡單的C語言轉換就能實現互換,另外這種轉換不須要使用額外的CPU資源,所以也被稱爲「免費橋」。

      那id類型和void * 類型之間的轉換該怎麼作呢?咱們說下面三個轉換方法:

      一、"__bridge 轉換類型",要是隻是簡單的賦值,就可使用它,這個轉換能夠 不改變對象的持有情況 它的缺點就是,要是轉換爲void * 的__bridge轉換其安全性與賦值給__unsafe_unretained修飾符相近,甚至更低,若是管理不善,沒有注意瀆職對象的全部者,就很容易由於野指針而形成程序崩潰 

      二、"__bridge_retained 轉換類型", 這個轉換可使 要轉換賦值的變量也持有所賦值變量持有的對象 ,一般用做OC對象轉換成CF對象

      三、"__bridge_transfer 轉換類型", 這個轉換可使 被轉換的變量所持有的對象在該變量被賦值給轉換目標變量以後隨之釋放  一般用做CF對象轉換成OC對象

      有一點須要說一下的,在下面的僞代碼中,CFBridgingRetain 和 __bridge_retained是相同的,CFBridgingRelease 和__bridge_transfer是相同的。

Demo1:

         CFMutableArrayRef mutableRef = NULL;
        {
                // obj 變量持有NSMutableArray生成的對象
                id obj = [[NSMutableArray alloc]init];
                // 經過CFBridgingRetain這個轉換,obj持有的對象mutableRef也持有
                mutableRef = CFBridgingRetain(obj);
                CFShow(mutableRef);
                // 這個時候在檢查mutableRef變量持有對象的引用計數,因爲這個對象被obj
                // 和mutableRef兩個變量持有,count就成了2
                printf("mutableRef count = %ld\n",(long)CFGetRetainCount(mutableRef));
                
                // 出了這個做用域的時候,obj超出其做用域,它對NSMutableArray生成的對象強引用就失效
                // 釋放持有的對象
        }
        
        // 因爲obj超出其做用域,釋放持有的對象
        // 前面NSMutableArray生成的對象就只有mutableRef一個持有
        // 因此mutableRef持有對象的引用計數就成 1
        printf("mutableRef count = %ld\n",(long)CFGetRetainCount(mutableRef));
        
        // 這裏就使用CFReleas釋放mutableRef持有的對象,它的引用計數就減 1 成了 0
        // 廢棄這個對象
        CFRelease(mutableRef);

Demo2: 

{
                // CF框架生成並持有本身生成的對象,而且將指向這個對象的指針賦給mutableRef變量
                // 這個時候這個生成的對象的強引用數爲 1
                CFMutableArrayRef mutableRef = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
                printf("mutableRef count = %ld\n",CFGetRetainCount(mutableRef));
                
                /*
                 因爲obj變量是強引用修飾符,因此變量obj持有對象強引用
                 經過CFBridgingRelease的賦值
                 變量obj持有對象強引用的同時
                 mutableRef持有的對象要被釋放掉
                 */
                id obj = CFBridgingRelease(mutableRef);
                printf("mutableRef count = %ld\n",CFGetRetainCount(mutableRef));
         
                /*
                   obj強引用了對象,對象的引用計數加1,但與此同時
                   mutableRef指向的對象要被釋放掉,強引用數減 1
                   這個時候對象也就只有被obj一個變量強引用,因此計數器值爲1
                 
                 */
                
                /*
                 另外:因爲賦值給變量mutableRef的指針也指向仍然存在的對象
                 因此能夠正常的使用
                 */
                NSLog(@"class %@",obj);
        }
        /*
          到了這,變量obj也超出了它的做用域
          強引用失效,對象引用計數爲0,就被釋放
          隨後沒有持有者在持有對象,對象被廢棄

這時候考慮一下mutableRef是什麼狀況!!!!
*/

 

      最後,考慮一下,仍是上面這個僞代碼,若是不是使用 CFBridgingRelease 而是使用 __bridge 會是什麼狀況呢?

               // 要是是這種狀況,使用__bridge轉換,會是怎樣的狀況
                // 首先是對象的引用計數會加1,這個理由和前面的同樣
                // 下面的打印計數就會成爲2,由於沒有release這麼一說
                // 當obj超出做用域後,強引用失效,引用計數減去1,
                // 可是引用計數減去1以後就會是1,對象任然存在,找超出其做用域以後任然沒有獲得釋放
                // 內存泄漏
                id obj = (__bridge id)mutableRef;

 

ARC怎麼實現的?


  

      咱們就得聊聊咱們說了這麼多的ARC究竟是怎麼實現的!

      你要是像書中那樣去具體的討論__strong或者__weak修飾符那樣去寫他們的實現,估計得寫好久好久,而且那一塊的代碼按照個人能力理解是有點點吃力,這個之後要是本身徹底懂了,有能力在總結這些修飾符的具體的實現。

      咱們在這裏大概的提一句: 蘋果的實現按照書中的說是採用的多是「散列表」也就是引用計數表來實現。

      剩下的咱們這裏就不在具體的說了,有興趣的能夠去翻翻書中的具體代碼。

相關文章
相關標籤/搜索