二級指針與ARC鮮爲人知的特性

先看一眼熟知的代碼

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSData *data = [@"{\"key\":\"value\"}" dataUsingEncoding:NSUTF8StringEncoding];
    
    NSError *error = nil;
    id dataObj = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
    if (error) {
        NSLog(@"解析JSON出錯。 error : %@",error);
    } else {
        NSLog(@"解析JSON正確。 dataObj : %@",dataObj);
    }
}
複製代碼

上述代碼中,出現了NSError的實例。該實例是用來代表發生了某種錯誤。在ARC中因爲使用異常處理會形成內存管理的不便(可能形成內存泄露,或者加入大量樣板代碼),因此用NSError代表發生了錯誤是一種不錯的選擇,蘋果的API中也大量使用了NSError。objective-c

這裏請關注[NSJSONSerialization JSONObjectWithData:data options:0 error:&error]的最後一個參數:error:(NSError **)error;。該方法使用了二級指針做爲參數傳入,經由此參數能夠將方法中新建立的NSError對象回傳給調用者,因此該參數也稱爲「輸出參數」。從這種類型的參數入手,後面咱們將討論一個很嚴肅的問題~函數

咱們來實現一個相似的方法(也就是方法裏新建立一個對象回傳給調用者)

1. 不用二級指針我直接傳個view進方法裏不就能夠建立一個view了嗎?

代碼:this

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;		// 聲明一個view,可是還有沒建立
    NSLog(@"1. thisIsNilView指向的實例 : %@",thisIsNilView);
    [self createView:thisIsNilView];
    NSLog(@"4. thisIsNilView指向的實例 : %@",thisIsNilView);
}

- (void)createView:(UIView *)view {
    NSLog(@"2. 方法裏的view指向的實例 : %@",view);
    view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    NSLog(@"3. 方法裏的view指向的實例 : %@",view);
}
複製代碼

看起來很簡單呢,我聲明一個空的thisIsNilView,傳到一個createView:方法裏,方法裏會幫我建立一個view,那麼thisIsNilView不就有值了?spa

讓咱們看看運行結果:3d

1. thisIsNilView指向的實例 : (null)
 2. 方法裏的view指向的實例 : (null)
 3. 方法裏的view指向的實例 : <UIView: 0x7f956ee00220; frame = (100 100; 100 100); layer = <CALayer: 0x600000029420>>
 4. thisIsNilView指向的實例 : (null)
複製代碼

哪裏出問題了?方法裏明明建立出了一個view啊?指針

咱們來探究探究究竟是哪裏出了問題。code

回想下thisIsNilView是個什麼東西?恩,是個指向UIView的指針(是個指針、是個指針、是個指針),那麼咱們來看看指針在方法裏是否正確指向了生成的UIView實例。cdn

我改動了下代碼:對象

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;
    NSLog(@"1.0 thisIsNilView指向的實例 : %@",thisIsNilView);
    NSLog(@"1.1 thisIsNilView指針的地址 : %p",&thisIsNilView);
    NSLog(@"--------- 開始執行createView:方法 ---------");
    [self createView:thisIsNilView];
    NSLog(@"--------- 執行createView:方法結束 ---------");
    NSLog(@"4.0 thisIsNilView指向的實例 : %@",thisIsNilView);
    NSLog(@"4.1 thisIsNilView指針的地址 : %p",&thisIsNilView);
}

- (void)createView:(UIView *)view {
    NSLog(@"2.0 方法裏的view指向的實例 : %@",view);
    NSLog(@"2.1 方法裏的view指針的地址 : %p",&view);
    view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    NSLog(@"3.0 方法裏的view指向的實例 : %@",view);
    NSLog(@"3.1 方法裏的view指針的地址 : %p",&view);
}
複製代碼

爲了方便查看結果,加了幾行打印~blog

1.0 thisIsNilView指向的實例 : (null)
 1.1 thisIsNilView指針的地址 : 0x16fd35f18
 
 --------- 開始執行createView:方法 ---------
 2.0 方法裏的view指向的實例 : (null)
 2.1 方法裏的view指針的地址 : 0x16fd35ee8
 
 3.0 方法裏的view指向的實例 : <UIView: 0x12de0b6a0; frame = (100 100; 100 100); layer = <CALayer: 0x610000034640>>
 3.1 方法裏的view指針的地址 : 0x16fd35ee8
 --------- 執行createView:方法結束 ---------
 
 4.0 thisIsNilView指向的實例 : (null)
 4.1 thisIsNilView指針的地址 : 0x16fd35f18
複製代碼

額,好像thisIsNilView這個指針(位於0x16fd35f18這塊內存區域中)傳入方法後變成另一個指針(位於0x16fd35ee8這塊內存區域中)了啊。

插個內存圖理解下:

第一步:

我是配圖

第二步:

我是配圖

第三步:

我是配圖

第四步:

我是配圖

爲什麼第二步進入方法後會憑空多出一個指針?哦忘了說,指針也是個變量,指針做爲參數傳遞的時候,指針「自己」也是值傳遞,也就是說複製了一個「與原指針指向相同內存地址」的指針。好像有點繞,其實就是第二步的圖。

回想下C語言基礎中的參數傳遞:基本數據類型是複製一份進行傳遞,可是指針傳遞是引用傳遞,能夠修改變量自己的內容。說是這樣說,可是不夠全面。指針傳遞其實也是個複製傳遞,只不過複製的是「指針」,可是「複製後的指針」中的內容(也就是指針指向的地址)仍是指向了原來指向的內容。

這個指針複製傳遞仍是有那麼點兒繞,咱們用指針與int基本類型作個對比:

int a = 10;

int *p = &a;

對應關係:

a 是個 int 類型的變量;

a 的內容是 10;

p 是個 int * 類型的變量(俗稱指針);

p 的內容是 a這個變量在內存中的地址(好比0xa);

函數:

void testIntCopy(int b) {
    int c = b;
}

void testPointCopy(int *pointer) {
    printf("%p",pointer);
}
複製代碼

在testIntCopy中傳入a,那麼將會拷貝一份a的內容:10(數值) 到 b(int類型的變量) 中。以後就能夠正常使用了。

在testPointCopy中傳入p,那麼將會拷貝一份p的內容:指向a在內存中的地址(如0xa) 到 pointer(int *類型的變量) 中。以後就能夠正常使用了,好比修改pointer指向的內存中的值。

這樣子理解是否是輕鬆一點?那麼以前第二步的圖就能夠理解了。

這說明了一個問題:一級指針做爲參數傳遞沒法修改原指針指向的值。


2. 那得用二級指針才能在方法裏建立並回傳給調用者一個view是嗎?

是否是咱們先上個代碼看看:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;
    NSLog(@"1.0 thisIsNilView指向的實例 : %@",thisIsNilView);
    NSLog(@"1.1 thisIsNilView指針的地址 : %p",&thisIsNilView);
    NSLog(@"--------- 開始執行createViewWithSecRankPointer:方法 ---------");
    [self createViewWithSecRankPointer:&thisIsNilView];
    NSLog(@"--------- 執行createViewWithSecRankPointer:方法結束 ---------");
    NSLog(@"4.0 thisIsNilView指向的實例 : %@",thisIsNilView);
    NSLog(@"4.1 thisIsNilView指針的地址 : %p",&thisIsNilView);
}

- (void)createViewWithSecRankPointer:(UIView **)view {
    NSLog(@"2.0 方法裏的*view指向的實例 : %@",*view);
    NSLog(@"2.1 方法裏的*view指針的地址 : %p",view);
    *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    NSLog(@"3.0 方法裏的*view指向的實例 : %@",*view);
    NSLog(@"3.1 方法裏的*view指針的地址 : %p",view);
}
複製代碼

注意方法已經不是原來的方法了,注意方法裏所打印的東西也已經有所變動。

看結果前咱們先分析分析這些代碼究竟幹了什麼:

1. 有一個UIView * 類型的指針: thisIsNilView ,而後應該還有一個指向thisIsNilView這個指針的指針:咱們姑且假設它爲thisIsNilViewFatherPointer。

2. 咱們要進入createViewWithSecRankPointer:方法了!按照上文講的「指針值傳遞」,咱們傳遞了thisIsNilViewFatherPointer的值(也就是thisIsNilView的地址)給了createViewWithSecRankPointer:方法。此時方法裏的view(二級指針),應該是個thisIsNilViewFatherPointer指針的拷貝,但指向的仍是thisIsNilView這個指針(內容從thisIsNilViewFatherPointer拷貝過來了嘛)。

3. 好的,我既然能夠拿到thisIsNilView這個指針了(經過*view),那麼我總算能夠修改thisIsNilView這個指針的指向了,讓thisIsNilView指向一個全新建立的UIView實例把!!!

4. 執行完方法了,那麼thisIsNilView這個指針應該指向的是剛纔在方法裏新建立的view,那麼咱們就完成了一個「輸出參數」了對嗎。

看看執行結果:

1.0 thisIsNilView指向的實例 : (null)
 1.1 thisIsNilView指針的地址 : 0x16fd75f18
 
 --------- 開始執行createViewWithSecRankPointer:方法 ---------
 2.0 方法裏的*view指向的實例 : (null)
 2.1 方法裏的*view指針的地址 : 0x16fd75f10
 3.0 方法裏的*view指向的實例 : <UIView: 0x15bd07ff0; frame = (100 100; 100 100); layer = <CALayer: 0x174033f20>>
 3.1 方法裏的*view指針的地址 : 0x16fd75f10
 --------- 執行createViewWithSecRankPointer:方法結束 ---------
 
 4.0 thisIsNilView指向的實例 : <UIView: 0x15bd07ff0; frame = (100 100; 100 100); layer = <CALayer: 0x174033f20>>
 4.1 thisIsNilView指針的地址 : 0x16fd75f18
複製代碼

很好,執行方法完畢後thisIsNilView有值了!並且仍是方法裏新建立的UIView實例!

等等!好像哪裏有點不對!

爲什麼方法裏的*view(也就是thisIsNilView指針)和方法外面的thisIsNilView不是同一個?????

根據咱們上述4點嚴謹的分析,方法裏的*view應該就是thisIsNilView這個指針無誤!

在實踐結果裏,方法內部出現了一個位於0x16fd75f10內存地址中的指針,而後讓這個指針指向了一個新建立的UIView實例,然鵝這和thisIsNilView這個指針(位於0x16fd75f18內存地址)有毛線關係?????然鵝出了方法thisIsNilView竟然仍是指向了那個新建立的對象!!!!!

畫個內存圖看看先:

第一步:

我是配圖

第二步:

我是配圖

第三步:

我是配圖

第四步:

我是配圖

這裏真的有兩個很神奇的地方:

1 第二步爲什麼會多出一個指針?

2 第四步爲什麼會把原先指向nil的thisIsNilView指向了新建立的UIView對象?


3. 總算要說說ARC鮮爲人知的特性了

單從上述代碼時沒法解釋爲什麼會產生這種現象的。

在瀏覽官方文檔《Transitioning to ARC Release Notes》的時候,偶然發現有這麼一段:

我是配圖

文中提到,二級指針做爲參數「一般」都是__autoreleasing修飾的,注意一般這個詞,後面會提到。當實際傳入的參數爲__strong修飾的時候,編譯器會建立一個用__autoreleasing修飾的臨時變量tmp,用來和方法參數的修飾符匹配,方法執行完畢後再從新用tmp賦值回error。 (蘋果這麼作主要是爲了保證在方法內部建立出來的對象可以被良好地釋放,由於createViewWithSecRankPointer:方法不能保證調用者在拿到這個對象後可以合理釋放掉) 編譯器的這種行爲恰好可以印證咱們上述「很神奇」的兩個地方:

1. tmp變量恰好就是第二步中多出的那個指針0x16fd75f10,用這個臨時變量來保存新建立的UIView對象

2. error = tmp恰好對應咱們的第四步,出了方法後從新賦值給原來的變量thisIsNilView

BUT:咱們的方法參數並非(UIView * __autoreleasing *)這種類型啊,咱們是(UIView **)類型呢。其實蘋果文檔裏說的「一般」是有依據的:

編譯器會把指向OC對象的指針的二級指針參數自動加上__autoreleasing修飾符。

咱們能夠經過Xcode自動補全功能一窺究竟:

我是配圖

4. 咱們反過來驗證下ARC鮮爲人知的特性

既然文檔裏說了,__strong__autoreleasing語義不符,因此編譯器會這麼作,那麼若是咱們使用__autoreleasing修飾了thisIsNilView指針呢。

看看修改後的代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView * __autoreleasing thisIsNilView = nil;
    NSLog(@"1.0 thisIsNilView指向的實例 : %@",thisIsNilView);
    NSLog(@"1.1 thisIsNilView指針的地址 : %p",&thisIsNilView);
    NSLog(@"--------- 開始執行createViewWithSecRankPointer:方法 ---------");
    [self createViewWithSecRankPointer:&thisIsNilView];
    NSLog(@"--------- 執行createViewWithSecRankPointer:方法結束 ---------");
    NSLog(@"4.0 thisIsNilView指向的實例 : %@",thisIsNilView);
    NSLog(@"4.1 thisIsNilView指針的地址 : %p",&thisIsNilView);
}

- (void)createViewWithSecRankPointer:(UIView **)view {
    NSLog(@"2.0 方法裏的*view指向的實例 : %@",*view);
    NSLog(@"2.1 方法裏的*view指針的地址 : %p",view);
    *view = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    NSLog(@"3.0 方法裏的*view指向的實例 : %@",*view);
    NSLog(@"3.1 方法裏的*view指針的地址 : %p",view);
}
複製代碼

直接看看執行結果:

1.0 thisIsNilView指向的實例 : (null)
 1.1 thisIsNilView指針的地址 : 0x16fde9f18
 
 --------- 開始執行createViewWithSecRankPointer:方法 ---------
 2.0 方法裏的*view指向的實例 : (null)
 2.1 方法裏的*view指針的地址 : 0x16fde9f18
 3.0 方法裏的*view指向的實例 : <UIView: 0x10020c4a0; frame = (100 100; 100 100); layer = <CALayer: 0x170222740>>
 3.1 方法裏的*view指針的地址 : 0x16fde9f18
 --------- 執行createViewWithSecRankPointer:方法結束 ---------
 
 4.0 thisIsNilView指向的實例 : <UIView: 0x10020c4a0; frame = (100 100; 100 100); layer = <CALayer: 0x170222740>>
 4.1 thisIsNilView指針的地址 : 0x16fde9f18
複製代碼

在語義相符的狀況下,傳入的就是&thisIsNilView無誤,編譯器不會添加額外代碼。

  • 補充一點:createViewWithSecRankPointer:方法就算內部不建立對象,參數也會被編譯器自動加上__autoreleasing。

總結下這篇文章講了什麼

1. 指針做爲參數傳遞的時候,指針自己是值傳遞。

2. 爲什麼用一級指針傳入參數沒法成爲「輸出參數」。

3. 二級指針做爲參數傳遞時,ARC爲了校準語義,會進行「自動補全」功能。

相關文章
相關標籤/搜索