向高手進階--性能優化

在學習了 Androd 的相關基礎知識后,自己也動手寫了幾個項目。IM、 O2O、 Live 等都寫過,而且大部分都是自己實現的。可是完成這么多項目之后,并沒有感覺自己成為了大神,但是我們和那些大神不都是在做項目么???我們離大神的差距在哪里呢???

我難道是大神,哇哈哈~!

當我自己做完了一個 O2O 仿淘寶的項目過后,界面和功能都高仿淘寶,感覺自己可以進淘寶了,沒什么毛病。可是一安裝使用過后,才發現這 App 要不是自己寫的,活不到下一回合啊。卡死了,數據加載太慢了,這還是從自己搭的服務器上獲取的少量數據,要是淘寶的數據一放上來估計活不過下一秒。用著用著還老是閃退、崩潰,傷心ing。傷心過后生活還必須正常進行下去,必須解決啊,這里就獻上一方良藥。那就是性能優化。

性能優化

性能優化對于我們來說很直接的一個點就是解決卡的現象。一個 APP 加載一個頁面在網絡良好的情況下還要 幾秒鐘,滑動一下半天才反應過來。相信誰都不想用這個 APP。

是怎么原因造成的呢?要理解卡頓的真正原因要先知道 Android 的渲染機制。

小知識擴展

相信大家小時候都應該玩過或者見過一個游戲,那就是一本小書上面畫著很多幅畫面,這些畫面是將一個動作拆分為一個個漸變的畫面,然后你可以快速的翻動它,這樣就感覺能看到一個動態的畫面。這里一幅畫面就是一幀。在短時間內(幾秒)在翻動多幅畫面(幾十幀),就會讓你看到一個動態的畫面。但是如果你翻得比較慢,你也就沒有這種感覺了。

  • Fps ( Frames Per Second): 幀 / 秒。(這里 Fps 越高越好,還是越低越好~!)
  • 幀:一幅畫面。

視覺暫留,由于人類眼睛的特殊生理結構,如果所看畫面之幀率高于每秒約 10-12 幀的時候,就會認為是連貫的,超過 10-12 幀就感覺不出畫面是有間隔的,幀數就是在 1 秒鐘時間里傳輸的圖片的量,也可以理解為圖形處理器每秒鐘能夠刷新幾次。超過大概 85 赫茲(幀)的圖像,像是畫面每更新一次只會發光幾百分之一秒的陰極射線管及等離子顯示屏,此時已經到達大腦處理圖像的極限,人眼并無法分辨與更高更新率的差異。

  • 12 fps:由于人類眼睛的特殊生理結構,如果所看畫面之幀率高于每秒約10-12幀的時候,就會認為是連貫的
  • 24 fps:有聲電影的拍攝及播放幀率均為每秒24幀,對一般人而言已算可接受
  • 30 fps:早期的高動態電子游戲,幀率少于每秒30幀的話就會顯得不連貫,這是因為沒有動態模糊使流暢度降低
  • 60 fps:在實際體驗中,60幀相對于30幀有著更好的體驗
  • 85 fps:一般而言,大腦處理視頻的極限

有人說是 24 幀無法識別流暢,其實不是眼睛的問題了,有聲電影的拍攝及播放幀率均為每秒 24 幀,對一般人而言已算可接受,但對早期的高動態電子游戲,尤其是射擊游戲或競速游戲來說,幀率少于每秒 30 幀的話,游戲就會顯得不連貫,這是因為電腦會準確地顯示瞬時的畫面(像是一臺快門速度無限大的相機),沒有動態模糊使流暢度降低。

第一個原因就是由兩者圖像生成原理不同造成的。

電影雖然只有24FPS,但是每一幀都包含了一段時間的信息,而游戲則只包含那一瞬間的信息。一個電影在一段時間內曝光,畫面的每一幀,都包含有一段時間的信息,這段時間的長度由快門時間決定,最長不能超過1/24秒,所以視頻中每一幀包含信息量較大。而游戲的第一幀包含第0秒的信息,第二幀包含了第1/24秒的信息,只有這一個瞬間的信息,這中間的信息完全丟失了,所以看起來會卡。

用圖來解釋一下,比如有一個圓從左上角移動到右下角,第一幀是這樣的:

第一幀

如果是電影,第二幀可能是類似下圖這樣的(圖畫得不好但是就是這個意思):

電影 第二幀

如果是游戲的話,第二幀就應該是這樣的圖:

游戲 第二幀

看出區別來了嗎?這是因為電影和游戲的畫面生成方式的本質不同造成的,電影的畫面是拍攝的實際場景,在快門時間內膠片/傳感器持續曝光,這一段時間里人物場景的變化都會被拍到膠片/傳感器上,每隔一段時間換下一張膠片再曝光一段時間。而游戲的畫面則是由顯卡生成的,顯卡通過計算生成一幀畫面,生成完畢后再計算下一幀,這樣每一幀都是清晰的,不會有模糊,像我上面圖中的那個圓,不管他的移動速度是快是慢,顯卡只計算兩幀畫面,中間的移動軌跡一概不會顯示,我們看到物體就好像老版西游記里面孫悟空施一個法術“就”的一聲飛過去了。

60 Fps 和 16 ms

市面上絕大多數Android設備的屏幕刷新頻率是 60 HZ。現在 Android 以 60 Fps 來作為 App 性能的衡量標準,因為這是人眼和大腦之間的協作感知到的流暢畫面更新體驗,當然,再高點也可以。也就是一秒內要繪制 60 幀,即 1000ms / 60Fps = 16ms 。因此 Android 系統要求每一幀都要在 16 ms 內繪制完成。這意味著每一幀你只有 16 ms(1秒 / 60幀率)的時間來處理所有的任務。

對我們來說任務主要是繪制界面,平滑的完成一幀意味著任何特殊的幀需要執行所有的渲染代碼(包括 framework 發送給 GPU 和 CPU 繪制到緩沖區的命令)都要在 16ms 內完成,保持流暢的體驗。

16ms

如果你的應用沒有在 16ms 內完成這一幀的繪制,假設你花了 24ms 來繪制這一幀,那么就會出現掉幀,也就是我們前面所說的卡的現象的情況。
這里掉幀是說這一幀延遲繪制呢,還是說直接不繪制了呢~!

drop frames

系統準備將新的一幀繪制到屏幕上,但是這一幀并沒有準備好,所有就不會有繪制操作,畫面也就不會刷新。反饋到用戶身上,就是用戶盯著同一張圖看了 32ms 而不是 16ms ,然后突然看到的是下下張圖,也就是說掉幀發生了。

掉幀 drop frames

先來了解一個概念:
Vertical Synchronization :垂直同步。簡稱為:VSYNC。
又沒有豎直刷新呢~!
定義:顯卡的輸出幀數和屏幕的垂直刷新頻率同步。

Android 系統每隔 16ms 就會發出一個 VSYNC 信號,通知 GPU 來渲染界面了。但是這個時候 GPU 上個工作還沒有做完,沒空理你,好吧,這一幀的畫面就沒有繪制,丟失了,掉了。下一次這個信號就又要等 16ms 了。在這 16ms 中,你還是只能看到上次渲染的畫面。于是你就感覺到卡住了。

引起掉幀的原因非常多,比如:

  • 布局嵌套層次太多,花了非常多時間重新繪制界面中的大部分東西,這樣非常浪費 GPU 時間。


    布局嵌套層級太多
  • 過度繪制嚴重,在繪制用戶看不到的對象上花費了太多的時間。


    OverDraw
  • 有一大堆動畫重復了一遍又一遍,消耗 CUP、GPU 資源。


    動畫重復
  • 頻繁的觸發垃圾回收
    虛擬機在執行 GC 垃圾回收操作時,所有線程(包括 UI 線程)都需要暫停,當 GC 垃圾回收完成之后所有線程才能工作。如果大量 GC 操作導致渲染時間操作 16ms , 就會導致丟幀卡頓的問題。

注意:Android4.4 引進了新的 ART 虛擬機來取代 Dalvik 虛擬機。它們的機制大有不同,簡單而言:

  • Dalvik 虛擬機的 GC 是非常耗資源的,并且在正常的情況下一個硬件性能不錯的Android設備也會很容易耗費掉 10 - 20 ms 的時間;
  • ART 虛擬機的GC會動態提升垃圾回收的效率,在 ART 中的中斷,通常在 2 - 3 ms 間。 比 Dalvik 虛擬機有很大的性能提升;

ART 虛擬機相對于 Dalvik 虛擬機來說的垃圾回收來說有一個很大的性能提升,但 2 - 3 ms 的回收時間對于超過16ms幀率的界限也是足夠的。因此,盡管垃圾回收在 Android 5.0 之后不再是耗資源的行為,但也是始終需要盡可能避免的,特別是在執行動畫的情況下,可能會導致一些讓用戶明顯感覺的丟幀。

既然知道了造成差體驗的原因,那下面就來講講有那些解決方法。

To 檢測和解決

想要解決這些沒有優化的地方,最笨的方法就是去看代碼,一行一行找,查看哪里沒有寫好,邏輯沒有優化等等。還可以使用很多工具來幫助我們來查找到應用的一些做的不好的地方。

調試工具

1. OverDraw

  • OverDraw: 過渡繪制。

生活一點的說法: 任何事物都有個度。你干好事過渡了也會變成壞事,做壞事也就不用說了。通俗來講,繪制界面可以類比成一個涂鴉客涂鴉墻壁,涂鴉是一件工作量很大的事情,墻面的每個點在涂鴉過程中可能被涂了各種各樣的顏色,但最終呈現的顏色卻只可能是 1 種。這意味著我們花大力氣涂鴉過程中那些非最終呈現的顏色對路人是不可見的,是一種對時間、精力和資源的浪費,存在很大的改善空間。繪制界面同理,花了太多的時間去繪制那些堆疊在下面的、用戶看不到的東西,這樣是在浪費CPU周期和渲染時間!

專業解釋:某些組件在屏幕上的一個像素點的繪制次數超過一次。

官方例子,被用戶激活的卡片在最上面,而那些沒有激活的卡片在下面,在繪制用戶看不到的對象上花費了太多的時間。

** Android 會在屏幕上顯示不同深淺的顏色來表示過度繪制:
沒顏色:沒有過度繪制,即一個像素點繪制了 1 次,顯示應用本來的顏色;
藍色:1倍過度繪制,即一個像素點繪制了 2 次;
綠色:2倍過度繪制,即一個像素點繪制了 3 次;
淺紅色:3倍過度繪制,即一個像素點繪制了 4 次;
深紅色:4倍過度繪制及以上,即一個像素點繪制了 5 次及以上;

4種過渡繪制相應的顏色表示

設備的硬件性能是有限的,當過度繪制導致應用需要消耗更多資源(超過了可用資源)的時候性能就會降低,表現為卡頓、不流暢、ANR 等。為了最大限度地提高應用的性能和體驗,就需要盡可能地減少過度繪制,即更多的藍色色塊而不是紅色色塊。


藍色 > 紅色

實際測試,常用以下兩點來作為過度繪制的測試指標,將過度繪制控制在一個約定好的合理范圍內:
應用所有界面以及分支界面均不存在超過4X過度繪制(深紅色區域);
應用所有界面以及分支界面下,3X過度繪制總面積(淺紅色區域)不超過屏幕可視區域的1/4;

過渡繪制的根源
過度繪制很大程度上來自于視圖相互重疊的問題,其次還有不必要的背景重疊。

同樣的視圖呈現,官方例子,比如一個應用所有的 View 都有背景的話,就會看起來像第一張圖中那樣,而在去除這些不必要的背景之后(指的是 Window 的默認背景、 Layout 的背景、文字以及圖片的可能存在的背景),效果就像第二張圖那樣,基本沒有過度繪制的情況。

優雅源碼

有能力且有興趣看源碼的童鞋,過度繪制的源碼位置在: /frameworks/base/libs/hwui/OpenGLRenderer.cpp ,有興趣的可以去研究查看。

 if (Properties::debugOverdraw && getTargetFbo() == 0) {
        const Rect* clip = &mTilingClip;
        mRenderState.scissor().setEnabled(true);
        mRenderState.scissor().set(clip->left,
                mState.firstSnapshot()->getViewportHeight() - clip->bottom,
                clip->right - clip->left,
                clip->bottom - clip->top);

        // 1x overdraw
        mRenderState.stencil().enableDebugTest(2);
        drawColor(mCaches.getOverdrawColor(1), SkXfermode::kSrcOver_Mode);

        // 2x overdraw
        mRenderState.stencil().enableDebugTest(3);
        drawColor(mCaches.getOverdrawColor(2), SkXfermode::kSrcOver_Mode);

        // 3x overdraw
        mRenderState.stencil().enableDebugTest(4);
        drawColor(mCaches.getOverdrawColor(3), SkXfermode::kSrcOver_Mode);

        // 4x overdraw and higher
        mRenderState.stencil().enableDebugTest(4, true);
        drawColor(mCaches.getOverdrawColor(4), SkXfermode::kSrcOver_Mode);

        mRenderState.stencil().disable();
    }
}
  • 追蹤過渡繪制
    通過在 Android 設備的設置 APP 的開發者選項(手機開發者選項怎么打開知道么,在手機設置里,關于手機這一項,進去后點(我的小米)版本號一欄,點擊 7 此次。 Android 模擬器點擊 Build Number。)里打開 “ 調試 GPU 過度繪制 ” ,來查看應用所有界面及分支界面下的過度繪制情況,方便進行優化。
Show GPU overdraw

2. Lint

Android Lint 是一個代碼掃描工具,能夠幫助識別代碼結構存在的問題,主要包括:

  • 布局性能(可以解決無用布局,嵌套太多,布局太多)
  • 未使用的資源
  • 不一致的數組大小
  • 國際化問題(硬編碼)
  • 圖標的問題(重復的圖標,錯誤的大小)
  • 可用性問題(如不指定的文本字段的輸入型)
  • manifest 文件的錯誤

使用步驟:

第一步
第二步
第三步

3. 多使用 include、merge、Stub 等標簽

include:用來代碼布局復用

使用格式:

<!-- include 復用布局 -->
    <include
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        layout="@layout/layout_title"/>

merge:用來減少布局層級,和 include 搭配使用

<include layout="@layout/layout_merge"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- reduce layout hierarchy -->
<!-- use with include -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        android:textSize="30sp"
        />
</merge>

ViewStub:按需加載,ViewStub 繼承自 View,它非常輕量級且寬/高都是0,因此它本身不參與任何的布局和繪制過程。

在實際開發中,有很多布局文件在正常情況下不會顯示,比如網絡異常的界面,在網絡正常時就沒有必要加載。

ViewStub 的示例:

<ViewStub
        android:id="@+id/stub_import"
      android:inflatedId="@+id/panel_import"
        android:layout="@layout/layout_network_error"
        android:layout_width="match_parent"
     android:layout_height="wrap_content"/>

其中 stub_import 是 ViewStub 的 id, panel_import 是 layout_network_error 這個布局的 id。那么它是如何做到按需加載的呢?在需要加載 ViewStub 中的布局時,可以按照如下兩種方式進行:

View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();

或者

((ViewStub)findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);

當 ViewStub 通過 setVisibility 或者 inflate 方法加載后,ViewStub 就會被它內部的布局替換掉,這個時候 ViewStub 就不再是整個布局結構中的一部分了。另外,目前 ViewStub 還不支持 <merge> 標簽。

4. TraceView

TraceView 是 Android 平臺特有的數據采集和分析工具,它可以查看每一個方法運行的時間,常用來定位兩類性能問題:

  • 方法調用一次需要耗費很長時間導致卡頓
  • 方法調用一次不長,但被頻繁調用導致累計時長卡頓

使用步驟:


第一步.png
第二步

ProfilePanel 各列作用說明:


ProfilePanel 各列作用說明

5. NinePatch

點9,.9,NinePatch:一種特殊格式的圖片。

NinePatchDrawable 繪畫的是一個可以伸縮的位圖圖像,Android會自動調整大小來容納顯示的內容。一個例子就是NinePatch為背景,使用標準的Android按鈕,按鈕必須伸縮來容納長度變化的字符
NinePatchDrawable是一個標準的PNG圖像,它包括額外的1個像素的邊界,你必須保存它后綴為.9.png,并且保持到工程的res/drawable目錄中。如果你是從APK解壓后得到的*.9.png文件,注意它是已將周圍的空白像素去掉了的,在使用時必須再加上。

左邊跟頂部的線來定義哪些圖像的像素允許在伸縮時被復制。
底部與右邊的線用來定義一個相對位置內的圖像,視圖的內容就放入其中。

.9.png 圖片四邊的作用

6. ANR

ANR:Application Not Response(應用無響應)的簡稱。當 UI 線程在 5 秒之后無法響應用戶的觸摸操作,就會發生 ANR 現象。ANR 導致應用卡死,必須解決。

ANR 的種類

  • 在 UI 線程中執行了耗時操作,此時用戶在該耗時操作尚未執行完畢時,觸摸了屏幕,導致 UI 線程在 5 秒內無法響應用戶操作。
  • 廣播接收者在 onReceive() 方法中執行了一段耗時操作,此時用戶在該耗時操作尚未完成時,觸摸了屏幕,導致 UI 線程 10 秒內無法響應用戶操作。
  • 在 Service 中 on 打頭的方法中執行了耗時操作(因為 Service 也是運行在主線程中的)

捕獲處理 ANR

  • 在 LogCat 中可以看到 ANR 的信息
  • 在 Android Device Monitor 的 FileExplore 中找到 data/data 文件夾下有一個 traces.txt 文件,該文件中描述了 ANR 的詳細信息。

如何避免 ANR

  • 不在 UI 線程中執行耗時操作,如:
    • 執行 IO 操作
    • 執行數據庫讀寫
    • 執行網絡操作
    • 自定義 View 中的 onDraw() 方法中執行耗時操作
    • 其它任何導致 UI 線程阻塞的操作。如長時間的循環、遞歸等。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容