原文: Swifty Objective-C
作者: Peter Steinberger / Michael Ochs / Matej Bukovinski,感謝作者對本文的翻譯授權
譯者: 孫薇
審校: 唐小引(@唐門教主),歡迎技術投稿、約稿,給文章糾錯,請發送郵件tangxy#csdn.net(請将#更換為@)。
Objective-C起源于20世紀80年代初,盡管多年來這種語言有了長足的發展,卻仍不敵Swift這樣真正的現代化語言。随着Swift 3.0即将上線,使用Swift來編寫新的應用會更加智能化。然而在PSPDFKit,我們仍堅守在Objective-C的世界裡,我們建立、釋出二進制架構以渲染/編輯PDF檔案。想要正确擷取所有的PDF細節是很複雜的,除了核心的PDF功能之外,我們還提供了大量可在典型用例中運用的UI類,進而産生了大約60萬行的代碼庫,包含了UI和封裝模型的混合代碼——shared C++和Objective-C++都有,header部分完全是現代化的Objective-C,使用泛型和nullability注釋,以確定在Swift中運作良好。
盡管目前我們還處于Objective-C的世界中,但這種情況也并非全然糟糕的:通過一些精巧的設計,在類似我們這樣的代碼庫中也甚至可以享用Swift的諸多好處。下面我們列出了一些将“新舊世界”結合起來的方法。
為什麼不單純地使用Swift呢?
我們先來談談這個十分明顯卻無人肯談的問題。Swift是很棒的語言,有很多原因促使我們使用它;不過在很多場景和需求下,選擇Objective-C則更為明智。具體選擇哪種語言,要取決于應用及用例本身、你的團隊還有項目的範圍及本質。在這裡蘋果給了我們選擇,這真是太棒了。
- Swift的發展速度快得不可思議。蘋果的開放過程簡直令人驚異,特别要考慮到這家公司謹言慎行的本質。盡管将Swift最初釋出的版本稱為1.0版尚且有些草率,不過很快它就發展成了一種快速、安全、可以編寫出優雅代碼的語言。對于早期的采用者來說,還有很多甚至稱得上嚴重的bug和問題需要解決。對于較小的項目或者典型應用來說,Swift可能運作良好,但大型項目可能會因為編譯時間、優化問題或者僅僅缺乏資源來停止開發并花上數周更新資料庫到Swift 3——這項任務可能會帶來極大的破壞性——而推遲采用Swift。
- 目前的Swift在很多方面與C++很相似,都屬于非常靜态的類型,在動态消息發送和運作環境方面比不上Objective-C。在過去,這一點雖然導緻了很嚴重的問題出現,比如優化問題或者不應使用的monkey-patching代碼,但也帶來了許多優雅的解決方案,比如包括
、NSManagedObject
、NSUndoManager
等Core Data對象的動态特性解析,還有很多其它蘋果架構中我們所喜愛的功能。這個問題很難平衡,即便是蘋果UIKit團隊的員工也要謹慎對待這種危險。UIAppearance
- 使用不具有二進制相容性的Swift語言意味着我們必須對使用者關閉一些技術參數,同時他們在選擇Xcode時也會有局限——如果我們的SDK仍舊使用Xcode 7.3.0編譯,他們也許就不能更新到Xcode 7.3.1。每個極小的編譯器版本更改都可能會導緻代碼與其他版本不能相容,而我們并不想讓使用者煩惱這種額外的技術複雜性。我們明白自己是極端型案例,對大多項目來說并不會有太大問題。我們也堅信,推遲采用并等待穩定的Swift ABI是件好事,盡管短期内不夠友善,但長期來講我們所使用的語言更完善。同時,我們的使用者也很在意資料包的大小,可能會介意采用Swift所造成的每個架構6MB的額外負擔。由于我們一直支援最近兩版的iOS,也就是說至少在最近兩年内我們都無法改用Swift。
在編寫測試和樣例代碼時,我們越來越多地使用Swift,也非常喜歡它。但同時,我們也擔心Xcode 8的轉變與額外的複雜性會對團隊産生負擔。由于ABI還在變化,我們無法在主要的SDK中使用Swift。是以我們決定采用Objective-C++在恰當的地方對純Objective-C進行補充。
這種做法看似非常複雜,可能會讓很多人産生擔心:在代碼中加入C++這種可能非常複雜的語言——難以學習甚至難以駕馭,也許會花費很多的時間和精力,但實際上并非如此。與其将Objective-C++當作Objective-C加C++,不如把它當成Objective-C的一個小語種。我們在Objective-C類中僅使用了極少量的C++,以借助C++的便利、安全性與性能方面的優勢。這與實作完全成熟的C++不同,在以Objective-C為主的代碼庫中嘗試使用一個小的子集是非常簡單的,即便對于沒有C++經驗的開發者來說也是如此。
Objective-C++入門
我們先來看一下在項目中使用Objective-C++所需的步驟,假設已有以Objective-C編寫的項目:
- 将想要使用Objective-C++的檔案重命名,從
改為<MyClass>.m
;<MyClass>.mm
- 完成,就是這樣,不需要步驟二。
真的非常簡單,Objective-C與C++具有高度的協作性,是以無需安裝任何内容,也完全不用修改設定。當然,也并非所有的C代碼都是有效的C++代碼,有時可能需要添加一些額外的轉換,不過大部分情況都是沒問題的。Xcode 7并不支援Objective-C++中的子產品,是以必須使用較舊的
#import
文法,而不是新的
@import
。
現在我們知道,在應用中支援Objective-C++實際上非常簡單,來看一下能用它做些什麼。下面是我們最喜歡的一些功能:
auto
看下這段代碼:
NSArray *files = [NSFileManager.defaultManager contentsOfDirectoryAtURL:samplesURL includingPropertiesForKeys:nil options: error:NULL];
PSPDFDocument *document = [[PSPDFDocument alloc] initWithBaseURL:samplesURL files:files];
這實際上是我們測試中的一個bug,在打算将檔案名清單作為字元串時,其中有檔案包含
NSURL
對象。最終由于相關檔案被自動過濾掉,測試過了,有一陣子沒人注意日志記錄。如果我們使用Objective-C的新泛型功能(蘋果專為Swift 2添加),編譯器就會捕捉到這個問題:
在使用泛型時,區分符的輸入非常煩人:
NSDictionary<NSNumber*, NSArray<PSPDFAnnotation*>*> *allAnnotationsDict = [document allAnnotationsOfType:PSPDFAnnotationTypeAll];
現在一下子就能解決了,我們可以簡化它,同時保持C++正确的模闆參數:
好多了,
allAnnotationsDict
是什麼仍然很明顯。在編譯時,
auto
功能可以像上面這樣自動完成,無需配置運作環境。Swift編譯器團隊的Joe Groff指出:目前在标準C和ObjC中,top-of-tree clang支援
__auto_type
類型推論了,是以我們終于可以在無需C++編譯開銷的情況使用它。
内聯塊
考慮一下使用内聯塊處理注釋的情況,由于需要三個參數,聲明的長度幾乎讓人難以忍受。通常我們會将它改成輔助函數,不過由于無法捕獲變量,結果可能會讓情況更加複雜。
void (^processAnnotation)(PSPDFAnnotation *annotation, BOOL addToIndex, NSUInteger objectID) =
^(PSPDFAnnotation *annotation, BOOL addToIndex, NSUInteger objectID) {
// code
};
這個聲明還有個問題,就是非常冗長,每個參數類型都要寫兩遍,開發者通常都很厭惡冗長,是以我們來做些清理。
auto
再次成了救星:
auto processAnnotation = ^(PSPDFAnnotation *annotation, BOOL addToIndex, NSUInteger objectID) {
// code
};
let
Swift的優點之一在于:在聲明變量時,
let
是使用最多也最友善的辦法,會自動産生
const
。同時在C語言中也有
const
,隻不過非常醜陋:
NSString *const password = @"test123";
有了
auto
,可以寫成可讀性更高的樣子:
const auto password = @"test123";
甚至可以更瘋狂——使用一個宏:
#define let auto const
let password = @"test123";
vector
在Swift中,我們可以将任何資料類型放在數組中:
let anglePoints = [CGPoint(x: , y: ), CGPoint(x: , y: ), CGPoint(x: , y: )]
在Objective-C中,
NSArray
隻能包含對象,不但更為複雜,同時由于封裝的問題,對于基本類型的處理速度也更慢。當然,我們可以使用C數組,但會使得添加移除元素或另存數組之類的通用操作更為複雜,可能需要手動執行記憶體管理與調用
malloc()
。有了Objective-C++,我們可以簡單地使用
std::vector
:
auto points = std::vector<CGPoint>{{, }, {.x=, .y=}, {, }};
無論顯式
struct
字段命名,還是較短的隐式版本
{0, 0}
都是可用的,由于vector
<CGPoint>
已知想要的資料類型,無需再編寫
(CGPoint)
轉換。此外對C++容器
const
之後,它們也會自動成為不可變量。
vector <-> NSArray
有時候會出現需要将
vector
轉化為
NSArray
的情況,反過來也是一樣。這種操作非常簡單,但如果使用helper會更好。
template <typename T>
static inline NSArray *PSPDFArrayWithVector(const std::vector<T> &vector,
id(^block)(const T &value)) {
NSMutableArray *result = [NSMutableArray arrayWithCapacity:vector.size()];
for (const T &value : vector) {
[result addObject:block(value)];
}
return result;
}
template <typename T>
static inline std::vector<T> PSPDFVectorWithElements(id<NSFastEnumeration> array,
T(^block)(id value)) {
std::vector<T> result;
for (id value in array) {
result.push_back(block(value));
}
return result;
}
運算符重載
大家是否有時候需要計算
CGRect
、
CGSize
或
Core Graphics
的其他幾何類型呢?它們都是
struct
,雖然好處很多,但計算時非常煩人,這裡再次出現了備援代碼:
在Swift中,定義運算符非常簡單,進而使得這些操作也很簡單,但在Objective-C++中我們也能這樣做:
CGSize operator/(const CGSize &lhs, CGFloat f) {
return (CGSize){lhs.width / f, lhs.height / f};
}
const CGSize zoomSize = self.bounds.size / zoomScale;
鎖定(Locks)
在建構線程安全API時,需要鎖定。在标準Objective-C中,可以像下面這樣做:
@interface PSPDFDocumentParser () {
NSLock *_parserLock;
}
@end
@implementation PSPDFDocumentParser
- (instancetype)initWithDocumentProvider:(PSPDFDocumentProvider *)documentProvider {
if ((self = [super init])) {
_parserLock = [NSLock new];
}
return self;
}
- (void)parse {
[_parserLock lock];
// Do stuff that needs locking
[_parserLock unlock];
}
@end
代碼很多,但隻描述了一個代碼應當執行的狀态。在Objective-C++中,我們可以采用更簡單的辦法:
@interface PSPDFDocumentParser () {
std::mutex _parserLock;
}
@end
@implementation PSPDFDocumentParser
- (void)parse {
std::lock_guard<std::mutex> parserGuard(_parserLock);
// Do stuff that needs locking
}
@end
在超出範圍後,C++會自動鎖定,在C++中,到處都是資源配置設定即初始化(RAII)模式,它也确實很好用,允許我們通過傳回語句來執行需要内聯鎖定的操作,因為鎖定隻會在傳回後自動解鎖。
如果我們隻需要鎖定某個method的很小一部分,就可以簡單地建立一個較小的範圍來執行:
- (void)parse {
// Do stuff without locking
{
std::lock_guard<std::mutex> parserGuard(_parserLock);
// Do stuff that needs locking
}
// Do stuff without locking
}
如果需要遞歸鎖,可以使用
std::recursive_mutex
來代替
std::mutex
。
可選方案:有一個簡單的純Objective-C解決方案,生成一個method,在鎖定時執行一個塊參數,比如
[NSManagedObjectContext performBlock:]
。
模闆
有時候模闆在避免重複代碼方面非常有效,試想一下負責比較類似
CGFloat
或
NSInteger
的helper,我們随時可以将其封裝并調用
compare:
,但開銷很大。更好的辦法是使用模闆函數:
template <typename T>
inline NSComparisonResult PSPDFCompare(const T value1, const T value2) {
if (value1 < value2) return (NSComparisonResult)NSOrderedAscending;
else if (value1 > value2) return (NSComparisonResult)NSOrderedDescending;
else return (NSComparisonResult)NSOrderedSame;
}
另一個很有用的helper是條件轉換——檢視某個類是否是正确的類型。
template<typename T>
static inline T *PSPDFDynamicCast(__unsafe_unretained id obj) {
if ([obj isKindOfClass:[T class]]) {
return obj;
}
return nil;
}
// Usage:
auto objectOrNil = PSPDFDynamicCast<PSPDFNavigationController>(self.navigationController);
在if中的變量聲明
在Swift中,典型用法就是在if-else塊區中聲明變量:
if let nav = controller.navigationController {
nav.pushViewController(myViewController, animated: true)
} else {
//show an alert or something else
}
在Objective-C++也可以采用類似的做法:
if (const auto nav = controller.navigationController) {
//...
}
STL算法
在标準的模闆庫中有很多有用的算法,這裡不列舉代碼片段,請參考Sean Parent的演講《C++ Seasoning》,對拓展思維很有好處。
問題和缺點
這些簡單的調整有什麼缺點呢?我們不想說謊——缺點确實有一些,但我們認為到目前為止使用它們的優勢更大。
編譯時間
将檔案擴充名從
.m
修改為
.mm
之後,clang将開始從C++的角度評估檔案,而在自動轉換方面C++更為嚴格。是以有時在使用中會收到一些警告,特别當代碼中包含類似
MAX()
這樣的宏時。在
std::max()
的情況下,可以通過顯式轉換或者使用C++函數的替代來解決這些問題。如果出現問題,或者有時Objective-C在類型上太松懈,就必須自行确定該如何處理。
編譯
.mm
檔案比标準的
.m
檔案花費的時間更長一些,不過憑我們的經驗來看,這點代價是值得的。如果你在使用一個大型代碼庫的話,額外時間累積起來可能會很多,但使用一些額外的編譯緩存能夠抵消很多消耗掉的時間。大量使用模闆或者用到模闆的庫會産生更大的影響。
工具
另一個風險在于:很多人廣泛使用Objective-C++,是以很可能會遇到編譯錯誤或邊緣情況。目前我們隻遇到過一個Clang Analyzer崩潰的問題,不過在純Objective-C代碼中我們也曾設法重制過這個問題。
在header中避免使用C++
如果非要添加的話,将它或者放在單獨的
.hpp header
中,或者放在
#if __cplusplus
後面;否則很快你就必須将整個項目轉換為
.mm
格式,而且
header
對Swift來說不可通路。
意外副本
C++喜歡複制内容,看看這段代碼:
@property (nonatomic) std::vector<int> values;
// later on
self.values.emplace_back();
這段代碼沒有任何作用,屬性會傳回一個修改後的向量副本,調用後自毀。有很多辦法來解決這個問題,使用共享指針就是辦法之一:
@property (nonatomic) std::shared_ptr<std::vector<int>> values;
// later on
self.values.get().emplace_back();
C++11加入了
unique_ptr
、
shared_ptr
和
weak_ptr
,它們在很多方面與ARC類似,隻是速度更快也更有确定性,因為其中沒有自動釋放池。共享指針很好用,在大多情況下調用
new
或者
delete
指令屬于糟糕的設計,可以使用智能指針來替代。
Objective-C的功能
我們使用了大量并非人盡皆知的Objective-C功能,還有大量可用的輔助函數讓代碼更具有可讀性,尤其是在處理集合時。
NS_NOESCAPE
在Swift中,@noescape聲明允許編譯器在block内優化代碼。盡管我們沒有NS_NOESCAPE,也可以使用下面的辦法:
// Equivalent to Swift's @noescape
#define PSPDF_NOESCAPE __attribute__((noescape))
當然我們送出了rdar://25737301,另有一個Swift proposal建議将其添加到Objective-C中,希望很快能看到這樣的變化。
點文法
這是一個有争議的話題,我們在任何沒有負面作用的方法中使用點文法——即便沒有聲明為屬性:
// Typical
[UIApplication sharedApplication].keyWindow
// Shorter
UIApplication.sharedApplication.keyWindow
在iOS 7的SDK中,蘋果将很多應當是屬性的方法都轉換成了屬性。功能上并無差別,隻是現在能更好地執行自動補全了。這裡的缺點在于,Xcode無法自動補全點文法調用的方法。
map, filter, flatMap
類似
NSArray
和
NSSet
這樣的資料結構缺少高階函數。一些有用的方法長度誇張,使用起來也很不友善。
看下這段從頁面通路中收集選擇注釋的代碼:
- (NSArray<PSPDFAnnotation *> *)selectedAnnotations {
NSMutableArray<PSPDFAnnotation *> *selectedAnnotations = [NSMutableArray array];
for (PSPDFPageView *visiblePageView in self.visiblePageViews) {
if (visiblePageView.selectedAnnotations.count > ) {
[selectedAnnotations addObjectsFromArray:visiblePageView.selectedAnnotations];
}
}
return [selectedAnnotations copy];
}
使用我們的
flatMap
助手來編輯相同的代碼:
- (NSArray<PSPDFAnnotation *> *)selectedAnnotations {
return [self.visiblePageViews pspdf_flatMap:^NSArray<PSPDFAnnotation *> *(PSPDFPageView *pageView) {
return pageView.selectedAnnotations;
}];
}
整個helper非常簡單,還有不同的變體可以傳回一個進行更好連結的block,我們選擇了更為Objective-C風格的API,在數組為空的情況下不會崩潰:
- (NSArray *(^)(NSArray * _Nullable (^)(__kindof id obj)))pspdf_flatMapBlock {
return ^(NSArray *(^block)(id obj)) {
NSMutableArray *result = [NSMutableArray new];
for (id obj in self) {
NSArray * _Nullable array = block(obj);
[result pspdf_addObjectsFromArray:array];
}
return [result copy];
};
}
- (NSArray *)pspdf_flatMap:(PSPDF_NOESCAPE NSArray * _Nullable (^)(__kindof id obj))block {
return self.pspdf_flatMapBlock(block);
}
我們有類似的方法可用于
filter
或
map
,以及一系列類似
-[NSArray pspdf_mutatedArrayUsingBlock:]
的helper可封裝大量每個人都寫了數百次的樣闆代碼。盡管我們的helper目前還沒有開源,但有不少有用的開源項目。BlocksKit在上述實作方面表現十分優秀。
結論
在PSPDFKit中,我們平時會使用本文中提到的方法,并且确信這些方法不但會使我們的代碼可讀性更高,同時也增加了代碼庫的安全性,另外,由于無需再重複編寫相同的樣闆代碼(在Objective-C開發中太過常見的一些代碼),其中的很多方法也加快了開發速度。有很多其他的應用與架構也使用Objective-C++,Realm Cocoa、Paper by FiftyThree、RxPromise、Dropbox Djinni、Facebook的ComponentKit還有Pop——甚至很多蘋果的架構,比如Core Graphics、WebKit/WKWebView甚至Objective-C runtime都有運用到Objective-C++。