天天看點

iOS知識小集 第四期(2015.08.15)

  1. ARC與MRC的性能對比
  2. Bitcode
  3. 在Swift中實作NS_OPTIONS

篇幅超過了預期,大家慢慢看,如有問題還請指正。

ARC與MRC的性能對比

MRC似乎已經是一個上古時代的話題了,不過我還是繞有興緻的把它翻出來。因為,今天我被一個問題問住了:ARC與MRC的性能方面孰優劣。确實,之前沒有對比過。

先來做個測試吧。首先我們需要一個計時輔助函數,我選擇使用mach_absolute_time,計算時間差的函數如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
      
double subtractTimes(uint64_t endTime, uint64_t startTime) {

    uint64_t difference = endTime - startTime;
    static double conversion = 0.0;

    if(conversion == 0.0) {

        mach_timebase_info_data_t info;
        kern_return_t err = mach_timebase_info(&info);                       //Convert the timebaseinto seconds

        if(err == 0)
            conversion = 1e-9 * (double) info.numer / (double) info.denom;
    }

    return conversion * (double)difference;
}

           

然後定義兩個測試類,一個是ARC環境下的,一個是MRC環境下的,分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
      
// Test1.m
+ (void)test {

    uint64_t start,stop;

    start = mach_absolute_time();

    for (int i = 0; i < 1000000; i++) {
        NSArray *array = [[NSArray alloc] init];
    }

    stop = mach_absolute_time();

    double diff = subtractTimes(stop, start);

    NSLog(@"ARC total time in seconds = %f\n", diff);
}

// Test2.m
// 在target->Build Phases->Compile Sources中,添加編譯辨別-fno-objc-arc
+ (void)test {

    uint64_t start,stop;

    start = mach_absolute_time();

    for (int i = 0; i < 1000000; i++) {
        NSArray *array = [[NSArray alloc] init];
        [array release];
    }

    stop = mach_absolute_time();

    double diff = subtractTimes(stop, start);

    NSLog(@"MRC total time in seconds = %f\n", diff);
}

           

多運作幾組測試,然後挑兩組吧來看看,資料如下:

1
2
3
4
5
6
7
      
// A組
ARC total time in seconds = 0.077761
MRC total time in seconds = 0.072469

// B組
ARC total time in seconds = 0.075722
MRC total time in seconds = 0.101671

           

從上面的資料可以看到,ARC與MRC各有快慢的情況。即使上升到統計學的角度,ARC也隻是以輕微的優勢勝出。看來我的測試姿勢不對,并沒有證明哪一方占絕對的優勢。

嗯,那我們再來看看官方文檔是怎麼說的吧。在Transitioning to ARC Release Notes中有這麼一段話:

Is ARC slow?

It depends on what you’re measuring, but generally “no.” The compiler efficiently eliminates many extraneous

retain

/

release

 calls and much effort has been invested in speeding up the Objective-C runtime in general. In particular, the common “return a retain/autoreleased object” pattern is much faster and does not actually put the object into the autorelease pool, when the caller of the method is ARC code.

One issue to be aware of is that the optimizer is not run in common debug configurations, so expect to see a lot more 

retain

/

release

 traffic at 

-O0

 than at 

-Os

.

再來看看别人的資料吧。Steffen Itterheim在Confirmed: Objective-C ARC is slow. Don’t use it! (sarcasm off)一文中給出了大量的測試資料。這篇文章是2013.3.20号發表的。Steffen Itterheim通過他的測試得出一個結論

ARC is generally faster, and ARC can indeed be slower

嗯,有些沖突。不過在文章中,Steffen Itterheim指出大部分情況下,ARC的性能是更好的,這主要得益于一些底層的優化以及autorelease pool的優化,這個從官方文檔也能看到。但在一些情況下,ARC确實是更慢,ARC會發送一些額外的retain/release消息,如一些涉及到臨時變量的地方,看下面這段代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
      
// this is typical MRC code:
{
    id object = [array objectAtIndex:0];
    [object doSomething];
    [object doAnotherThing];
}

// this is what ARC does (and what is considered best practice under MRC):
{
    id object = [array objectAtIndex:0];
    [object retain]; // inserted by ARC
    [object doSomething];
    [object doAnotherThing];
    [object release]; // inserted by ARC
}

           

另外,在帶對象參數的方法中,也有類似的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
      
// this is typical MRC code:
-(void) someMethod:(id)object
{
    [object doSomething];
    [object doAnotherThing];
}

// this is what ARC does (and what is considered best practice under MRC):
-(void) someMethod:(id)object
{
    [object retain]; // inserted by ARC
    [object doSomething];
    [object doAnotherThing];
    [object release]; // inserted by ARC
}

           

這些些額外的retain/release操作也成了降低ARC環境下程式性能的罪魁禍首。但實際上,之是以添加這些額外的retain/release操作,是為了保證代碼運作的正确性。如果隻是在單線程中執行這些操作,可能确實沒必要添加這些額外的操作。但一旦涉及以多線程的操作,問題就來了。如上面的方法中,object完全有可能在doSoming和doAnotherThing方法調用之間被釋放。為了避免這種情況的發生,便在方法開始處添加了[object retain],而在方法結束後,添加了[object release]操作。

如果想了解更多關于ARC與MRC性能的讨論,可以閱讀一下Are there any concrete study of the performance impact of using ARC?與ARC vs. MRC Performance,在此就不過多的摘抄了。

實際上,即便是ARC的性能不如MRC,我們也應該去使用ARC,是以它給我們帶來的好處是不言而喻的。我們不再需要像使用MRC那樣,去過多的關注記憶體問題(雖然記憶體是必須關注的),而将更多的時間放在我們真正關心的事情上。如果真的對性能非常關切的話,可以考慮直接用C或C++。反正我是不會再回到MRC時代了。

參考

  1. Are there any concrete study of the performance impact of using ARC?
  2. ARC vs. MRC Performance
  3. Confirmed: Objective-C ARC is slow. Don’t use it! (sarcasm off)
  4. Transitioning to ARC Release Notes

Bitcode

今天試着用Xcode 7 beta 3在真機(iOS 8.3)上運作一下我們的工程,結果發現工程編譯不過。看了下問題,報的是以下錯誤:

ld: ‘/UsersFramework/SDKs/PolymerPay/Library/mobStat/lib**SDK.a(**ForSDK.o)’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

得到的資訊是我們引入的一個第三方庫不包含bitcode。嗯,不知道bitcode是啥,是以就得先看看這貨是啥了。

Bitcode是什麼?

找東西嘛,最先想到的當然是先看官方文檔了。在App Distribution Guide – App Thinning (iOS, watchOS)一節中,找到了下面這樣一個定義:

Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the store.

說的是bitcode是被編譯程式的一種中間形式的代碼。包含bitcode配置的程式将會在App store上被編譯和連結。bitcode允許蘋果在後期重新優化我們程式的二進制檔案,而不需要我們重新送出一個新的版本到App store上。

嗯,看着挺進階的啊。

繼續看,在What’s New in Xcode-New Features in Xcode 7中,還有一段如下的描述

Bitcode. When you archive for submission to the App Store, Xcode will compile your app into an intermediate representation. The App Store will then compile the bitcode down into the 64 or 32 bit executables as necessary.

當我們送出程式到App store上時,Xcode會将程式編譯為一個中間表現形式(bitcode)。然後App store會再将這個botcode編譯為可執行的64位或32位程式。

再看看這兩段描述都是放在App Thinning(App瘦身)一節中,可以看出其與包的優化有關了。喵大(@onevcat)在其部落格開發者所需要知道的 iOS 9 SDK 新特性中也描述了iOS 9中蘋果在App瘦身中所做的一些改進,大家可以轉場到那去研讀一下。

Bitcode配置

在上面的錯誤提示中,提到了如何處理我們遇到的問題:

You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

要麼讓第三方庫支援,要麼關閉target的bitcode選項。

實際上在Xcode 7中,我們建立一個iOS程式時,bitcode選項預設是設定為YES的。我們可以在”Build Settings”->”Enable Bitcode”選項中看到這個設定。

不過,我們現在需要考慮的是三個平台:iOS,Mac OS,watchOS。

  • 對應iOS,bitcode是可選的。
  • 對于watchOS,bitcode是必須的。
  • Mac OS不支援bitcode。

如果我們開啟了bitcode,在送出包時,下面這個界面也會有個bitcode選項:

iOS知識小集 第四期(2015.08.15)

盜圖,我的應用沒辦法在這個界面顯示bitcode,因為依賴于第三方的庫,而這個庫不支援bitcode,暫時隻能設定ENABLE_BITCODE為NO。

是以,如果我們的工程需要支援bitcode,則必要要求所有的引入的第三方庫都支援bitcode。我就隻能等着公司那些大哥大姐們啥時候提供一個新包給我們了。

題外話

如上面所說,bitcode是一種中間代碼。LLVM官方文檔有介紹這種檔案的格式,有興趣的可以移步LLVM Bitcode File Format。

參考

  1. App Distribution Guide – App Thinning (iOS, watchOS)
  2. What’s New in Xcode-New Features in Xcode 7
  3. 開發者所需要知道的 iOS 9 SDK 新特性
  4. LLVM Bitcode File Format

在Swift中實作NS_OPTIONS

從Xcode 4.5以後,我們在Objective-C中使用NS_ENUM和NS_OPTIONS來定義一個枚舉,以替代C語言枚舉的定義方式。其中NS_ENUM用于定義普通的枚舉,NS_OPTIONS用于定義選項類型的枚舉。

而到了Swift中,枚舉增加了更多的特性。它可以包含原始類型(不再局限于整型)以及相關值。正是由于這些原因,枚舉在Swift中得到了更廣泛的應用。在Foundation中,Objective-C中的NS_ENUM類型的枚舉,都會自動轉換成Swift中enum,并且更加精煉。以Collection View的滾動方向為例,在Objective-C中,其定義如下:

1
2
3
4
      
typedef NS_ENUM(NSInteger, UICollectionViewScrollDirection) {
  UICollectionViewScrollDirectionVertical,
  UICollectionViewScrollDirectionHorizontal
};

           

而在Swift中,其定義如下:

1
2
3
4
      
enum UICollectionViewScrollDirection : Int {
  case Vertical
  case Horizontal
}

           

精練多了吧,看着舒服多了,還能少碼兩個字。我們自己定義枚舉時,也應該采用這種方式。

不過對于Objective-C中NS_OPTIONS類型的枚舉,Swift中的實作似乎就沒有那麼美好了。

我們再來對比一下UICollectionViewScrollPosition的定義吧,在Objective-C中,其定義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
      
typedef NS_OPTIONS(NSUInteger, UICollectionViewScrollPosition) {
    UICollectionViewScrollPositionNone                 = 0,

    // The vertical positions are mutually exclusive to each other, but are bitwise or-able with the horizontal scroll positions.
    // Combining positions from the same grouping (horizontal or vertical) will result in an NSInvalidArgumentException.
    UICollectionViewScrollPositionTop                  = 1 << 0,
    UICollectionViewScrollPositionCenteredVertically   = 1 << 1,
    UICollectionViewScrollPositionBottom               = 1 << 2,

    // Likewise, the horizontal positions are mutually exclusive to each other.
    UICollectionViewScrollPositionLeft                 = 1 << 3,
    UICollectionViewScrollPositionCenteredHorizontally = 1 << 4,
    UICollectionViewScrollPositionRight                = 1 << 5
};

           

而在Swift 2.0中,其定義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
      
struct UICollectionViewScrollPosition : OptionSetType {
    init(rawValue: UInt)

    static var None: UICollectionViewScrollPosition { get }

    // The vertical positions are mutually exclusive to each other, but are bitwise or-able with the horizontal scroll positions.
    // Combining positions from the same grouping (horizontal or vertical) will result in an NSInvalidArgumentException.
    static var Top: UICollectionViewScrollPosition { get }
    static var CenteredVertically: UICollectionViewScrollPosition { get }
    static var Bottom: UICollectionViewScrollPosition { get }

    // Likewise, the horizontal positions are mutually exclusive to each other.
    static var Left: UICollectionViewScrollPosition { get }
    static var CenteredHorizontally: UICollectionViewScrollPosition { get }
    static var Right: UICollectionViewScrollPosition { get }
}

           

額,光看代碼,不看實作,這也是化簡為繁的節奏啊。

為什麼要這樣做呢?Mattt給了我們如下解釋:

Well, the same integer bitmasking tricks in C don’t work for enumerated types in Swift. An 

enum

 represents a type with a closed set of valid options, without a built-in mechanism for representing a conjunction of options for that type. An 

enum

 could, ostensibly, define a case for all possible combinations of values, but for 

n > 3

, the combinatorics make this approach untenable.

意思是Swift不支援C語言中枚舉值的整型掩碼操作的技巧。在Swift中,一個枚舉可以表示一組有效選項的集合,但卻沒有辦法支援這些選項的組合操作(“&”、”|”等)。理論上,一個枚舉可以定義選項值的任意組合值,但對于n > 3這種操作,卻無法有效的支援。

為了支援類NS_OPTIONS的枚舉,Swift 2.0中定義了OptionSetType協定【在Swift 1.2中是使用RawOptionSetType,相比較而言已經改進了不少】,它的聲明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
      
/// Supplies convenient conformance to `SetAlgebraType` for any type
/// whose `RawValue` is a `BitwiseOperationsType`.  For example:
///
///     struct PackagingOptions : OptionSetType {
///       let rawValue: Int
///       init(rawValue: Int) { self.rawValue = rawValue }
///     
///       static let Box = PackagingOptions(rawValue: 1)
///       static let Carton = PackagingOptions(rawValue: 2)
///       static let Bag = PackagingOptions(rawValue: 4)
///       static let Satchel = PackagingOptions(rawValue: 8)
///       static let BoxOrBag: PackagingOptions = [Box, Bag]
///       static let BoxOrCartonOrBag: PackagingOptions = [Box, Carton, Bag]
///     }
///
/// In the example above, `PackagingOptions.Element` is the same type
/// as `PackagingOptions`, and instance `a` subsumes instance `b` if
/// and only if `a.rawValue & b.rawValue == b.rawValue`.
protocol OptionSetType : SetAlgebraType, RawRepresentable {

    /// An `OptionSet`'s `Element` type is normally `Self`.
    typealias Element = Self

    /// Convert from a value of `RawValue`, succeeding unconditionally.
    init(rawValue: Self.RawValue)
}

           

從字面上來了解,OptionSetType是選項集合類型,它定義了一些基本操作,包括集合操作(union, intersect, exclusiveOr)、成員管理(contains, insert, remove)、位操作(unionInPlace, intersectInPlace, exclusiveOrInPlace)以及其它的一些基本操作。

作為示例,我們來定義一個表示方向的選項集合,通常我們是定義一個實作OptionSetType協定的結構體,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
      
struct Directions: OptionSetType {

    var rawValue:Int
    init(rawValue: Int) {
        self.rawValue = rawValue
    }

    static let Up: Directions = Directions(rawValue: 1 << 0)
    static let Down: Directions = Directions(rawValue: 1 << 1)
    static let Left: Directions = Directions(rawValue: 1 << 2)
    static let Right: Directions = Directions(rawValue: 1 << 3)
}

           

所需要做的基本上就是這些。然後我們就可以建立Directions的執行個體了,如下所示:

1
2
3
4
      
let direction: Directions = Directions.Left
if direction == Directions.Left {
    // ...
}

           

如果想同時支援兩個方向,則可以如上處理:

1
2
3
4
      
let leftUp: Directions = [Directions.Left, Directions.Up]
if leftUp.contains(Directions.Left) && leftUp.contains(Directions.Up) {
    // ...
}

           

如果leftUp同時包含Directions.Left和Directions.Up,則傳回true。

這裡還有另外一種方法來達到這個目的,就是我們在Directions結構體中直接聲明聲明Left和Up的靜态常量,如下所示:

1
2
3
4
5
6
7
      
struct Directions: OptionSetType {

    // ...
    static let LeftUp: Directions = [Directions.Left, Directions.Up]
    static let RightUp: Directions = [Directions.Right, Directions.Up]
    // ...
}

           

這樣,我們就可以以如下方式來執行上面的操作:

1
2
3
      
if leftUp == Directions.LeftUp {
    // ...
}

           

當然,如果單一選項較多,而要去組合所有的情況,這種方法就顯示笨拙了,這種情況下還是推薦使用contains方法。

總體來說,Swift中的對選項的支援沒有Objective-C中的NS_OPTIONS來得簡潔友善。而且在Swift 1.2的時候,我們還是可以使用”&“和”|”操作符的。下面這段代碼在Swift 1.2上是OK的:

1
2
3
      
UIView.animateWithDuration(0.3, delay: 1.0, options: UIViewAnimationOptions.CurveEaseIn | UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
    // ...
}, completion: nil)

           

但到了Swift 2.0時,OptionSetType已經不再支援”&“和”|”操作了,是以,上面這段代碼需要修改成:

1
2
3
      
UIView.animateWithDuration(0.3, delay: 1.0, options: [UIViewAnimationOptions.CurveEaseIn, UIViewAnimationOptions.CurveEaseInOut], animations: { () -> Void in
        // ...
}, completion: nil)

           

不過,慢慢習慣就好。

參考

  1. RawOptionSetType
  2. Exploring Swift 2.0 OptionSetTypes
  3. Notes from WWDC 2015: The Enumerated Delights of Swift 2.0 Option Sets​
  4. 《100個Swift開發必備Tip》— Tip 66. Options

零碎

靜态分析中”Potential null dereference”的處理

我們在寫一個方法時,如果希望在方法執行出錯時,擷取一個NSError對象,我們通常會像下面這樣來定義我們的方法

1
2
3
4
5
6
7
8
9
      
+ (NSString )checkStringLength:(NSString *)str error:(NSError **)error {

  if (str.length <= 0) {
          *error = [NSError errorWithDomain:@"ErrorDomain" code:-1 userInfo:nil];
      return nil;
  }

  return str;
}

           

這段代碼看着沒啥問題,至少在文法上是OK的,是以在編譯時,編譯器并不會報任何警告。

如果我們用以下方式去調用的話,也是一切正常的:

1
2
      
NSError *error = nil;
[Test checkStringLength:@"" error:&error];

           

不過我們如果就靜态分析器來分析一下,發現會在”*error = …“這行代碼處報如下的警告:

Potential null dereference. According to coding standards in ‘Creating and Returning NSError Objects’ the parameter may be null

這句話告訴我們的是這裡可能存在空引用。實際上,如果我們像下面這樣調用方法的話,程式是會崩潰的:

[Test checkStringLength:@"" error:NULL];

           

因為此時在方法中,error實際上是NULL,*error這貨啥也不是,對它指派肯定就出錯了。

這裡正确的姿式是在使用error之前,先判斷它是否為NULL,完整的代碼如下:

1
2
3
4
5
6
7
8
9
10
11
      
+ (NSString )checkStringLength:(NSString *)str error:(NSError **)error {

    if (str.length <= 0) {

        if (error != NULL) {
            *error = [NSError errorWithDomain:@"ErrorDomain" code:-1 userInfo:nil];
        }
        return nil;
    }
    return str;
}

           

實際上,對此這種方式的傳值,我們始終需要去做非空判斷。

Charles支援iOS模拟器

咬咬牙花了50刀買了一個Charles的License。

今天臨時需要在模拟器上跑工程,想抓一下資料包,看一下請求Header裡面的資訊。工程跑起來時,發現Charles沒有抓取到資料。嗯,本着有問題先問stackoverflow的原則,跑到上面搜了一下。找到了這個貼子:How to use Charles Proxy on the Xcode 6 (iOS 8) Simulator?。不過我的處理沒有他這麼麻煩,基本上兩步搞定了:

1.在Charles的菜單中選擇Help > SSL Proxying > Install Charles Root Certificate in iOS Simulators,直接點選就行。這時候會彈出一個提示框,點選OK就行。

2.如果這時候還不能抓取資料,就重新開機模拟器。

這樣就OK了。在Keychain裡面,真機和模拟器的證書是同一個。

至于stackoverflow裡面提到的在3.9.3版本上還需要覆寫一個腳本檔案,這個沒有嘗試過,哈哈,我的是最新的3.10.2。

還有個需要注意的是,在抓取模拟器資料時,如果關閉Charles,那麼模拟器将無法再請求到網絡資料。這時需要重新開啟Charles,或者是重新開機模拟器。另外如果重置了模拟器的設定(Reset Content and Settings…),Charles也抓取不到模拟器的資料,需要重新來過。

參考

  1. How to use Charles Proxy on the Xcode 6 (iOS 8) Simulator?