天天看點

讓App的運作速度與響應速度趨于一流(iOS)

  有關App運作速度與響應速度優化的好文,按個人了解意譯,受限于水準而不夠嚴謹,附原文位址

  PS,覺得鄙人幹翻譯好過幹編碼的兄弟們頂一下哦!

  第一部分是說理念,太啰嗦,可以直接跳第二部分。

  第二部分是一些實用的優化技術總結(高潮部分)。

  iPad的出現對行業軟體品質提升有着巨大的沖擊。蘋果公司多次提升了其準入标準,最明顯的是要求軟體運作更快更平滑。iPad能被迅速開啟和喚醒,iPad應用能被迅速打開,點按Home鍵應用能迅速切到背景。桌面使用者往往更具耐心,但是iPad使用者期望任何操作能瞬間完成。諷刺的是iPad的計算能力很大程度落後于桌面電腦,不過使用者才不管呢。相比開發iPad應用時對性能優化的充分關注,隻有少數桌面應用開發才會如此。即使擁有偉大的概念和良好的設計,當App讓人感覺很慢時,體驗是毀滅性的。

  不同的App,有的響應迅捷有的響應延緩,為什麼會有這樣的差異呢?我們怎樣讓它們變得更靈敏快捷呢?整篇文章分為兩部分,前部分我們讨論App優化的理念,後部分将提及各種優化技術。我的目的不是詳盡的展現所有的優化技術,而是為你提供一個關鍵技術的指引,以及為什麼你應該去使用。

---------- PART 1

------------

  響應速度vs運作速度,哪個更重要?

  響應速度和運作速度之間有着微妙的差別,響應速度是指監聽使用者輸入到回報使用者的速度,而運作速度是指處理任務的速度。

  使用者都讨厭等待,是以你會說讓App運作的更快非常重要。确實如此,但是運作速度的提升有一個邊界,假如資料要通過網絡下載下傳,或者要進行複雜的計算和渲染,那麼App不可能立即顯示這些内容。這種情況下使用者實際上還是願意等待的,但是你要針對他們的操作給出即時的響應,這種響應可以是簡單的按鈕狀态的改變也可以是複雜的動畫效果。讓App運作更快很重要,讓其迅速響應同樣重要。

  大部分的App是下圖這樣的,而我們的目标是讓feedback在heavy

computing之前執行。

讓App的運作速度與響應速度趨于一流(iOS)

  為什麼響應速度如此重要?

  使用現實中真實的按鈕和開關時會讓人感覺靠譜,當按下按鈕或者打開開關時你可以百分百确定你進行了操作。但是在觸摸屏上你無法感覺,是以視覺響應非常重要。如果一個App不能提供這種即時的響應那體驗将變得非常糟糕,更具體點說就是響應時間不要超過三分之一秒。當你點選了某個位置但是沒有任何事情發生,你會自然而然的認為點選有可能沒有被接受。絕大多數人在這時會再點選一次,這可能造成重複的操作。

  iPad的一個巨大的成功在于大部分軟體讓人感覺真實。App通常使用仿現實世界的方式來讓使用者忘記他們是在使用軟體(例如Paper和iBooks),這正是輕松愉快的使用軟體的方式。但是當App經常花費過多時間來響應你的觸摸時這種軟體的美好使用體驗将消失。大多使用者無法區分一款App是否具有良好的響應能力,但他們能知道到哪款App爽哪款App不爽。

三條原則

  當你的App感覺有點慢了的時候,該做些什麼呢?我這裡給出三條簡單的原則來幫助你聚焦問題。

立即響應

  迅速回饋使用者他的操作已經被接受,然後迅速執行。例如點選按鈕時提供一個touch-down狀态呈現給使用者。

  允許使用者任意時刻中斷

  當耗時操作進行時,回報使用者一個進度

----------- PART 2

-----------

  在這個部分我将講到具體的優化技術。

運作速度

  前一個部分講到了響應能力比運作速度更重要,但是我們還是要從運作速度優化講起。

理論上的方法(不要照做)

  我從計算機科學課程上學到的一件事就是從理論上尋找解決性能問題的方法。看下面的代碼:

//a

very large array with N elements

NSArray * array = [self

createArray];

for(id object in array) {

    [object

performSomeAction];

}for(id object in array) {

performAnotherAction];

}

  對比下面的

//a very large array with N

elements

NSArray * array = [self createArray];

for(id object in array)

{

    [object performSomeAction];

performAnotherAction];}

  第一段代碼裡面你的代碼将對大數組進行兩次周遊,這将比第二段代碼中一次周遊完成所有的任務要低效。從這樣的層面來關注你的代碼能保證算法更具效率,但是我認為你沒必要這樣去做。現代的編譯器能很智能的優化出高效的最終代碼,我們将第一段代碼刻意的寫成第二段代碼那樣對整體的性能提升幾乎是沒有任何意義的。有時候這種刻意而為的代碼層面優化會導緻邏輯不清晰且難以維護的代碼(這也是譯者萬分贊同的,寫出優雅的代碼而不是機器友好的)。

性能的測量

  蘋果為我們提供了強大的工具來測量App性能,是以我們沒必要絞盡腦汁評估寫出的代碼将占用多少時間,我們可以直接測量它們。

  第一條軟體優化的準則就是瞄準能帶來巨大收益的改進,不要一上來就在代碼細節優化上浪費時間。不過我們可以從代碼的角度去分析下哪一塊的改進能帶來較大收益。

  在Xcode的菜單中選擇“Product”,然後執行“profile”。成功編譯之後Xcode将啟動Instruments,稍後你可以看到如下的彈出框:

讓App的運作速度與響應速度趨于一流(iOS)

  這裡有許多的“儀器”可以幫你分析你的App。當聚焦運作速度時,上面紅圈标記的兩個(Time

Profiler工具和Core Animation工具)是最有效的。

Time Profiler

  當我們讨論運作速度時離不開Time

Profiler工具。當你的程式運作緩慢時第一件事情就是調查時什麼占用了大量的時間。總是可以找出一些可以寫得更高效的代碼塊,但是在重寫這些代碼前請列出一個最耗時代碼塊清單。

  當運作Time

Profiler時你将得到一個方法名清單。下面的截圖展示了一個按照時間消耗排序的方法名清單,你可以在時間軸添加一個起點位置和終點位置來關注你程式的不同階段。

  勾選“Invert

call tree”和“Hide system libraries”對清單進行過濾,隻留下你自己所編寫的方法。如果不勾選“Invert call

tree”的話你需要深入到調用堆棧的最裡層才能看到耗時方法。可以嘗試玩弄下這些設定項來擷取對你有用的結果。

讓App的運作速度與響應速度趨于一流(iOS)

  輕按兩下一個方法名可以檢視到具體哪一行代碼花費了如此多的時間:

讓App的運作速度與響應速度趨于一流(iOS)

  這也是Time

Profiler最強大的功能之一,你可以輕松找出哪些代碼需要優化。

  在我們的一個項目裡面使用了一些NSDataFormatter和NSNumberFormatter來顯示不同格式和時區的時間與日期。當我運作Time

Profiler時它指出大部分的時間都是被下面的代碼消耗的:

NSDateFormatter * df = [[NSDateFormatter

alloc] init];

  如果不使用Time

Profiler我很難注意到這個問題,我們完全沒有必要每次都建立NSDateFormatter,是以我們重寫了這塊代碼:

NSDateFormatter

* df = [self

sharedDateFormatter];

  通過下面的方法來防止每次都建立一個新的dateFormatter:

static

NSDateFormatter * sharedDateFormatterInstance;-

(NSDateFormatter)sharedDateFormatter {

   if(sharedDateFormatterInstance ==

nil)

      sharedDateFormatterInstance = [[NSDateFormatter alloc] init];

 return

sharedDateFormatterInstance;

  通過上面的簡單改寫,我們僅僅調用了一次NSDateFormatter那如此耗時的init方法,節省了較多的時間開銷。

  上面就是有關使用Time

Profiler進行優化的經驗,建議你把Time

Profiler當作日常工作流程的一部分。在寫代碼時持續優化保持高效,比事後再回過頭去做所謂的優化專項工作要輕松得多。

Stuttering /

flickering

  不幸的是Time

Profiler不能找出所有的性能問題。當你的App的幀率掉到60(幀/秒)以下你的App就感覺運作得不是那麼平滑了。低幀率導緻滾動視圖和動畫卡頓。

  幀率下降通常意味着iPad的渲染速度跟不上。視覺上較為理想的幀率不低于60(幀/秒),意味着每一幀應該在六十分之一秒内渲染完。

  是以這是該Core

Animation工具閃亮登場的時候了。Core

Animation可以用來測量App的幀率。在左側有一些勾選項用于幫助你記錄下什麼導緻低幀率。我們将重溫下那些比較重要的項。

讓App的運作速度與響應速度趨于一流(iOS)

Offscreen rendering(離屏渲染)

  第一個關注項是off-screen

rendering。離屏渲染意味着你App的部分區域每一幀渲染了兩次。大部分的離屏渲染是陰影和遮罩導緻。iOS首先為目标層渲染陰影,然後再渲染目标層,同樣遮罩也需要如此一個過程,首先渲染目标層,然後為其渲染遮罩。

  當你的App被強制進行離屏渲染時幀率将會大打折扣。勾選Color

Offscreen-Rendered Yellow将高亮進行離屏渲染的所有區域。

讓App的運作速度與響應速度趨于一流(iOS)

  當離屏渲染是陰影導緻的話,常常能夠比較輕松的解決。陰影的耗時計算發生在計算陰影的精确形狀上,目标層将不得不遞歸周遊其子層來計算陰影的形狀,是以當你确知目标層的形狀時你可以為其指定陰影路徑。陰影路徑決定了陰影的形狀。

//we

now assume that thumbView is rectangular. With the bezier path we create nice

round borders.

yourView.layer.shadowPath = [[UIBezierPath

bezierPathWithRect:yourView.bounds] CGPath];

現在請啟用Core

Animation工具并重新運作你的App,當那些離屏渲染減少甚至消滅之後,你的App将會變得流暢很多。

Blended

layers(混合層)

  iPad再渲染每一幀的時候,都将計算每一個像素點的顔色。當最上面有一個不透明層的時候,計算每一個像素點的最終顔色非常簡單,隻需要拷貝該最上面的層的對應點顔色即可。而混合層(非不透明層)則需要對對應的畫面區域進行顔色混合。

  在視圖的層次結構中,混合層越多,渲染的計算量就越大。如果最上面的層是混合層,渲染引擎需要處理其下面覆寫的層,計算每一個點的顔色,如果該下面覆寫的層也是混合層,引擎将繼續檢查其下面的層,如此遞歸下去。

讓App的運作速度與響應速度趨于一流(iOS)

  你可以通過選中“Blender

layers”來檢查混合曾的數量。深紅色區域表示這個區域的渲染非常費勁,可能有多個混合層重疊。如果你的App有過多的紅色區域,應該考慮将視圖的層次結構調整得更加扁平。同時應該将完全不需要透明效果的層添加背景色并設定其“Opaque”屬性為YES,這樣相當于告訴渲染引擎其下面的層不需要進行處理。

Rasterization(光栅化)

  某些情況下層會比較難以渲染(使用了陰影、遮罩、複雜形狀、漸變等),為了優化對這種層的處理,iOS提供了一個叫做“光栅化”的API來對其進行緩存,這将隐式的建立一個位圖,進而減少渲染的頻度。

[layer

setShouldRasterize:YES];

  開啟光栅化的優勢是該特定的層基本不會影響你的整體幀率,劣勢是光栅化将占用更多的記憶體,同時初始化時将占用更多的時間,在對該層進行縮放操作時它将表現為像素化(不是矢量圖形了而是位圖)。

  當你使用光栅化時,你可以開啟“Color

Hits Green and Misses

Red”來檢查該場景下光栅化操作是否是一個好的選擇。如果光栅化的層變紅得太頻繁那麼光栅化對優化可能沒有多少用處。位圖緩存從記憶體中删除又重新建立得太過頻繁,紅色表明緩存重建得太遲。可以針對性的選擇某個較小而較深的層結構進行光栅化,來嘗試減少渲染時間。

響應速度

  上面所講的都是關于性能方面的,提升App的性能通常也能帶來響應速度的提升,但不總是這樣。是以我要總結一些保持你的界面快速響應的技術。

Threads

  一個線程可以被視為CPU按照順序執行的指令隊列。當方法A調用方法B,然後方法B調用方法C,所有的代碼是按順序執行的。這種特性帶來的好處明顯,想象下當你寫代碼時你不能确定哪行代碼先執行哪行代碼後執行,這會多麼的恐怖。一個應用可以有多個并發執行的線程。CPU不斷把執行時間配置設定到各線程,各線程在配置設定到的較短的時間内完成一些工作。

主線程

  應用程式的主線程是很有必要去深入了解的。所有的使用者輸入和UIKit的渲染是在主線程執行。如果你沒有考慮過在你的應用中使用多線程,你可以假定你的應用是運作在主線程,當然實際情況不完全是這樣,iOS在元件封裝時做了些優化,會把一些任務自動的排入其它線程中。

  有的應用隻需要使用主線程就足夠了,所有的代碼都線性執行的(嚴格的按照你期望的順序),如果該應用體驗還行那你也不用線上程問題上糾結太多啦。主線程的一個重要職責是響應使用者輸入(點選、觸摸、手勢等),因為線程一次隻能做一件事,過分依賴主線程可能讓你遇到麻煩(例如要在主線程執行一個耗時多達幾百毫秒的運算)。你的App需要能夠在處理耗時計算的同時迅速響應使用者輸入。

  是以下面的代碼是非常傻叉的:

NSURLConnection

* conn = [NSURLConnection sendSynchronousRequest:req returningResponse:res

error:&error];

  或者采用不太容易看出來但仍然傻叉的方式:

NSData * data = [[NSData alloc]

initWithContentsOfURL:

someExternalURL];

  在主線程做這種操作很危險,你完全不知道你的使用者界面将失去響應多久。

概括起來:

  大多數代碼在主線程執行,包括所有的UIKit代碼和所有的事件處理。

  主線程(其它線程也一樣)同一時刻隻能做一件事情。

  如果你的App在主線程執行一個持續3秒的任務,那麼它将失去響應3秒鐘。

  你可以建立和使用一個獨立的線程來執行耗時操作以免使用者界面阻塞。

Grand

Central

Dispatch

  那麼怎樣來使用線程呢?一般建議使用GCD。GCD和線程是有差別的,我們這裡不深入研究了,但你會發現GCD是線上程基礎上建立起來的一套強大機制。GCD是為了并行運作代碼(注意不是并發)設計的,在多核系統上尤其強大。

  要使用GCD首先要建立一個dispatch

queue。你可以這樣做:

//blocks added to this queue are executed

serially

dispatch_queue_t backgroundQueue =

dispatch_queue_create("yourqueue", 0);

  或者你可以使用一個系統的background

queue,系統的background

queue是配置為block并行運作的。如果你需要block有序的執行時,可以使用dispatch_queue_create來建立一個串行queue。

//blocks

added to this queu can be executed concurrently

dispatch_queue_t

existingQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,

0);

  你可以從其它線程為queue指派任務。dispatch_async将異步執行,也就是說該方法能夠立即傳回,不需要等block執行完。在該queue執行block時你又可以為其它queue指派任務。

dispatch_async(backgroundQueue,

^{

    //your code that should run in the background

    //do some heavy

work here.....

    dispatch_sync(dispatch_get_main_queue(), ^{

   //notify the main thread here

});

  允許使用者在任意時刻中斷

  雖然線程是很強大的,但是不能解決你的所有問題:

  UIKit是非線程安全的。這意味着所有具有UI字首的類和方法不能在背景線程調用。

  當線程開始執行一段代碼時,是很難停止下來的(如果你不能了解這句話說明你還太年輕,譯者按)

  多線程讓你的代碼變得複雜,線程共享變量共享記憶體的操作将困擾你。

  上面的問題給我們帶來了最後一個話題:runloops。Runloops提供了一套機制讓你在主線程執行代碼時App依然能夠響應輸入。主runloop是一個在主線程上運作的持續的循環,在每個循環過程它都将監聽使用者輸入,更新螢幕,執行計劃好的任務(例如定時器)。下面的來自蘋果官方的圖講解了在每次循環中要做的事情,你的應用停止響應是因為這個runloop被耗時的代碼段給阻塞。如果你能夠将這耗時代碼段打散成更輕量的段落,并将它們分散到多次循環中來運作,那問題将得到解決,因為在每次循環中App都将在執行輕量的段落之前監聽使用者輸入。

繼續閱讀