先看一眼熟知的代碼
- (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為了校準語義亚侠,會進行“自動補全”功能曹体。