天天看點

将Swift與Objective-C相結合

原文: 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

    UIAppearance

    等Core Data對象的動态特性解析,還有很多其它蘋果架構中我們所喜愛的功能。這個問題很難平衡,即便是蘋果UIKit團隊的員工也要謹慎對待這種危險。
  • 使用不具有二進制相容性的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編寫的項目:

  1. 将想要使用Objective-C++的檔案重命名,從

    <MyClass>.m

    改為

    <MyClass>.mm

  2. 完成,就是這樣,不需要步驟二。

真的非常簡單,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添加),編譯器就會捕捉到這個問題:

将Swift與Objective-C相結合

在使用泛型時,區分符的輸入非常煩人:

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++。

繼續閱讀