在知乎回答了一個“為什么微博的app在iPhone比Android上流暢”的問題。后面部分是一個典型的動畫卡頓的性能分析過程,因此帖在這里。有編程問題可以在這里交流。知乎鏈接。
=========================================================
我來說下我所知道的事情。我不知道iOS為什么流暢,但我知道一些Android為什么不流暢的原因。
首先,就題主所說的問題,我用iPad和小米Pad對比了一下微博滑動滾屏這件事情(2014年8月10日目前微博app最新版本)。正如題主所說,直觀感受上明顯感覺iOS要流暢、舒服。
在這件事情上我認為主要是這三個原因:
- 速度曲線。
當你滑動界面然后松手,這時界面會繼續滑動,然后速度減小,直到速度為0時停止。iOS下速度減小的這個過程比較慢,尤其是快要停的時候是慢慢停的,視覺上有種很順滑的感覺;Android下則從松手到停要快很多,相比之下有種戛然而止的感覺。
從數據/技術角度來看這個事情,我們滑動界面的最終目的不是為了“動”,而是為了“停”,因此只要平滑的到達目的地,似乎越快完成這個過程越好,所以Android的選擇是理所當然的。但事實是,大家普遍更喜歡iOS的方式,這樣做顯得更順滑、更優雅。 - 幀率。
絕大部分時間兩者都能保持60FPS左右的滿幀率。但都會有偶爾的掉幀。並且Android上要比iOS上嚴重很多。(好吧,比起前兩年,已經好太多了。)我前前后后滑動了幾十次,iOS在前面遇到1次掉幀,后面就很穩定了。而Android幾乎每滑動一次都會伴隨一次掉幀。這完全就是真真實實的卡頓,用戶必然會感覺到那一刻的不流暢。Android掉幀的原因我后面再詳細分析。 - 觸摸響應速度。
從手指碰到觸摸屏,到屏幕上顯示處理這次觸摸產生的畫面,是需要時間的。時間越短感覺越跟手。據說iOS的觸摸屏的處理時間要比一般的Android手機快,這不是我的專長,不知道怎么驗證。但在軟件系統層面,Android的顯示機制是app-->SurfaceFlinger-->Display,這比傳統的app-->Display多了一步,主要基於這個原因,畫面最終輸出到屏幕要比傳統的方式慢一幀(16.7ms)。
我覺得第1點造成的影響最大,恰恰卻是最技術/設備無關的。如果微博app或者Android系統要改變,很容易就可以調得跟iOS一模一樣。但正是由於這是產品形態上的差別而不是純粹技術上的優劣,反倒成了Android最不太可能改變的。
第2點的影響其次,當然是指在目前這個大部分時候都能滿幀的情況下。這方面是Android從早期到現在進步最明顯的方面,使用了很多方法來優化幀率。但就算現在Android進化了很多,硬件性能也進化了很多,卻仍舊不可能徹底消滅掉幀的情況。
第3點通俗的講就是跟手性,跟手性的重要性不言而喻,但現在的差別比較細微,且具體數據我也不清楚。
我想過一個辦法讓桌面、微博這種內容和手一起動的應用繪制到屏幕的速度快一幀(16.7ms),其實就是抵消之前提到的慢的那一幀,需要framework層和app層一起配合改動,目前已申請了專利但代碼還沒進,將來有時間了應該會進到MIUI。感興趣的可以看看專利:滑動操作響應方法、裝置及終端設備。
最后我來用專業技術分析一下微博app在Android里掉幀的原因。非編程人員可以不看下去了。(這個過程我使用的是小米3高通版+最新版微博app。)
最初,我認為這種現象很像GC(垃圾回收)導致的,於是打開logcat觀察每次卡頓的時候有沒有GC發生。結果發現並沒有。停下來的時候才會有GC,這說明微博app在滑動過程中控制得不錯,在停下來的一刻才去分配內存,使GC不影響幀率。
然后我打開“開發者選項”->“GPU呈現模式分析”->“在屏幕上顯示為條形圖”(好像是4.4才有這個選項,之前的只能在dumpsys里看),這會在屏幕上直觀的顯示每幀繪制花費的時間。屏幕上有條基准線大概是16ms,如果超過這條線則很有可能掉幀了。如果下面的藍色部分很長則說明是軟件draw的部分太費時,那么可以通過traceview來繼續分析draw的java代碼。(我假定了現在的app都是硬件加速的方式。)如果中間紅色部分很長則說明是OpenGL ES繪制過程太費時。可以用gltrace來分析OpenGL ES的調用過程。
打開選項后使用微博app,發現雖然偶爾有超基准線,但並不嚴重,並且卡頓的時候並沒有明顯異常。
於是我開始使用systrace,這下就很明顯了:
可以發現每隔幾幀,就會有一次大間隙,圖中我圈了紅圈圈的兩個地方都明顯超過正常的耗時時間。現在問題是,這些地方的代碼在干什么呢?是什么花費了這么長時間?
放大后發現都是這兩個:obtainView和setupListItem。它們都是在draw之前調用的,這也解釋了通過上一步的方法(“GPU呈現模式分析”)為什么沒有顯示出來,因為那個方法只展示draw里面的時間消耗。
通過在源碼里搜索obtainView和setupListItem,可以發現是AbsListView的obtainView方法和ListView的setupChild方法打下的這個log。
接下來,我們用traceview來看看這兩個方法里面究竟調用了什么代碼導致耗時過長。
跟蹤obtainView最終到了這里:
代碼混淆了,看不太出來了,也懶得再跟下去了。但隨便點了點然后看到:
TextView.setText用了挺多的時間。感覺Android團隊應該優化優化這個方法。
然后再來跟蹤setupChild方法:
都是measure占用的時間,並且調用次數比較多。這應該說明了View的數量不少。用hierarchyviewer看了下,的確不少,一條微博有50多個View:
結論:我們最終大致找到了耗時的代碼及部分原因。這兩個耗時方法應該都是有可能減下來的,但應該不那么容易。
在Android上實現功能比較容易,但如果默認實現有了性能問題,要想解決就不好說了,有時候要繞很遠。
最后,列一些相關鏈接:
Android圖形架構(繪圖慢一幀的事情這里有說):http://source.android.com/devices/graphics/architecture.html
Android Performance Case Study:http://www.curious-creature.org/docs/android-performance-case-study-1.html
