用Swift做了一個登入介面。背景是一個全螢幕的UIImageView
,用定時器來定時更換圖片,點選登入/註冊按鈕會出現UITextField
。為了使用方便簡單,比如使用者退出登入時、使用者令牌超期等情況,直接彈出登入介面——我把這個登入ViewController做成了單例模式。
本文由Swift語言做示例,由於Objective-C和Swift均使用ARC方式管理記憶體,所以優化思路和方式完全相同,只是相關方法的使用稍有不同。
對於單例模式想必各位開發者並不陌生,優點是有的,缺點也是非常明顯的,在整個應用的生命週期中不會被銷燬重生。這就造成了一個問題,資源浪費——顯然使用者不會經常使用登入檢視控制器的。那這麼說來是不是就沒有辦法了呢,當然不是,經過我的除錯,顯著減少了單例模式的記憶體消耗。
問題嚴重性
先看看不經過優化,問題有多嚴重。
五張圖,居然吃掉了26MB+的記憶體!你會好奇我放了什麼圖,其實就是五張解析度一般普通圖片而已,在真機除錯時,這個問題並不明顯,然而在模擬器上問題會被放大。這個原因目前不明, 有機會要了解一下。
其實就是幾百K的jpg圖片。
但如果是一張圖片幾兆呢,可以壓縮,那如果圖片特別多呢?更何況這是執行在單例模式下的檢視控制器,所以必須要做效能優化。
按照本文所述的幾種方式進行優化,能夠保證單例模式檢視控制器不展現在當前螢幕上時,保持較低記憶體。
分析記憶體大戶
擒賊先擒王,記憶體消耗小的物件我們可以不管他,抓住重點,釋放掉記憶體大戶就是我們優化的主要目標。
開始執行應用,在Xcode左側Navigator中找到Debug Navigator,我們會看到幾條綿延的圖表,找到memory雙擊,選擇Profile in Instruments,點選Transfer,我們會進入到一個Instruments的記憶體除錯例項中,螢幕上部的柱狀圖表會顯示該應用當前的在堆中佔用記憶體的大小。下部分的列表顯示了具體是哪些東西吃掉了多少記憶體。
點選下部列表的Persistent Bytes表頭,來以該列作為索引由大到小排序。表格前三行分別是應用總體記憶體消耗量,我們不用管,直接看第四行開始,那就是我們需要重點優化的專案。目前還沒進入消耗記憶體最大的ViewController,所以我們看到的都是些小兵小卒。
現在present進我們的記憶體大戶ViewController,一下子就會看到幾個兆級的記憶體消耗,馬上看到排第一的就是ImageIO_jpeg_Data,點選這一專案Category中圓形按鈕,我們會看到記憶體的具體去向,我們共有五張圖,和這裡五個專案一一對應,第二張圖解析度最大,所以消耗記憶體最大,達到了16MB。所以現在我們就找到了對應該優化的專案了。
回收檢視
在適當的時候去呼叫self.view = nil
,如果沒有其他指標指向self.view
,那麼這塊記憶體區域就會被釋放。
『適當的時候』對於單例模式檢視控制器來說,就是該VC不在螢幕上的時候。熟悉檢視控制器生命週期的同學們應該會馬上想到在viewDidDisappear
方法中新增相關方法,但請注意,這樣會導致dismiss的動畫失效,因為沒等動畫開始,就過早地將相關view釋放了。所以,這裡我們包裝了dismissViewControllerAnimated
方法,在該方法的completion
閉包中新增移除檢視的方法,程式碼如下:
1 2 3 4 5 6 7 8 9 |
func dismiss() { dismissViewControllerAnimated(true) { for v in self.view.subviews { v.removeFromSuperview() } self.view = nil self.backgroundArray = [] } } |
別忘了把記憶體大戶,背景圖的陣列清空。
現在開啟Instrument檢視情況,並沒有發現什麼變化。什麼原因呢,我們接著看。
載入圖片的正確姿勢
我們在背景圖陣列的初始化的地方,用了最常見的UIImage(named:)
構造方法來建立。UIImage
還有其他初始化方法UIImage.init(contentsOfFile:)
、UIImage(data:)
,殊不知用不同的方式來建立影像,會產生iOS對記憶體管理的極大區別。
在蘋果提供的文件中,對這一問題解釋的很清楚了:
If you have an image file that will only be displayed once and wish to ensure that it does not get added to the system’s cache, you should instead create your image using
imageWithContentsOfFile:
. This will keep your single-use image out of the system image cache, potentially improving the memory use characteristics of your app.
通過UIImage(named:)
讀到的圖片,直接進入快取區,並且這塊快取區的資料,並不會遵循ARC記憶體管理方式,也就是說即使目前應用沒有任何擁有該記憶體區域的物件了,這塊記憶體區域也不會被釋放,只有當應用週期結束(應用退出),該記憶體區域才會被釋放。
所以說,載入圖片的正確姿勢一目瞭然,當載入需要重複使用的,體積比較小的圖片,比如系統UI元素,使用者頭像等等,我們可以使用UIImage(named:)
來初始化UIImage
。如果像我們本文的需求呢,登入介面並不經常使用,不需要的時候直接從記憶體移除,就應該利用UIImage.init(contentsOfFile:)
來初始化UIImage
。
關於UIImage(data:)
,是從NSData
資料中載入影像,遵循ARC記憶體管理方式。
善後
我們在dismiss方法的completion
閉包中設定了self.view = nil
,這會導致再次進入該檢視控制器的時候,會從loadView
開始依次呼叫檢視生命週期方法。
所以我們應該在viewDidLoad
中逐個正常完成UI物件佈局。
另外,特別要注意NSTimer
這類物件的生命週期,該停掉的時候(比如在viewWillDisappear
中)要注意invalidate
。
結果
一張圖說明問題,記憶體峰值的尾部,都是我dismiss掉檢視控制器的時候,可見記憶體被立刻釋放了。當我再次present該檢視控制器時,又迎來一個波峰。
完整的演示專案:SKBFLoginViewController。