你們好,我叫大聖;java
本人於2018年5月加入37手遊安卓團隊,曾經就任於愛拍等互聯網公司;android
目前是37手遊安卓團隊的國內負責人,主要負責相關業務開發和一些平常業務統籌等。web
最近在處理一個遊戲內嵌社區頁面卡頓問題的過程當中,發如今Manifest的Activity中有將硬件加速關閉的配置,若是將其開啓,那麼頁面卡頓的問題就解決了。對這一塊的知識還不是很瞭解,也不知道直接打開硬件加速對遊戲是否有影響。因而乎在網上查了一波別人的文章,作了一個簡單的總結。canvas
在瞭解什麼是硬件加速以前,先了解一下什麼是CPU和GPU。 CPU(Centr(l Processing Unit,中央處理器)是計算機設備核⼼器件,⽤於執⾏ 程序代碼,軟件開發者對此都很熟悉;GPU(Gr(phics Processing Unit,圖形處理 器)主要⽤於處理圖形運算,一般所說「顯卡」的核⼼部件就是GPU。緩存
從上面的結構圖能夠看出,CPU的控制器較爲複雜,擅長各類複雜的邏輯運算;GPU的控制器比較簡單,但包含了大量ALU,擅長大量的數學運算。微信
硬件加速的主要原理,就是經過底層軟件代碼,將CPU不擅長的圖形計算轉換成GPU專用指令,由GPU完成。markdown
顯卡和GPU不是同一個概念,顯卡指的是包含GPU的一個完整的卡,GPU是單指顯卡的核心芯片,是一顆芯片;
據說比特幣挖礦的機器都是採用GPU運算的,是否是和上面的原理同樣,挖礦是大量的數學計算,GPU更加擅長呢?app
接下來看看Android中如何使用硬件加速的。ide
從 Android 3.0(API 級別 11)開始,Android 2D 渲染管道⽀持硬件加速,也 就是說,在 View 的畫布上執⾏的全部繪製操做都會使⽤ GPU。啓⽤硬件加速須要 更多資源,所以應⽤會佔⽤更多內存。oop
若是您的⽬標 API 級別爲 14 及更⾼級別,則硬件加速默認處於啓⽤狀態,但也能夠 明確啓⽤該功能。若是您的應⽤僅使⽤標準視圖和 Dr(w(ble,則全局啓⽤硬件加速 不會形成任何不良繪製效果。
您能夠在如下級別控制硬件加速:
在 Android 清單⽂件中,將如下屬性添加到 標記中,爲整個應⽤啓⽤硬件加速:
<application android:hardwareAccelerated="true" ...>
複製代碼
若是全局啓用硬件加速後,您的應用沒法正常運行,則您也能夠針對各個 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,只須要保持原樣。那麼這個工做量就很小了。
在默認狀況下,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
,而後進羣進行問題和技術的交流等。
參考連接