如你所知,已廢棄(Deprecated)的API指的是那些已經過時的並且在將來某個時間最終會被移除掉的方法或類。通常,蘋果在引入一個更優秀的API後就會把原來的API給廢棄掉。因為,新引入的API通常意味著可以更好的發揮新硬體或作業系統的效能,或者可以使用一些在構建原有API時根本還沒有的語言特性(e.g. blocks)。

每當蘋果新增新方法的時候,他們都會在方法宣告的後面用一個很特殊的巨集來標明哪些iOS版本支援它們。例如,在UIViewController中,蘋果引入了一個使用block來處理回撥的方法用來展示一個模態controller,它的宣告是這樣的:

1
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^)(void))completion NS_AVAILABLE_IOS(5_0);

注意到NS_AVAILABLE_IOS(5_0)了嗎?這就告訴我們這個方法可以在iOS5.0及以後的版本中使用。如果我們在比指定版本更老的版本中呼叫這個方法,就會引起崩潰。

那被這個方法替換了的那個舊方法又怎麼樣了呢?同樣,它的聲明後面也帶了一個類似的語法,表示它已經被廢棄了:

1
- (void)presentModalViewController:(UIViewController *)modalViewController animated:(BOOL)animated NS_DEPRECATED_IOS(2_0, 6_0);

NS_DEPRECATED_IOS(2_0, 6_0)這個巨集中有兩個版本號。前面一個表明了這個方法被引入時的iOS版本,後面一個表明它被廢棄時的iOS版本。被廢棄並不是指這個方法就不存在了,只是意味著我們應當開始考慮將相關程式碼遷移到新的API上去了。

還有類似形式的一些巨集用在iOS和OS X共用的類上。比如NSArray中的這個方法:

1
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0);

這裡的NS_AVAILABLE巨集告訴我們這方法分別隨Mac OS 10.8和iOS 6.0被引入。和NS_DEPRECATED_IOS類似,也有個巨集叫NS_DEPRECATED,但它的引數要稍微複雜些:

1
- (void)removeObjectsFromIndices:(NSUInteger *)indices numIndices:(NSUInteger)cnt NS_DEPRECATED(10_0, 10_6, 2_0, 4_0);

這裡表示這個方法隨Mac OS 10.0和iOS 2.0被引入,在Mac OS 10.6和iOS 4.0後被廢棄。

Easy Come, Easy Go

上週我們討論了在iOS7和Mac OS 10.9 SDK中被新引入的Base64 API。有趣的是,有一組有相同功能的Base64方法,在被引入的同時也被廢棄掉了。為什麼蘋果在引入一個API的同時又把它廢棄掉了?那不是毫無意義的嗎?好吧,其實也不是——它在下面這種情況下就非常有意義:

實際上,這些現在已經廢棄的Base64方法從iOS4和Mac 0S 10.6開始就一直存在,只是它們是私有的。直到現在蘋果才把它們公開,大概是蘋果一直對它們的實現不滿意,一直都想把它們改寫。

果然,在iOS7中,蘋果選定了一個他們感到滿意的Base64 API,並且將它新增到了NSData的一個公有類別中。但現在,他們知道老方法已經被取代,不會被改寫了,因此他們把它公開出來。當開發者的app仍然需要支援iOS6及以前的版本時,就有了一個系統內建的Base64 api可以用。

這就是為什麼,如果你檢視這些新API的方法宣告,可以看到NS_DEPRECATED巨集部分中的起始版本是4_0,雖然實際上直到iOS7之前,它從來都沒有被作為公有API被引入過:

1
- (NSString *)base64Encoding NS_DEPRECATED(10_6, 10_9, 4_0, 7_0);

這告訴你,基於iOS7 SDK開發的app如果呼叫了這個方法,它同樣可以執行在iOS4+或Mac OS 10.6+的系統上而不會崩潰。很有用的吧?

如何使用已廢棄的API

那麼,如果我們有一個app需要同時支援iOS6和iOS7,想用內建的Base64方法,我們該怎麼做呢?事實上,這相當簡單,你只需要呼叫這些廢棄的API就可以了。

那樣編譯器不是會產生警告嗎?不會——只有你的deployment target版本號設定成大於或等於方法被棄用的版本號的時候才會收到編譯器警告。只要你仍然在支援那些還沒有廢棄這個方法的iOS版本,都不會收到警告。

那麼,如果蘋果決定在iOS8中移除已棄用的Base64方法,你的應用程式會發生情況?簡單來說,它肯定會崩潰,但是不要讓這把你嚇跑了:蘋果不可能只在幾個iOS版本後就將已廢棄的API給移除(絕大多數已廢棄的API在任何的iOS版本中都還沒有被移除),除非你決定不再更新你的app,否則在你放棄支援iOS6之前有很多機會都可以更新到新的API。

但是如果假定我們在最壞的情況下(例如:我們不更新我們的app了,而蘋果突然宣佈了一個零容忍的不再向下相容的政策),怎樣讓我們的程式碼保持永不過時並且仍然能夠支援舊的系統版本呢?

這其實很簡單,我們只需要做一些執行時的方法檢測。使用NSObject的respondsToSelector:方法,我們可以檢測,如果新的API存在,我們就呼叫它。否則,我們退回到已廢棄的API。很簡單:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSData *someData = ...
NSString *base64String = nil;
// Check if new API is available
if ([someData respondsToSelector:@selector(base64EncodedDataWithOptions:)])
{
  // It exists, so let's call it
  base64String = [someData base64EncodedDataWithOptions:0];
}
else
{
  // Use the old API
  base64String = [someData base64Encoding];
}

此程式碼在iOS4及以上版本中有效,並且如果蘋果在未來的iOS版本中移除base64Encoding方法後,同樣可以正常工作。

為其他開發者編碼的時候

如果你是在寫一個app,這一切都很好,但是如果你是在編寫一個給其他人使用的程式碼庫呢?如果project的target是iOS4或iOS6的時候,上面的程式碼會工作的很好。但是如果deployment target是iOS 7+的時候,你就會收到編譯器警告,說你使用了已廢棄的base64Encoding方法。

該程式碼實際上永遠都可以正常工作,因為那個方法在執行時永遠都不會被呼叫(因為respondsToSelector:那個檢查在iOS7上總是會返回YES)。但是可惜的是,編譯器還不是足夠的聰明能發現這點。而且,比如像我,你不會想用那些會產生編譯器警告的第三方庫,你肯定也不想自己的庫中產生任何警告。

那麼,我們如何改寫我們的程式碼,以便它可以用於任何deployment target,而不會產生警告?幸好,有一個編譯器巨集指令可以基於不同的deployment target做不同的程式碼分支。取決於app是為哪個最小的iOS版本編譯的,我們可以用__IPHONE_OS_VERSION_MIN_REQUIRED這個巨集來生成不同的程式碼。

下面的程式碼可以工作在任何的iOS版本上(不管是過去的還是將來的),而且不會產生任何警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0
// Check if new API is not available
if (![someData respondsToSelector:@selector(base64EncodedDataWithOptions:)])
{
	// Use the old API
	base64String = [someData base64Encoding];
}
else
#endif
{
	// Use the new API
	base64String = [someData base64EncodedDataWithOptions:0];
}

看清楚我們在這裡做了什麼嗎?我們變換了respondsToSelector:的用法:我們用它來測試是否新的API不可用,然後將整段程式碼放到一個條件程式碼塊中,這樣它就只會在deployment target比iOS7低的情況下才會被編譯。如果app是為iOS6編譯的,它就會先檢查新的API是否存在,如果不存在就呼叫舊的API。如果app是為iOS7編譯的,那一整塊邏輯程式碼都會被跳過,直接呼叫新的API。

補充閱讀