各大手機廠商發佈的旗艦機,都將 90Hz 甚至 120Hz 高幀率流速屏當成了標配,那這對我們實際開發的 App 會不會有影響?原本在 60Hz 下,每幀只需保證 16ms 內繪製完成就可以做到流暢,換到高幀屏中,實際留給我們繪製時間是縮短了的,是否對我們的代碼質量要求更高?
推薦一篇文章,來講講對此的影響,希望對大家有幫助。
以下文內的 "我" 爲文章原作者。
前言
昨天在 IT 之家留言說,如果應用無法滿足 120hz 的繪製會怎樣?假設如果繪製一幀的時間,如果大於 1/120 秒,哪怕是多了 1 毫秒,就會導致應用在 120Hz 的手機上也就變成了 60Hz。
後來仔細想想,這句話說的並不是特別嚴謹,爲什麼這麼說呢?
一、證明我的觀點
首先我寫一個 demo 來證明我的觀點。
1.1 滿幀的應用
public class MyTextView extends TextView {
int i = 0; @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (i < 6) {
this.post(new Runnable() {
@Override
public void run() {
MyTextView.this.setText(i++ + "");
}
});
}
}
}
我寫了一個只繪製 6 幀的一個界面,Activity 中包含這個 MyTextView,通過抓 Trace 可以看到,下面這個 6 幀繪製圖,都是在一個 Vsync 週期(圖中黑白相見的色塊,均是一個 Vsync 週期)繪製一幀。
爲什麼第一幀會有點延遲,主要是因爲啓動時,主線程在幹了些其他事情,好在繪製花不了太多時間。
大概繪製也就 3 毫秒左右,畢竟界面簡單。
1.2 幀數減半的應用
public class MyTextView extends TextView {
int i = 0;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
try {
Thread.sleep(16);
} catch (Exception e) {
}
if (i < 6) {
this.post(new Runnable() {
@Override
public void run() {
MyTextView.this.setText(i++ + "");
}
});
}
}
}
我手動人爲在 MyTextView.onDraw()
方法中 sleep 了 16 毫秒,來模擬很多應用實現的不好,導致繪製耗時的情況,例如 View 層級厲害複雜,甚至在 onDraw()
方法中進行文件讀寫。
通過抓 trace 可以看到,原本之前的例子(1.1)中 6 幀由 6 個時間週期繪製完成,現在變成了 6 幀由 12 個時間週期完成了。
1.3 小結
這就是我一直想說的一個觀點,120Hz 的手機體驗不一定有 90Hz 的手機好的原因,一旦主線程一次 traversal(一次 ViewRootImpl 的 onDraw() 的遍歷)的時間超出了 vsync 的時間週期 1/120s,就會導致幀率直接砍半,降爲 60 幀,90Hz 可以讓這個折半降幀率的現象減少。
二、繪製一幀指的是什麼
爲什麼我說我的觀點不準確?其實是我太籠統的定義了繪製一幀這個詞。
在硬件加速開啓的情況下:
如果我將繪製一幀的時間定義成:主線程一次 traversal,那我說的話是正確的。
如果我將繪製一幀的時間定義成:主線程一次 traversal + renderthread 一次渲染,那我說的話是不嚴謹的。
2.1 引發我思考的 trace
因爲要讓主線程一次 traversal + renderthread 超時不是特別好寫 demo,我就用一個昨天引發我思考的一個 trace 給大家講解一下。
大家從第一個黃色 F 的圓圈開始看。
UI Thread 的 doFrame + RenderThread 的 DrawFrame 的時間超出了一個 Vsync 週期,接下來每一幀的情況,都是按照第一幀繪製的情況一樣的情況運行,最好造成的現象就是雖然幀數是滿幀,但是每一幀其實都是延遲顯示的。
用一句哲學的話來概括就是:你眼睛看到任何事物,其實都是事物過去的樣子。
假如主線程一次 traversal + renderthread 一次渲染時間超出了 vsync 週期不多,這樣子的應用大概率是可以滿幀運行(滿幀不代表顯示正常)。
我上面一句話中用了不多,大概率這種模糊的文字,因爲這個情況下能否滿幀的臨界點比較難定義。
2.2 如何定義這個 120 變成 60 幀的臨界點
繼續下面這個圖,你會發現 UI Thread 的 doFrame 會非常 happy 的按照 Vsync 信號執行,但是下面 renderthread 已經忙的不可開交了。
其實第一幀已經延遲了,假如運氣不好導致了 drawframe 的時間橫跨了 2 個 Vsync 週期,還是會導致丟幀,這個時候就從 120 幀降爲 60 幀。
三、總結
3.1 理想中的結論
假設開啓硬件加速,一幀繪製的時間 = UI 線程(T1)+ RenderThread 線程(T2)。
那麼 Vsync 時間的週期是 1/120s,約等於 8.3ms,此時:
如果 16.6ms > T1 > 8.3ms,會導致從 120 幀降爲 60 幀。
如果 T1 <8.3ms,16.6ms> T1 + T2 > 8.3ms,雖然還是滿幀,但是你看到的永遠是前一幀的畫面。
如果 T1 + T2 > 16.6ms,毫無疑問會導致從 120 幀降爲 60 幀。
上面理論看起來是很完美的結論,千萬別把這個當做定理去記憶,因爲現實會比這些情況複雜的多。
3.2 顯示中的結論
可能會有人說現在 CPU,GPU 那麼強,怎麼可能會超時呢?
舉個微信在 865 的手機上,滑動消息列表的例子,T1 約等於 6ms,T1 + T2 約等於 8.2ms。
這還並不是太過複雜的界面,大家會發現,6ms 已經很接近 8.3ms 了。
3.3 給開發者的建議
對於要適配 120hz 手機的應用的工程師,你們要注意以下事情。
避免 T1 > 8.3ms,也就是要減少主線程乾的活,要減少一次 draw 的時間。千萬別在 draw 的流程進行文件訪問或者 sleep。
避免在滑動響應的時候 T1+T2 > 8.3ms,雖然界面在顯示的時候,推遲一幀用戶是察覺不了的,但是在滑動的響應中如果推遲一幀,可能會影響 "跟手" 的體驗。
千萬別關閉硬件加速,一旦關閉硬件加速 T1+T2 > 8.3ms 就會導致 120 降爲 60 幀。
四、尾巴
說到這裏,如果聽得懂我在說什麼的人,應該能懂我想表達的意思。
如果不懂我說的什麼的人,希望你去補一下以下知識點,再回過頭來看這個文章。
知識點 1:
TextView.setText 到屏幕顯示文字,整個過程發生了什麼。
知識點 2:
硬件加速之後,主線程(UI Thread)和 RenderThread 之間的協同工作的方式。
知識點 3:
APP 界面和 SurfaceFlinger 之間的關係
可能會有人說,你揪那麼細緻幹嘛,對於用戶來說,掉了幾幀的影響也不大,,但是對於性能優化工程師來說,有時候就是要糾結那幾個丟幀的情況,只有做好了每一幀的完美繪製,才能給用戶帶來最完美的用戶體驗。
- End -
文章看完了,最後我換個角度補充一些觀點。
手機硬件雖然增加了高幀屏,確實看似對每一幀繪製的時間要求更短了,但別忘了,在升級屏幕的同時,對其他硬件(CPU、GPU、ROM等)也做了相應的升級。雖然留給我們的刷新時間縮短了,但計算和渲染繪製的能力,也相應的提高了。
那麼到具體的代碼上,你在舊手機上繪製一幀需要 10ms 的代碼,在 120Hz 的旗艦機上可能只需要 6ms,兩相抵消之下問題不會太大。
所以最後核心還是在日常編碼的過程中,隨時注意性能的問題。管他高幀低幀,領導覺得"不跟手",咱就嘚優化。
本文對你有幫助嗎?留言、轉發、點好看是最大的支持,謝謝!
公衆號後臺回覆成長『成長』,將會得到我準備的學習資料。