Objectve-C內(nèi)存管理

為什么進行內(nèi)存管理?

由于移動設(shè)備的內(nèi)存極其有限,所以每個APP所占的內(nèi)存也是有限制的,當(dāng)app所占用的內(nèi)存較多時,系統(tǒng)就會發(fā)出內(nèi)存警告,這時需要回收一些不需要再繼續(xù)使用的內(nèi)存空間,比如回收一些不再使用的對象和變量等。如果內(nèi)存占用過大,用戶會感覺界面卡頓,app運行速度慢等,甚至系統(tǒng)可能會強制退出app已釋放資源。這時候app就會發(fā)生閃退現(xiàn)象,給用戶造成影響。

那么問題來了,內(nèi)存管理范圍有那些呢?

  • 任何繼承NSObject的對象,
  • 對其他的基本數(shù)據(jù)類型無效(int、char、float、double、struct、enum等 )。
  • 本質(zhì)原因是因為對象和其他數(shù)據(jù)類型在系統(tǒng)中的存儲空間不一樣,其它局部變量主要存放于棧中,而對象存儲于堆中,當(dāng)代碼塊結(jié)束時這個代碼塊中涉及的所有局部變量會被回收,指向?qū)ο蟮闹羔樢脖换厥眨藭r對象已經(jīng)沒有指針指向,但依然存在于內(nèi)存中,造成內(nèi)存泄露。

知道了需要管理內(nèi)存的范圍,我們就需要知道如何管理那些不需要使用的對象。這時候我們需要引入一個概念。

引用計數(shù)器
  • 每個OC對象都有自己的引用計數(shù)器
  • 它是一個整數(shù)
  • 從字面上, 可以理解為”對象被引用的次數(shù)”
  • 也可以理解為: 它表示有多少人正在用這個對象
  • 簡單來說, 可以理解為:引用計數(shù)器表示有多少人正在使用這個對象 當(dāng)沒有任何人使用這個對象時, 系統(tǒng)才會回收這個對象。 也就是說,當(dāng)對象的引用計數(shù)器為0時,對象占用的內(nèi)存就會被系統(tǒng)回收;如果對象的計數(shù)器不為0,那么在整個程序運行過程,它占用的內(nèi)存就不可能被回收(除非整個程序已經(jīng)退出 )。
  • 任何一個對象, 剛生下來的時候, 引用計數(shù)器都為1
  • 當(dāng)使用alloc、new或者copy創(chuàng)建一個對象時,對象的引用計數(shù)器默認就是1
引用計數(shù)器的操作
  • 引用計數(shù)器的常見操作

    • 給對象發(fā)送一條retain消息,可以使引用計數(shù)器值+1(retain方法返回對象本身)
    • 給對象發(fā)送一條release消息, 可以使引用計數(shù)器值-1
    • 給對象發(fā)送retainCount消息, 可以獲得當(dāng)前的引用計數(shù)器值
  • 需要注意的是: release并不代表銷毀\回收對象, 僅僅是計數(shù)器-1

dealloc方法基本概念

當(dāng)對象銷毀的時候,有一個常用的方法(dealloc)也需要我們了解。

  • 當(dāng)一個對象的引用計數(shù)器值為0時,這個對象即將被銷毀,其占用的內(nèi)存被系統(tǒng)回收對象即將被銷毀時系統(tǒng)會自動給對象發(fā)送一條dealloc消息
    (因此, 從dealloc方法有沒有被調(diào)用,就可以判斷出對象是否被銷毀)
    一般會重寫dealloc方法,在這里釋放相關(guān)資源。一旦重寫了dealloc方法, 就必須調(diào)用[super dealloc],并且放在最后面調(diào)用。

  • 使用注意

    • 不能直接調(diào)用dealloc方法(該方法是系統(tǒng)自動調(diào)用)
    • 一旦對象被回收了, 它占用的內(nèi)存就不再可用,堅持使用會導(dǎo)致程序崩潰(野指針錯誤)

例子:

// 類聲明文件
#import <Foundation/Foundation.h>

@interface Person : NSObject

@end
// 類實現(xiàn)文件
#import "Person.h"

@implementation Person

- (void)dealloc
{
    
    NSLog(@"%s", __func__);
    [super dealloc];
}
@end
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {

    // 默認情況下所有的指針都是強指針
    Person *p = [[Person alloc] init];// 引用計數(shù)為1
    // 調(diào)用release方法,引用計數(shù)會減一,
    [p release]; // 引用計數(shù)為0,自動調(diào)用delloc方法
    return 0;
}

輸出結(jié)果:
[849:245988] -[Person dealloc]
MRC和ARC
  • MRC:手動引用計數(shù)(Manual Referecen Counting)所有對象的內(nèi)容都需要我們手動管理, 需要程序員自己編寫release/retain等代碼
    內(nèi)存管理的原則就是有加就有減
    也就是說, 一次alloc對應(yīng)一次release, 一次retain對應(yīng)一次relese
#import <Foundation/Foundation.h>
@interface Person : NSObject

@end


#import "Person.h"

@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");
    [super dealloc];
}
@end
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {

    @autoreleasepool {
        // 只要創(chuàng)建一個對象默認引用計數(shù)器的值就是1
        Person *p = [[Person alloc] init];
       
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
        
        // 只要給對象發(fā)送一個retain消息, 對象的引用計數(shù)器就會+1
        [p retain];
        
        NSLog(@"retainCount = %lu", [p retainCount]); // 2

        // 通過指針變量p,給p指向的對象發(fā)送一條release消息
        // 只要對象接收到release消息, 引用計數(shù)器就會-1
        // 只要一個對象的引用計數(shù)器為0, 系統(tǒng)就會釋放對象
        [p release];
        // 需要注意的是: release并不代表銷毀\回收對象, 僅僅是計數(shù)器-1
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
        
        [p release]; // 0
        NSLog(@"--------");
    }
 
    return 0;
}

輸出結(jié)果:
[928:316970] retainCount = 1
[928:316970] retainCount = 2
[928:316970] retainCount = 1
[928:316970] Person dealloc
[928:316970] --------
  • MRC注意點:
    1.如果一個對象的 retainCount = 0 了之后繼續(xù)調(diào)用release方法會報錯。也就是說不能過度release。

MRC手動引用計數(shù)管理在2011蘋果推出ARC之后開發(fā)的過程中基本不再使用了,但是我們還是要了解其中的原理,方便我們對ARC的理解。

  • ARC: Automatic Reference Counting(自動引用計數(shù))
    ARC機制可以說是WWDC2011和iOS5所引入的最大的變革和最激動人心的變化。它是LLVM編譯器的特性。可以說ARC解決了讓程序員一直以來感覺淡淡的憂傷的手動內(nèi)存管理的麻煩。
    • ARC的判斷原則
      • 只要還有一個強指針變量指向?qū)ο螅瑢ο缶蜁3衷趦?nèi)存中
    • 強指針
      • 默認所有指針變量都是強指針
      • 被__strong修飾的指針
    • 弱指針
      • 被__weak修飾的指針
 Person *p1 = [[Person alloc] init];
 __strong  Person *p2 = [[Person alloc] init];
__weak  Person *p = [[Person alloc] init];

在工程中使用ARC非常簡單:只需要像往常那樣編寫代碼,只不過永遠不寫retain,release和autorelease三個關(guān)鍵字就好~這是ARC的基本原則。當(dāng)ARC開啟時,編譯器在編輯階段將自動在代碼合適的地方插入retain, release和autorelease,而作為開發(fā)者,完全不需要擔(dān)心編譯器會做錯(除非開發(fā)者自己錯用ARC了),在默認情況下ARC機制是自動開啟的。上邊的MRC的代碼用ARC寫就簡單多了。

#import <Foundation/Foundation.h>
@interface Person : NSObject

@end
#import "Person.h"
@implementation Person

@end
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {

    Person *p = [[Person alloc] init];
    NSLog(@"--------"); 
    return 0;
}

在main方法中,使用代碼Person *p = [[Person alloc] init];創(chuàng)建了一個Person對象,拿一個指針p來指向這個對象的內(nèi)存地址,此時對象的引用計數(shù) = 1。開啟了ARC手動內(nèi)存管理機制后,在程序編譯的時候編譯器發(fā)現(xiàn) Person這個對在初始化了之后沒有其他的對象對它進行引用,那么編譯器會自動在return之前添加一句[p release];的代碼,引用計數(shù) - 1 = 0,當(dāng)main的大括號代碼執(zhí)行完的時候Person這個對象的內(nèi)存會被釋放。這一點和java的垃圾回收機制不太一樣,在java中的垃圾回收是jvm中的gc進行處理的(不知道是否準(zhǔn)確),而OC是編譯器處理的。

  • ARC下的內(nèi)存管理
  • 局部變量釋放對象隨之被釋放
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        Person *p = [[Person alloc] init];
    } // 執(zhí)行到這一行局部變量p釋放
    // 由于沒有強指針指向?qū)ο? 所以對象也釋放
    return 0;
}
  • 清空指針對象隨之被釋放
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        Person *p = [[Person alloc] init];
        p = nil; // 執(zhí)行到這一行, 由于沒有強指針指向?qū)ο? 所以對象被釋放
    }
    return 0;
}
  • 默認清空所有指針都是強指針
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        // p1和p2都是強指針
        Person *p1 = [[Person alloc] init];
        __strong Person *p2 = [[Person alloc] init];
    }
    return 0;
}
  • 弱指針需要明確說明
    • 注意: 千萬不要使用弱指針保存新創(chuàng)建的對象
int main(int argc, const char * argv[]) {
   @autoreleasepool {
        // p是弱指針, 對象會被立即釋放
        __weak Person *p1 = [[Person alloc] init];
    }
    return 0;
}

了解更多請點擊:蘋果官網(wǎng)文檔-關(guān)于內(nèi)存管理

  • Xcode中ARC和MRC的配置
    • 要想手動調(diào)用retain、release等方法 , 就必須關(guān)閉ARC功能


    • ARC和MRC的混編
      轉(zhuǎn)變?yōu)榉茿RC:-fno-objc-arc
      轉(zhuǎn)變?yōu)锳RC的:-f-objc-arc

      在Xcode設(shè)置方法


    • MRC轉(zhuǎn)換ARC
      如果一個工程時間比較老,那時候應(yīng)該是使用的MRC,在Xcode提供了功能將MRC的工程轉(zhuǎn)換為ARC。如果工程比較簡單還是可以使用的。如果比較復(fù)雜一般都會轉(zhuǎn)換失敗(原因有可能c和oc語言的橋接,dealloc方法,autorelease等等),這個有點坑。。。不多說了。


@property修飾符內(nèi)存管理
  • 1.控制set方法的內(nèi)存管理
    • retain : release舊值,retain新值(用于OC對象)
    • assign : 直接賦值,不做任何內(nèi)存管理(默認,用于非OC對象類型)
    • copy : release舊值,copy新值(一般用于NSString *)
  • 2.控制需不需生成set方法
    • readwrite :同時生成set方法和get方法(默認)
    • readonly :只會生成get方法
  • 3.多線程管理
    • atomic :性能低(默認)
    • nonatomic :性能高
  • 非ARC

    • copy : 只用于NSString\block
    • retain : 除NSString\block以外的OC對象
    • assign :基本數(shù)據(jù)類型、枚舉、結(jié)構(gòu)體(非OC對象),當(dāng)2個對象相互引用,一端用retain,一端用assign
  • ARC

    • copy : 只用于NSString\block
    • strong : 除NSString\block以外的OC對象
    • weak : 當(dāng)2個對象相互引用,一端用strong,一端用weak
    • assgin : 基本數(shù)據(jù)類型、枚舉、結(jié)構(gòu)體(非OC對象)
autorelease
  • autorelease是一種支持引用計數(shù)的內(nèi)存管理方式,只要給對象發(fā)送一條autorelease消息,會將對象放到一個自動釋放池中,當(dāng)自動釋放池被銷毀時,會對池子里面的所有對象做一次release操作

    • 注意,這里只是發(fā)送release消息,如果當(dāng)時的引用計數(shù)(reference-counted)依然不為0,則該對象依然不會被釋放。

  • autorelease方法會返回對象本身

Person *p = [Person new];
p = [p autorelease];
  • 調(diào)用完autorelease方法后,對象的計數(shù)器不變
Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 1
  • autorelease的好處
    • 不用再關(guān)心對象釋放的時間
    • 不用再關(guān)心什么時候調(diào)用release
  • autorelease的原理
    • autorelease實際上只是把對release的調(diào)用延遲了,對于每一個autorelease,系統(tǒng)只是把該 Object放入了當(dāng)前的autorelease pool中,當(dāng)該pool被釋放時,該pool中的所有Object會被調(diào)用Release。
自動釋放池
  • 創(chuàng)建自動釋放池格式:
    • iOS 5.0前
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 創(chuàng)建自動釋放池
[pool release]; // [pool drain]; 銷毀自動釋放池
  • iOS 5.0 開始
@autoreleasepool
{ //開始代表創(chuàng)建自動釋放池

} //結(jié)束代表銷毀自動釋放池

  • 在iOS程序運行過程中,會創(chuàng)建無數(shù)個池子。這些池子都是以棧結(jié)構(gòu)存在(先進后出)
  • 當(dāng)一個對象調(diào)用autorelease方法時,會將這個對象放到棧頂?shù)尼尫懦?/li>
autorelease基本使用
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];

Person *p = [[[Person alloc] init] autorelease];

[autoreleasePool drain];
@autoreleasepool
{ // 創(chuàng)建一個自動釋放池
        Person *p = [[Person new] autorelease];
} // 銷毀自動釋放池(會給池子中所有對象發(fā)送一條release消息)
autorelease使用注意
  • 并不是放到自動釋放池代碼中,都會自動加入到自動釋放池
 @autoreleasepool {
    // 因為沒有調(diào)用 autorelease 方法,所以對象沒有加入到自動釋放池
    Person *p = [[Person alloc] init];
    [p run];
}
  • 在自動釋放池的外部發(fā)送autorelease 不會被加入到自動釋放池中
    • autorelease是一個方法,只有在自動釋放池中調(diào)用才有效。
 @autoreleasepool {
 }
 // 沒有與之對應(yīng)的自動釋放池, 只有在自動釋放池中調(diào)用autorelease才會放到釋放池
 Person *p = [[[Person alloc] init] autorelease];
 [p run];

 // 正確寫法
  @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }

 // 正確寫法
 Person *p = [[Person alloc] init];
  @autoreleasepool {
    [p autorelease];
 }
  • 自動釋放池的嵌套使用
    • 自動釋放池是以棧的形式存在
    • 由于棧只有一個入口, 所以調(diào)用autorelease會將對象放到棧頂?shù)淖詣俞尫懦?/li>
    • 棧頂就是離調(diào)用autorelease方法最近的自動釋放池
@autoreleasepool { // 棧底自動釋放池
        @autoreleasepool {
            @autoreleasepool { // 棧頂自動釋放池
                Person *p = [[[Person alloc] init] autorelease];
            }
            Person *p = [[[Person alloc] init] autorelease];
        }
    }
  • 自動釋放池中不適宜放占用內(nèi)存比較大的對象
    • 盡量避免對大內(nèi)存使用該方法,對于這種延遲釋放機制,還是盡量少用
    • 不要把大量循環(huán)操作放到同一個 @autoreleasepool 之間,這樣會造成內(nèi)存峰值的上升
// 內(nèi)存暴漲
    @autoreleasepool {
        for (int i = 0; i < 99999; ++i) {
            Person *p = [[[Person alloc] init] autorelease];
        }
    }
// 內(nèi)存不會暴漲
 for (int i = 0; i < 99999; ++i) {
        @autoreleasepool {
            Person *p = [[[Person alloc] init] autorelease];
        }
    }

autorelease錯誤用法

  • 不要連續(xù)調(diào)用autorelease
 @autoreleasepool {
 // 錯誤寫法, 過度釋放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }
  • 調(diào)用autorelease后又調(diào)用release
 @autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 錯誤寫法, 過度釋放
 }
集合對象內(nèi)存管理
  • 1.官方內(nèi)存管理原則

    • 當(dāng)調(diào)用alloc、new、copy(mutableCopy)方法產(chǎn)生一個新對象的時候,就必須在最后調(diào)用一次release或者autorelease
    • 當(dāng)調(diào)用retain方法讓對象的計數(shù)器+1,就必須在最后調(diào)用一次release或者autorelease
  • 2.集合的內(nèi)存管理細節(jié)

    • 當(dāng)把一個對象添加到集合中時,這個對象會做了一次retain操作,計數(shù)器會+1
    • 當(dāng)一個集合被銷毀時,會對集合里面的所有對象做一次release操作,計數(shù)器會-1
    • 當(dāng)一個對象從集合中移除時,這個對象會一次release操作,計數(shù)器會-1
  • 3.普遍規(guī)律

    • 如果方法名是add\insert開頭,那么被添加的對象,計數(shù)器會+1
    • 如果方法名是remove\delete開頭,那么被移除的對象,計數(shù)器-1
  • 例子

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

int main(int argc, const char * argv[]) {

    @autoreleasepool {
        
        Person *p = [Person new]; // 1
        NSLog(@"reatinCount = %lu", [p retainCount]);
        NSMutableArray *arrM = [[NSMutableArray alloc] init];
        [arrM addObject:p]; // 2
        NSLog(@"reatinCount = %lu", [p retainCount]);
        
        [p release]; // 1
        // 當(dāng)數(shù)組移除一個對象之后, 會給這個對象發(fā)送一條release消息
        [arrM removeObject:p];
    }
    return 0;
}
copy內(nèi)存管理
  • 淺拷貝
    • 原對象引用計數(shù)器+1
    • 必須對原對象進行釋放
    char *cstr = "this is a c string";
    NSString *str1 = [[NSString alloc] initWithUTF8String:cstr];
    NSLog(@"str = %lu", [str1 retainCount]);
    NSString *str2 = [str1 copy];
    NSLog(@"str = %lu", [str1 retainCount]);
    [str2 release];// 必須做一次release
  • 深拷貝
    • 必須釋放新對象
    char *cstr = "this is a c string";
    NSString *str1 = [[NSString alloc] initWithUTF8String:cstr];
    NSLog(@"str = %lu", [str1 retainCount]);
    NSMutableString *str2 = [str1 mutableCopy];
    NSLog(@"str = %lu", [str1 retainCount]);
    [str2 release]; // 必須做一次release
block內(nèi)存管理

block就是一段可以靈活使用的代碼,你可以把它當(dāng)變量傳遞,賦值,甚至可以把它聲明到函數(shù)體里,更靈活的是你可以在里面引用外部的環(huán)境。 最后一條使得block要有更多的考慮,既然block可以引用外部環(huán)境,那如何保證block被調(diào)用的時候當(dāng)時的環(huán)境變量不被釋放呢?(block調(diào)用的時機可能是隨意的)在說明block的內(nèi)存管理之前需要先理解幾個概念。

  • 函數(shù)指針
int sum(int value1, int value2)
{
    return value1 + value2;
}

int minus(int value1, int value2)
{
    return value1 - value2;
}

int main(int argc, const char * argv[]) {
    int (*sumP) (int, int) = sum;
    int res = sumP(10, 20);
    NSLog(@"res = %i", res);

    int (*minusP) (int , int) = minus;
    res = minusP(10, 20);
    NSLog(@"res = %i", res);
    return 0;
}

該代碼中定義了指向函數(shù)的指針sumP和minusP,然后利用這兩個指針調(diào)用對應(yīng)的函數(shù),返回函數(shù)計算后的結(jié)果。

  • 函數(shù)指針別名
typedef int (*calculate) (int, int);
int main(int argc, const char * argv[]) {
    calculate sumP = sum;
    int res = sumP(10, 20);
    NSLog(@"res = %i", res);
    calculate minusP = minus;
    res = minusP(10, 20);
    NSLog(@"res = %i", res);
    return 0;
}

typedef關(guān)鍵字:給函數(shù)或者變量起別名,就像給我們某個人起個外號一樣,叫這個外號就是叫那個人。typedef的作用個人理解主要是為了簡化代碼,方法代碼編寫。
typedef int (*calculate) (int, int);給函數(shù)起別名,定義calculate是一個函數(shù)指針,有兩個int類型的參數(shù),并且有一個int類型的返回值。
calculate sumP = sum; 定義calculate類型的函數(shù)指針,該指針指向sum函數(shù)。
int res = sumP(10, 20);利用函數(shù)指針調(diào)用該函數(shù),根據(jù)傳遞的參數(shù)值函數(shù)返回對應(yīng)計算結(jié)果。

  • block和typedef
    知道了函數(shù)別名的概念后同樣的代碼使用block代碼如下。
int main(int argc, const char * argv[]) {
    int (^sumBlock) (int, int) = ^(int value1, int value2){
        return value1 + value2;
    };
    int res = sumBlock(10 , 20);
    NSLog(@"res = %i", res);

    int (^minusBlock) (int, int) = ^(int value1, int value2){
        return value1 - value2;
    };
    res = minusBlock(10 , 20);
    NSLog(@"res = %i", res);
    return 0;
}
  • block別名
int main(int argc, const char * argv[]) {
    typedef int (^calculteBlock)(int , int);
    calculateBlock sumBlock = ^(int value1, int value2){
        return value1 + value2;
    };
    int res = sumBlock(10, 20);
    NSLog(@"res = %i", res);
    calculateBlock minusBlock = ^(int value1, int value2){
        return value1 - value2;
    };
    res = minusBlock(10, 20);
    NSLog(@"res = %i", res);

    return 0;
}

利用typedef給block起別名, 和指向函數(shù)的指針一樣, block變量的名稱就是別名。

  • Block注意事項
    • 在block內(nèi)部可以訪問block外部的變量
{
      int  a = 10;
      void (^myBlock)() = ^{
            NSLog(@"a = %i", a);
      }
      myBlock();
}
輸出結(jié)果: 10
  • block內(nèi)部使用static修飾符的全局變量
{
        static int base = 100;
        void (^sum)(int, int) = ^ void (int a, int b) {
            base++;
            NSLog(@"block內(nèi)部base = %d",base + a + b);
        };
        
        base = 0;
        sum(1,2);
        NSLog(@"base = %d",base);
        輸出結(jié)果:
        [1410:348974] block內(nèi)部base = 4
        [1410:348974] base = 1
 }

static修飾的全局變量在內(nèi)存中只有一份,在Block中使用全局靜態(tài)變量的時候會去當(dāng)前該變量的最新的值。

  • block內(nèi)部也可以定義和block外部的同名的變量(局部變量),此時局部變量會暫時屏蔽外部(就近原則)
int  a = 10;
void (^myBlock)() = ^{
    int a = 50;
    NSLog(@"a = %i", a);
    }
myBlock();
輸出結(jié)果: 50
  • 默認情況下, Block內(nèi)部不能修改外面的局部變量
int b = 5;
void (^myBlock)() = ^{
    b = 20; // 報錯
    NSLog(@"b = %i", b);
    };
myBlock();

  • Block內(nèi)部可以修改使用__block修飾的局部變量
 __block int b = 5;
void (^myBlock)() = ^{
    b = 20;
    NSLog(@"b = %i", b);
    };
myBlock();
輸出結(jié)果: 20

局部變量加上__block修飾符之后Block內(nèi)部為什么可以改變局部變量的值呢?
默認情況下, 不可以在block中修改外界變量的值,因為block中的變量和外界的變量并不是同一個變量。
如果block中訪問到了外界的變量, block會將外界的變量拷貝一份到堆內(nèi)存中。因為block中使用的外界變量是copy的, 所以在調(diào)用之前修改外界變量的值, 不會影響到block中copy的值。

代碼如下

    int a = 10;
    NSLog(@"外部a的地址:&a = %p", &a);
    void (^myBlock)() = ^{
//        a = 50;
        NSLog(@"Block內(nèi)部a的地址:&a = %p", &a);
        NSLog(@"Block內(nèi)部a的值:a = %i", a);
    };
    a = 20;
    NSLog(@"Block內(nèi)部a的值:a = %i", a);

    myBlock();

    運行結(jié)果:
    [854:137804] 外部a的地址:&a = 0x7fff5fbff7fc
    [854:137804] Block內(nèi)部a的地址:&a = 0x7fff5fbff7e8
    [854:137804] Block內(nèi)部a的值:a = 10
    [854:137804] 外部a的值:a = 20


如果將上邊代碼做如下修改

int a = 10; => __block int a = 10;
運行結(jié)果如下:
[1029:184764] 外部a的地址:&a = 0x7fff5fbff7f8
[1029:184764] Block內(nèi)部a的地址:&a = 0x7fff5fbff7f8
[1029:184764] Block內(nèi)部a的值:a = 10
[1029:184764] 外部a的值:a = 20

運行結(jié)果我們可以發(fā)現(xiàn),Block內(nèi)部a的地址和外部a的地址是一樣的,
因此我們可以說加上__block之后是地址傳遞,而不加__block使用外部的變量是做了某些操作分配了新的內(nèi)存地址。而這個操作就是copy。

  • Block位于堆還是棧(ARC)
    根據(jù)Block在內(nèi)存中的位置分為三種類型NSGlobalBlock,NSStackBlock, NSMallocBlock。
    NSGlobalBlock:類似函數(shù),位于text段;
    NSStackBlock:位于棧內(nèi)存,函數(shù)返回后Block將無效;
    NSMallocBlock:位于堆內(nèi)存。

直接看代碼

typedef void (^blk) (void);
{
    __block int val = 10;
    __strong blk strongPointerBlock = ^{NSLog(@"val = %d", ++val);};
    NSLog(@"strongPointerBlock: %@", strongPointerBlock); //1
    __weak blk weakPointerBlock = ^{NSLog(@"val = %d", ++val);};
    NSLog(@"weakPointerBlock: %@", weakPointerBlock); //2
    NSLog(@"copyBlock: %@", [weakPointerBlock copy]); //3    
    NSLog(@"默認test %@", ^{NSLog(@"val = %d", ++val);}); //4
}
運行結(jié)果:
[1437:366203] strongPointerBlock: <__NSMallocBlock__: 0x7fbe7a6762e0>
[1437:366203] weakPointerBlock: <__NSStackBlock__: 0x7fff5746d9e0>
[1437:366203] copyBlock: <__NSMallocBlock__: 0x7fbe7a743240>
[1437:366203] 默認test <__NSStackBlock__: 0x7fff5746d9b8>

從運行結(jié)果可以看出
1>默認情況下,Block是在棧內(nèi)存中。
2>strong指針指向的block已經(jīng)放到堆上了。
3>weak指針指向的block還在棧上(這種聲明方法只在block上有效,正常的weak指針指向堆上對象,直接就會變nil,需要用strong指針過一道。)
4>copy了Block之后就會從棧轉(zhuǎn)移到堆上

在ARC下的另外一種情況,將block作為參數(shù)返回

- (__unsafe_unretained blk) blockTest {
    int val = 11;
    return ^{NSLog(@"val = %d", val);};
}
調(diào)用方
NSLog(@"block return from function: %@", [self blockTest]);

輸出結(jié)果:
block return from function: <__NSMallocBlock__: 0x7685640>

5>在ARC環(huán)境下,當(dāng)block作為參數(shù)返回的時候,block也會自動被移到堆上。

所有我們可以做出如下總結(jié):
如果block在棧中, block中訪問了外界的對象, 那么不會對對象進行retain操作。
如果block在堆中, block中訪問了外界的對象, 那么會對外界的對象進行一次retain。(在ARC下沒有retain的概念,這里只是為了后面循環(huán)引用的理解)。

了解更多:Objective-C Block的實現(xiàn)
[Block內(nèi)存管理](http://www.tanhao.me/pieces/310.html/

  • block的循環(huán)引用防止
@property(nonatomic, readwrite, copy) completionBlock completionBlock;

self.completionBlock = ^ {
        if (self.success) {
            self.success(self.responseData);
        }
    }
};

//對象有一個Block屬性,然而這個Block屬性中又引用了對象的其他
//成員變量,那么就會對這個變量本身產(chǎn)生強應(yīng)用,那么變量本身和他
//自己的Block屬性就形成了循環(huán)引用。在ARC下需要修改成這樣:
@property(nonatomic, readwrite, copy) completionBlock completionBlock;

__weak typeof(self) weakSelf = self;
self.completionBlock = ^ {
    if (weakSelf.success) {
        weakSelf.success(weakSelf.responseData);
    }
};

也就是生成一個對自身對象的弱引用,如果是倒霉催的項目還需要支持iOS4.3(好low啊),就用__unsafe_unretained替代__weak。如果是non-ARC環(huán)境下就將__weak替換為__block即可。non-ARC情況下,__block變量的含義是在Block中引入一個新的結(jié)構(gòu)體成員變量指向這個__block變量,那么__block typeof(self) weakSelf = self;就表示Block別再對self對象retain啦,這就打破了循環(huán)引用。

循環(huán)引用問題
  • MRC下對象的循環(huán)引用
    如下代碼
    Dog類
#import <Foundation/Foundation.h>
@class Person;
@interface Dog : NSObject
//@property(nonatomic, retain)Person *owner;
@property(nonatomic, assign)Person *owner;
@end

#import "Dog.h"
#import "Person.h"
@implementation Dog
-(void)dealloc
{
    NSLog(@"%s", __func__);
    [super dealloc];
}
@end

Person類

#import <Foundation/Foundation.h>
@class Dog;
@interface Person : NSObject
@property(nonatomic, retain)Dog *dog;
@end

#import "Person.h"
#import "Dog.h"
@implementation Person

- (void)dealloc
{
    NSLog(@"%s", __func__);
    self.dog = nil;
    [super dealloc];
}
@end

main函數(shù)

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Dog.h"

int main(int argc, const char * argv[]) {

    Person *p = [Person new];
    Dog *d = [Dog new];
    p.dog = d; // retain
    d.owner = p; // retain  assign
    
    [p release];
    [d release];
    return 0;
}

Person要擁有dog對象, 而dog對應(yīng)又要擁有Person對象, 此時會形成循環(huán)retain,在main執(zhí)行完的時候,dealloc方法不會自動被調(diào)用,造成內(nèi)存泄露。

引用關(guān)系如圖


解決辦法: 不要讓A retain B, B retain A,讓其中一方不要做retain操作即可。
替換前:@property(nonatomic, retain)Person *owner;
替換后:@property(nonatomic, assign)Person *owner;

  • 此處替換不用weak的原因,weak是ARC提供的防止循環(huán)引用的關(guān)鍵字。此處代碼是MRC的,所有使用assign關(guān)鍵字。

修正后引用關(guān)系


  • ARC下對象的循環(huán)引用
    • ARC和MRC一樣, 如果A擁有B, B也擁有A, 那么必須一方使用弱指針
@interface Person : NSObject

//@property (nonatomic, retain) Dog *dog;
@property (nonatomic, strong) Dog *dog;

@end

@interface Dog : NSObject

// 錯誤寫法, 循環(huán)引用會導(dǎo)致內(nèi)存泄露
//@property (nonatomic, strong) Person *owner;

// 正確寫法, 當(dāng)如果保存對象建議使用weak
//@property (nonatomic, assign) Person *owner;
@property (nonatomic, weak) Person *owner;
@end
其他
  • Objective-C指針與CoreFoundation指針之間的轉(zhuǎn)換(Toll-Free Bridging)
1.MRC下的內(nèi)存管理

倘若不使用ARC,手動管理內(nèi)存,思路比較清晰,使用完,release轉(zhuǎn)換后的對象即可。

//NSString 轉(zhuǎn) CFStringRef  
CFStringRef aCFString = (CFStringRef) [[NSString alloc] initWithFormat:@"%@", string];  
//...  
CFRelease(aCFString);  
  
  
//CFStringRef 轉(zhuǎn) NSString  
CFStringRef aCFString = CFStringCreateWithCString(kCFAllocatorDefault,  
                                                  bytes,  
                                                  NSUTF8StringEncoding);  
NSString *aNSString = (NSString *)aCFString;  
//...  
[aNSString release];  
2.ARC下的內(nèi)存管理

ARC :只會對OC對象進行內(nèi)存管理,蘋果有句名言:ARC is only for NSObject。但是對c對象或是CF開頭的對象,即存在于 Core Foundation框架 (CoreFoundation.framework 是一組C語言接口)中的對象無效,需要手動的retain 和release(CF中沒有autorelease)。

CocoaFoundation指針與CoreFoundation指針轉(zhuǎn)換,需要考慮的是所指向?qū)ο笏袡?quán)的歸屬。ARC提供了3個修飾符來管理。

  • __bridge,__bridge_retained和__bridge_transfer三個轉(zhuǎn)換關(guān)鍵字。
    • __bridge只做類型轉(zhuǎn)換,但是不修改對象(內(nèi)存)管理權(quán);
    • __bridge_retained(也可以使用CFBridgingRetain)將Objective-C的對象轉(zhuǎn)換為Core Foundation的對象,同時將對象(內(nèi)存)的管理權(quán)交給我們,后續(xù)需要使用CFRelease或者相關(guān)方法來釋放對象;
    • __bridge_transfer(也可以使用CFBridgingRelease)將Core Foundation的對象轉(zhuǎn)換為Objective-C的對象,同時將對象(內(nèi)存)的管理權(quán)交給ARC。

使用__bridge_retained 或者 CFBridgingRetain()

- (void)viewDidLoad  
{  
    [super viewDidLoad];  
      
    NSString *aNSString = [[NSString alloc]initWithFormat:@"test"];  
    CFStringRef aCFString = (__bridge_retained CFStringRef) aNSString;  
      
    (void)aCFString;  
      
    //正確的做法應(yīng)該執(zhí)行CFRelease  
    //CFRelease(aCFString);   
} 

使用__bridge_transfer 或者 CFBridgingRelease()

- (void)viewDidLoad  
{  
    [super viewDidLoad];  
      
    NSString *aNSString = [[NSString alloc]initWithFormat:@"test"];  
    CFStringRef aCFString = (__bridge_retained CFStringRef) aNSString;  
    aNSString = (__bridge_transfer NSString *)aCFString;  
} 

使用__bridge

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    NSString *aNSString = [[NSString alloc]initWithFormat:@"test"];
    CFStringRef aCFString = (__bridge CFStringRef)aNSString;
    
    (void)aCFString;
}
  • 3中轉(zhuǎn)換方式總結(jié)
轉(zhuǎn)換方式 轉(zhuǎn)換結(jié)果
__bridge 不改變對象所有權(quán)
__bridge_retained 或者 CFBridgingRetain() 解除 ARC 所有權(quán)
__bridge_transfer 或者 CFBridgingRelease() 給予 ARC 所有權(quán)

了解更多:Toll-Free Bridging

iOS中內(nèi)存分析
  • 靜態(tài)內(nèi)存分析
    靜態(tài)內(nèi)存分析是不運行程序,直接對代碼進行內(nèi)存分析。查看代碼是否偶內(nèi)存泄露。

    Analyze主要分析以下四種問題

    • 邏輯錯誤:訪問空指針或未初始化的變量等;
    • 內(nèi)存管理錯誤:如內(nèi)存泄漏等;
    • 聲明錯誤:從未使用過的變量;
    • Api調(diào)用錯誤:未包含使用的庫和框架。

    優(yōu)點:分析速度快,并且可以對所有的代碼進行內(nèi)存分析。
    缺點:分析不一定準(zhǔn)確(沒有真正的運行程序,根據(jù)代碼的上下文語法結(jié)構(gòu))
    注意:如果有提示內(nèi)存泄露,一定要檢查代碼,90%以上的都是準(zhǔn)確的。

1>在XCode Product菜單下,點擊Analyze對工程進行靜態(tài)分析(shift+command+b)。

2>在開啟arc的環(huán)境下,輸入以下一段代碼:

+(UIImage*)getSubImage:(unsigned long)ulUserHeader
{
    UIImage * sourceImage = [UIImage imageNamed:@"header.png"];
    CGFloat height = sourceImage.size.height;
    CGRect rect = CGRectMake(0 + ulUserHeader*height, 0, height, height);
 
    CGImageRef imageRef = CGImageCreateWithImageInRect([sourceImage CGImage], rect);
    UIImage* smallImage = [UIImage imageWithCGImage:imageRef];
    //CGImageRelease(imageRef);
 
    return smallImage;
}

3>使用Analyze進行分析,在導(dǎo)航欄Analyze選擇Analyzer查看分析結(jié)果:


  • 動態(tài)內(nèi)存分析
    動態(tài)內(nèi)存分析:程序真正運行起來的時候?qū)Τ绦蜻M行內(nèi)存分析、
    (查看內(nèi)存的分配/內(nèi)存泄露)。
    優(yōu)點:分析非常準(zhǔn)確,如果發(fā)現(xiàn)內(nèi)存泄露,基本可以判斷是代碼問題。
    缺點:分析效率低(只有真正運行了一段代碼,才能對該代碼進行內(nèi)存分析,如果程序中的某一段有內(nèi)存泄露的沒有執(zhí)行到就不會被檢測出來。)
    注意:如果發(fā)現(xiàn)內(nèi)存泄露,基本都需要修改代碼。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容