Swift如何優化效能?

edithfang發表於2015-03-06
Swift在記憶體管理上使用的是自動引用計數(ARC)的一套方法,在ARC中雖然不需要手動地呼叫像是retain,release或者是autorelease這樣的方法來管理引用計數,但是這些方法還是都會被呼叫的——只不過是編譯器在編譯時在合適的地方幫我們加入了而已。其中retain和release都很直接,就是將物件的引用計數加一或者減一。但是autorelease就比較特殊一些,它會將接受該訊息的物件放到一個預先建立的自動釋放池 (auto release pool)中,並在自動釋放池收到drain訊息時將這些物件的引用計數減一,然後將它們從池子中移除(這一過程形象地稱為“抽乾池子”)。



在App中,整個主執行緒其實是跑在一個自動釋放池裡的,並且在每個主Runloop結束時進行drain操作。這是一種必要的延遲釋放的方式,因為我們有時候需要確保在方法內部初始化的生成的物件在被返回後別人還能使用,而不是立即被釋放掉。

在Objective-C中,建立一個自動釋放池的語法很簡單,使用@autoreleasepool就行了。如果你新建一個Objective-C專案,可以看到main.m中就有我們剛才說到的整個專案的autoreleasepool:

int main(int argc, char *argv[]) {  
    @autoreleasepool {  
        int retVal = UIApplicationMain(  
            argc,   
            argv,   
            nil,   
            NSStringFromClass([AppDelegate class]));  
        return retVal;  
    }  
}  
更進一步,其實@autoreleasepool在編譯時會被展開為NSAutoreleasePool,並附帶drain方法的呼叫。

而在Swift專案中,因為有了@UIApplicationMain,我們不再需要main檔案和main函式,所以原來的整個程式的自動釋放池就不存在了。即使我們使用main.swift來作為程式的入口時,也是不需要自己再新增自動釋放池的。

但是在一種情況下我們還是希望自動釋放,那就是在面對在一個方法作用域中要生成大量的autorelease物件的時候。在Swift 1.0時,我們可以寫這樣的程式碼:

func loadBigData() {  
    if let path = NSBundle.mainBundle()  
        .pathForResource("big", ofType: "jpg") {  
        for i in 1...10000 {  
            let data = NSData.dataWithContentsOfFile(  
                path, options: nil, error: nil)  
            NSThread.sleepForTimeInterval(0.5)  
        }          
    }  
}


dataWithContentsOfFile返回的是autorelease的物件,因為我們一直處在迴圈中,因此它們將一直沒有機會被釋放。如果數量太多而且資料太大的時候,很容易因為記憶體不足而崩潰。在Instruments下可以看到記憶體alloc的情況:



這顯然是一幅很不妙的情景。在面對這種情況的時候,正確的處理方法是在其中加入一個自動釋放池,這樣我們就可以在迴圈進行到某個特定的時候施放記憶體,保證不會因為記憶體不足而導致應用崩潰。在Swift中我們也是能使用autoreleasepool的——雖然語法上略有不同。相比於原來在Objective-C中的關鍵字,現在它變成了一個接受閉包的方法:

func autoreleasepool(code: () -> ())  


利用尾隨閉包的寫法,很容易就能在Swift中加入一個類似的自動釋放池了:

func loadBigData() {  
    if let path = NSBundle.mainBundle()  
        .pathForResource("big", ofType: "jpg") {  
        for i in 1...10000 {  
            autoreleasepool {  
                let data = NSData.dataWithContentsOfFile(  
                    path, options: nil, error: nil)  
                NSThread.sleepForTimeInterval(0.5)  
            }  
        }          
    }  
}


這樣改動以後,記憶體分配就沒有什麼憂慮了:



這裡我們每一次迴圈都生成了一個自動釋放池,雖然可以保證記憶體使用達到最小,但是釋放過於頻繁也會帶來潛在的效能憂慮。一個折中的方法是將迴圈分隔開加入自動釋放池,比如每10次迴圈對應一次自動釋放,這樣能減少帶來的效能損失。

其實對於這個特定的例子,我們並不一定需要加入自動釋放。在Swift中更提倡的是用初始化方法而不是用像上面那樣的類方法來生成物件,而且在Swift 1.1中,因為加入了可以返回nil的初始化方法,像上面例子中那樣的工廠方法都已經從API中刪除了。今後我們都應該這樣寫:

let data = NSData(contentsOfFile: path)  


使用初始化方法的話,我們就不需要面臨自動釋放的問題了,每次在超過作用域後,自動記憶體管理都將為我們處理好記憶體相關的事情。
相關閱讀
評論(1)

相關文章