二級指針與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的實例昼激。該實例是用來表明發(fā)生了某種錯誤螺戳。在ARC中由于使用異常處理會造成內存管理的不便(可能造成內存泄露霞溪,或者加入大量樣板代碼)隆圆,所以用NSError表明發(fā)生了錯誤是一種不錯的選擇锦担,蘋果的API中也大量使用了NSError。

這里請關注[NSJSONSerialization JSONObjectWithData:data options:0 error:&error]的最后一個參數:error:(NSError **)error;凉倚。該方法使用了二級指針作為參數傳入彭则,經由此參數可以將方法中新創(chuàng)建的NSError對象回傳給調用者,所以該參數也稱為“輸出參數”占遥。從這種類型的參數入手俯抖,后面我們將討論一個很嚴肅的問題~

我們來實現一個類似的方法(也就是方法里新創(chuàng)建一個對象回傳給調用者)

1. 不用二級指針我直接傳個view進方法里不就可以創(chuàng)建一個view了嗎?

代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;        // 聲明一個view瓦胎,但是還有沒創(chuàng)建
    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:方法里搔啊,方法里會幫我創(chuàng)建一個view柬祠,那么thisIsNilView不就有值了?

讓我們看看運行結果:

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

哪里出問題了负芋?方法里明明創(chuàng)建出了一個view奥住?

我們來探究探究到底是哪里出了問題旧蛾。

回想下thisIsNilView是個什么東西莽龟?恩,是個指向UIView的指針(是個指針锨天、是個指針毯盈、是個指針),那么我們來看看指針在方法里是否正確指向了生成的UIView實例病袄。

我改動了下代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;
    NSLog(@"1.0 thisIsNilView指向的實例 : %@",thisIsNilView);
    NSLog(@"1.1 thisIsNilView指針的地址 : %p",&thisIsNilView);
    NSLog(@"--------- 開始執(zhí)行createView:方法 ---------");
    [self createView:thisIsNilView];
    NSLog(@"--------- 執(zhí)行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);
}

為了方便查看結果搂赋,加了幾行打印~

 1.0 thisIsNilView指向的實例 : (null)
 1.1 thisIsNilView指針的地址 : 0x16fd35f18
 
 --------- 開始執(zhí)行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
 --------- 執(zhí)行createView:方法結束 ---------
 
 4.0 thisIsNilView指向的實例 : (null)
 4.1 thisIsNilView指針的地址 : 0x16fd35f18

額,好像thisIsNilView這個指針(位于0x16fd35f18這塊內存區(qū)域中)傳入方法后變成另外一個指針(位于0x16fd35ee8這塊內存區(qū)域中)了啊益缠。

插個內存圖理解下:

第一步:

我是配圖

第二步:

我是配圖

第三步:

我是配圖

第四步:

我是配圖

為何第二步進入方法后會憑空多出一個指針脑奠?哦忘了說,指針也是個變量幅慌,指針作為參數傳遞的時候宋欺,指針“本身”也是值傳遞,也就是說復制了一個“與原指針指向相同內存地址”的指針欠痴。好像有點繞迄靠,其實就是第二步的圖。

回想下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. 那得用二級指針才能在方法里創(chuàng)建并回傳給調用者一個view是嗎龟糕?

是不是我們先上個代碼看看:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *thisIsNilView = nil;
    NSLog(@"1.0 thisIsNilView指向的實例 : %@",thisIsNilView);
    NSLog(@"1.1 thisIsNilView指針的地址 : %p",&thisIsNilView);
    NSLog(@"--------- 開始執(zhí)行createViewWithSecRankPointer:方法 ---------");
    [self createViewWithSecRankPointer:&thisIsNilView];
    NSLog(@"--------- 執(zhí)行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指向一個全新創(chuàng)建的UIView實例把3碘!尺锚!*

4. 執(zhí)行完方法了珠闰,那么thisIsNilView這個指針應該指向的是剛才在方法里新創(chuàng)建的view,那么我們就完成了一個“輸出參數”了對嗎瘫辩。

看看執(zhí)行結果:

 1.0 thisIsNilView指向的實例 : (null)
 1.1 thisIsNilView指針的地址 : 0x16fd75f18
 
 --------- 開始執(zhí)行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
 --------- 執(zhí)行createViewWithSecRankPointer:方法結束 ---------
 
 4.0 thisIsNilView指向的實例 : <UIView: 0x15bd07ff0; frame = (100 100; 100 100); layer = <CALayer: 0x174033f20>>
 4.1 thisIsNilView指針的地址 : 0x16fd75f18

很好伏嗜,執(zhí)行方法完畢后thisIsNilView有值了坛悉!而且還是方法里新創(chuàng)建的UIView實例!

等等承绸!好像哪里有點不對裸影!

為何方法里的*view(也就是thisIsNilView指針)和方法外面的thisIsNilView不是同一個?军熏?轩猩??荡澎?

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

在實踐結果里衔瓮,方法內部出現了一個位于0x16fd75f10內存地址中的指針浊猾,然后讓這個指針指向了一個新創(chuàng)建的UIView實例,然鵝這和thisIsNilView這個指針(位于0x16fd75f18內存地址)有毛線關系热鞍?葫慎??薇宠?偷办?然鵝出了方法thisIsNilView居然還是指向了那個新創(chuàng)建的對象!3胃邸=费摹!回梧!

畫個內存圖看看先:

第一步:

我是配圖

第二步:

我是配圖

第三步:

我是配圖

第四步:

我是配圖

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

1 第二步為何會多出一個指針废岂?

2 第四步為何會把原先指向nil的thisIsNilView指向了新創(chuàng)建的UIView對象?


3. 總算要說說ARC不為人知的特性了

單從上述代碼時無法解釋為何會產生這種現象的狱意。

在瀏覽官方文檔《Transitioning to ARC Release Notes》的時候湖苞,偶然發(fā)現有這么一段:

我是配圖

文中提到,二級指針作為參數“通诚甓冢”都是__autoreleasing修飾的财骨,注意通常這個詞,后面會提到藏姐。當實際傳入的參數為__strong修飾的時候隆箩,編譯器會創(chuàng)建一個用__autoreleasing修飾的臨時變量tmp,用來和方法參數的修飾符匹配羔杨,方法執(zhí)行完畢后再重新用tmp賦值回error捌臊。 (蘋果這么做主要是為了保證在方法內部創(chuàng)建出來的對象能夠被良好地釋放,因為createViewWithSecRankPointer:方法不能保證調用者在拿到這個對象后能夠合理釋放掉)
編譯器的這種行為剛好能夠印證我們上述“很神奇”的兩個地方:

1. tmp變量剛好就是第二步中多出的那個指針0x16fd75f10问畅,用這個臨時變量來保存新創(chuàng)建的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(@"--------- 開始執(zhí)行createViewWithSecRankPointer:方法 ---------");
    [self createViewWithSecRankPointer:&thisIsNilView];
    NSLog(@"--------- 執(zhí)行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);
}

直接看看執(zhí)行結果:

 1.0 thisIsNilView指向的實例 : (null)
 1.1 thisIsNilView指針的地址 : 0x16fde9f18
 
 --------- 開始執(zhí)行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
 --------- 執(zhí)行createViewWithSecRankPointer:方法結束 ---------
 
 4.0 thisIsNilView指向的實例 : <UIView: 0x10020c4a0; frame = (100 100; 100 100); layer = <CALayer: 0x170222740>>
 4.1 thisIsNilView指針的地址 : 0x16fde9f18

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

  • 補充一點:createViewWithSecRankPointer:方法就算內部不創(chuàng)建對象滚粟,參數也會被編譯器自動加上__autoreleasing寻仗。

總結下這篇文章講了什么

1. 指針作為參數傳遞的時候,指針本身是值傳遞凡壤。

2. 為何用一級指針傳入參數無法成為“輸出參數”署尤。

3. 二級指針作為參數傳遞時,ARC為了校準語義亚侠,會進行“自動補全”功能曹体。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市硝烂,隨后出現的幾起案子箕别,更是在濱河造成了極大的恐慌,老刑警劉巖滞谢,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件串稀,死亡現場離奇詭異,居然都是意外死亡爹凹,警方通過查閱死者的電腦和手機厨诸,發(fā)現死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來禾酱,“玉大人,你說我怎么就攤上這事绘趋〔眨” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵陷遮,是天一觀的道長滓走。 經常有香客問我,道長帽馋,這世上最難降的妖魔是什么搅方? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任比吭,我火速辦了婚禮,結果婚禮上姨涡,老公的妹妹穿的比我還像新娘衩藤。我一直安慰自己,他們只是感情好涛漂,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布赏表。 她就那樣靜靜地躺著,像睡著了一般匈仗。 火紅的嫁衣襯著肌膚如雪瓢剿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天悠轩,我揣著相機與錄音间狂,去河邊找鬼。 笑死火架,一個胖子當著我的面吹牛鉴象,可吹牛的內容都是我干的。 我是一名探鬼主播距潘,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼炼列,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了音比?” 一聲冷哼從身側響起俭尖,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎洞翩,沒想到半個月后稽犁,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡骚亿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年已亥,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片来屠。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡虑椎,死狀恐怖,靈堂內的尸體忽然破棺而出俱笛,到底是詐尸還是另有隱情捆姜,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布迎膜,位于F島的核電站泥技,受9級特大地震影響,放射性物質發(fā)生泄漏磕仅。R本人自食惡果不足惜珊豹,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一簸呈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧店茶,春花似錦蜕便、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至段直,卻和暖如春吃溅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鸯檬。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工决侈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人喧务。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓赖歌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親功茴。 傳聞我的和親對象是個殘疾皇子庐冯,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內容

  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,160評論 30 470
  • __block和__weak修飾符的區(qū)別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,327評論 0 6
  • 1.寫一個NSString類的實現 +(id)initWithCString:(c*****t char *)nu...
    韓七夏閱讀 3,772評論 2 37
  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛閱讀 1,988評論 0 7
  • 臨早坎穿, 臨晚展父。 萬物并作, 不臨其盡玲昧。 人如此栖茉, 循環(huán), 不解孵延。
    借酒消愁閱讀 174評論 0 0