硬件加速淺談

做者

你們好,我叫大聖;java

本人於2018年5月加入37手遊安卓團隊,曾經就任於愛拍等互聯網公司;android

目前是37手遊安卓團隊的國內負責人,主要負責相關業務開發和一些平常業務統籌等。web

背景

最近在處理一個遊戲內嵌社區頁面卡頓問題的過程當中,發如今Manifest的Activity中有將硬件加速關閉的配置,若是將其開啓,那麼頁面卡頓的問題就解決了。對這一塊的知識還不是很瞭解,也不知道直接打開硬件加速對遊戲是否有影響。因而乎在網上查了一波別人的文章,作了一個簡單的總結。canvas

CPU與GPU

在瞭解什麼是硬件加速以前,先了解一下什麼是CPU和GPU。 CPU(Centr(l Processing Unit,中央處理器)是計算機設備核⼼器件,⽤於執⾏ 程序代碼,軟件開發者對此都很熟悉;GPU(Gr(phics Processing Unit,圖形處理 器)主要⽤於處理圖形運算,一般所說「顯卡」的核⼼部件就是GPU。緩存

16209e544e11314d.png

  • 黃色的Control爲控制器,用於協調控制整個CPU的運行,包括取出指令、控制其餘模塊的運行等;
  • 綠色的ALU(Arithmetic Logic Unit)是算術邏輯單元,用於進行數學、邏輯運算;
  • 橙色的Cache和DRAM分別爲緩存和RAM,用於存儲信息。

從上面的結構圖能夠看出,CPU的控制器較爲複雜,擅長各類複雜的邏輯運算;GPU的控制器比較簡單,但包含了大量ALU,擅長大量的數學運算。微信

硬件加速的主要原理,就是經過底層軟件代碼,將CPU不擅長的圖形計算轉換成GPU專用指令,由GPU完成。markdown

顯卡和GPU不是同一個概念,顯卡指的是包含GPU的一個完整的卡,GPU是單指顯卡的核心芯片,是一顆芯片;
據說比特幣挖礦的機器都是採用GPU運算的,是否是和上面的原理同樣,挖礦是大量的數學計算,GPU更加擅長呢?app

Android中的硬件加速

接下來看看Android中如何使用硬件加速的。ide

如何開啓硬件加速

從 Android 3.0(API 級別 11)開始,Android 2D 渲染管道⽀持硬件加速,也 就是說,在 View 的畫布上執⾏的全部繪製操做都會使⽤ GPU。啓⽤硬件加速須要 更多資源,所以應⽤會佔⽤更多內存。oop

若是您的⽬標 API 級別爲 14 及更⾼級別,則硬件加速默認處於啓⽤狀態,但也能夠 明確啓⽤該功能。若是您的應⽤僅使⽤標準視圖和 Dr(w(ble,則全局啓⽤硬件加速 不會形成任何不良繪製效果。

您能夠在如下級別控制硬件加速:

  • 應⽤
  • Activity
  • 窗⼝
  • 視圖

應⽤級別

在 Android 清單⽂件中,將如下屬性添加到 標記中,爲整個應⽤啓⽤硬件加速:

<application android:hardwareAccelerated="true" ...>
複製代碼

Activity 級別

若是全局啓用硬件加速後,您的應用沒法正常運行,則您也能夠針對各個 Activity 控制硬件加速。要在 Activity 級別啓用或停用硬件加速,您可使用 元素的 android:hardwareAccelerated 屬性。如下示例展現瞭如何爲整個應用啓用硬件加速,但爲一個 Activity 停用硬件加速:

<application android:hardwareAccelerated="true">
	<activity ... />
	<activity android:hardwareAccelerated="false" />
</application>
複製代碼

窗口級別

若是您須要實現更精細的控制,可使用如下代碼爲給定窗口啓用硬件加速:

getWindow().setFlags(
	WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
複製代碼

視圖級別

您可使用如下代碼在運行時爲單個視圖停用硬件加速:

myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
複製代碼

繪製流程上面的優化

在說完以上CPU和GPU的特性,以及Android如何控制硬件加速以後,來看看在界面View在刷新和繪製過程當中,具體是哪些關鍵點來作了硬件加速的控制。

硬件加速狀況下Canvas生成的是DisplayListCanvas對象

在View的繪製過程當中會沿着draw(canvas,parent,drawingTime) --> draw(canvas) --> onDraw --> dispachDraw 這條遞歸路徑往下走,在draw(canvas,parent,drawingTime)中,若是是硬件加速狀況下,會走updateDisplayListIfDirty()。

if (drawingWithRenderNode) {
    renderNode = updateDisplayListIfDirty();
    if (!renderNode.isValid()) {
        // ...
        renderNode = null;
        drawingWithRenderNode = false;
    }
}
複製代碼

在updateDisplayListIfDirty()方法中,生成了DisplayListCanvas對象,而後調用draw(canvas)方法,此時Canvas已經不是普通的Canvas了,而是DisplayListCanvas。

final DisplayListCanvas canvas = renderNode.start(width, height);
canvas.setHighContrastText(mAttachInfo.mHighContrastText);

try {
        //...
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }
        } else {
            draw(canvas);
        }
    }
} finally {
    renderNode.end(canvas);
    setDisplayListProperties(renderNode);
}
複製代碼

在調用DisplayListCanvas的drawXXX方法過程當中,並無執行真正的繪製,而是用構建DisplayList,並保存在RenderNode中,經過renderNode.end(canvas)進行該操做。

/** * Ends the recording for this display list. A display list cannot be * replayed if recording is not finished. Calling this method marks * the display list valid and {@link #isValid()} will return true. * * @see #start(int, int) * @see #isValid() */
public void end(DisplayListCanvas canvas) {
    long displayList = canvas.finishRecording();
    nSetDisplayList(mNativeRenderNode, displayList);
    canvas.recycle();
}
複製代碼

JNI層將DisplayListCanvas的操做轉化成DisplayList

一個RenderNode包含若干個DisplayList,一般一個RenderNode對應一個View,包含View自身及其子View的全部DisplayList。

在ViewRootImpl中,若是是支持硬件加速,會執行mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this)方法。

// ViewRootImpl.java
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
    mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
    //...
}
複製代碼

ThreadedRenderer的draw過程,會調用到updateRootDisplayList,而後調用updateViewTreeDisplayList,此過程會更新整個View樹的DisplayList,最終執行ThreadedRenderer.nSyncAndDrawFrame來啓動線程對DisplayList進行最終的繪製操做

// ThreadedRenderer.java
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
    ...
    updateRootDisplayList(view, callbacks);
    ...
    int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);

}
複製代碼

因爲繪製流程的不一樣,硬件加速在界面內容發生重繪的時候繪製流程能夠獲得優化,避免了一些重複操做,從而大幅提高繪製效率。在updateDisplayListIfDirty方法從名字能夠看出,更新Displaylist是發生在髒區域。

在硬件加速關閉時,繪製內容會被 CPU 轉換成實際的像素,而後直接渲染到屏幕。具體來講,這個「實際的像素」,它是由 Bitmap 來承載的。在界面中的某個 View 因爲內容發生改變而調用 invalidate() 方法時,若是沒有開啓硬件加速,那麼爲了正確計算 Bitmap 的像素,這個 View 的父 View、父 View 的父 View 乃至一直向上直到最頂級 View,以及全部和它相交的兄弟 View,都須要被調用 invalidate()來重繪。一個 View 的改變使得大半個界面甚至整個界面都重繪一遍,這個工做量是很是大的。

而在硬件加速開啓時,前面說過,繪製的內容會被轉換成 GPU 的操做保存下來(承載的形式稱爲 display list,對應的類也叫作 DisplayList),再轉交給 GPU。因爲全部的繪製內容都沒有變成最終的像素,因此它們之間是相互獨立的,那麼在界面內容發生改變的時候,只要把發生了改變的 View 調用 invalidate() 方法以更新它所對應的 GPU 操做就好,至於它的父 View 和兄弟 View,只須要保持原樣。那麼這個工做量就很小了。

image-20210322002222220.png

在默認狀況下,View的clipChildren的屬性爲true,即每一個View繪製區域不能超出其父View的範圍。若是設置一個頁面根佈局的clipChildren屬性爲false,則子View能夠超出父View的繪製區域。

上圖中右邊的ViewGroup的clipChildren屬性即爲false,其子View的繪製超出了自身範圍。

當一個View觸發invalidate,且沒有播放動畫、沒有觸發layout的狀況下:

  • 對於全不透明的View,其自身會設置標誌位PFLAG_DIRTY,其父View會設置標誌位PFLAG_DIRTY_OPAQUE。在draw(canvas)方法中,只有這個View自身重繪。

    上圖中的TextView背景全不透明,此時調用TextView的invalidate,自由其自身重繪製。

  • 對於可能有透明區域的View,其自身和父View都會設置標誌位PFLAG_DIRTY

    • clipChildren爲true時,髒區會被轉換成ViewRoot中的Rect,刷新時層層向下判斷,當View與髒區有重疊則重繪。若是一個View超出父View範圍且與髒區重疊,但其父View不與髒區重疊,這個子View不會重繪。

      上圖中若是TextView背景透明,那麼髒區域會從TextView投影至ViewRoot上面的Rect,也就是綠色區域,此時在這個投影之上的View都會被觸發繪製(即TextView、左邊ViewGroup、ViewRoot)

    • clipChildren爲false時,ViewGroup.invalidateChildInParent()中會把髒區擴大到自身整個區域,因而與這個區域重疊的全部View都會重繪。

      上圖中右邊ViewGroup的clipChildren爲false,那麼髒區域即爲右邊ViewGroup的整個投影的區域,與此重疊的View都會被從新繪製。

實際問題解決

背景中提到的遊戲內嵌web頁面卡頓的問題, 在開啓硬件加速後卡頓的問題解決了。可是因爲對遊戲引擎不瞭解,因此不清楚開啓硬件加速是否會影響到遊戲的運行,好比遊戲界面繪製會不會錯亂,耗電量會不會增長等等問題。其實從上面第三小節提到的,Android不只僅能夠對整個應用開啓硬件加速還支持對單個Activity進行設置,那麼這個問題其實就很好解決了, 直接將顯示Web的頁面寫成一個Activity單獨開啓硬件急速便可,這樣徹底不會影響到遊戲。

總結

在硬件加速過程當中,充分利用CPU和GPU各自的特性,將邏輯部分交個CPU處理,將大量的數學計算交個GPU處理,在硬件層面對整個過程效率提高。界面刷新過程當中,CPU只會更新須要重繪的DisplayList,進一步提升繪製效率。

上文的介紹只是在一些關鍵的代碼上面作了講解,未深刻介紹細節;對於DisplayListCanvas如何生成DisplsyList的過程,屬於JNI層的操做,文檔並未涉及。對於RenderNode是如何組織DiaplayList,以及View樹的各個RenderNode是如何組織的都未闡明,文章充分借鑑了一些大神文章的內容,文末有參考連接,須要深刻了解能夠自行查閱。

結束語

過程當中有問題或者須要交流的同窗,能夠添加微信號AndroidAssistant37,而後進羣進行問題和技術的交流等。

參考連接

  1. juejin.cn/post/684490…
  2. developer.android.com/guide/topic…
  3. www.codeleading.com/article/337…
  4. juejin.cn/post/684490…
相關文章
相關標籤/搜索