Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自繪動畫實現與優化實戰——以Tencent OS錄音機波形動

Android自繪動畫實現與優化實戰——以Tencent OS錄音機波形動

編輯:關於Android編程

前言

我們所熟知的,Android 的圖形繪制主要是基於 View 這個類實現。 每個 View 的繪制都需要經過 onMeasure、onLayout、onDraw 三步曲,分別對應到測量大小、布局、繪制。

Android 系統為了簡化線程開發,降低應用開發的難度,將這三個過程都放在應用的主線程(UI 線程)中執行,以保證繪制系統的線程安全。

這三個過程通過一個叫 Choreographer 的定時器來驅動調用更新, Choreographer 每16ms被 vsync 這個信號喚醒調用一次,這有點類似早期的電視機刷新的機制。在 Choreographer 的 doFrame 方法中,通過樹狀結構存儲的 ViewGroup,依次遞歸的調用到每個 View 的 onMeasure、onLayout、onDraw 方法,從而最後將每個 View 都繪制出來(當然最後還會經過 SurfaceFlinger 的類來將 View 合成起來顯示,實際過程很復雜)。

同時每個 View 都保存了很多標記值 flag,用來判斷是否該 View 需要重新被 Measure、Layout、Draw。 這樣對於那些沒有變化,不需要重繪的 View,則不再調用它們的方法,從而能夠提高繪制效率。

Android 為了方便開發者進行動畫開發,提供了好幾種動畫實現的方式。 其中比較常用的是屬性動畫類(ObjectAnimator),它通過定時以一定的曲線速率來改變 View 的一系列屬性,最後產生 View 的動畫的效果。比較常見的屬性動畫能夠動態的改變 View 的大小、顏色、透明度、位置等值,此種方式實現的效率比較高,也是官方推薦的動畫形式。

為了進一步的提升動畫的效率,防止每次都需要多次調用 onMeasure、onLayout、onDraw,重新繪制 View 本身。 Android 還提出了一個層 Layer 的概念。

通過將 View 保存在圖層中,對於平移、旋轉、伸縮等動畫,只需要對該層進行整體變化,而不再需要重新繪制 View 本身。 層 Layer 又分為軟繪層(Software Layer)和硬繪層(Harderware Layer) 。它們可以通過 View 類的 setLayerType(layerType, paint);方法進行設置。軟繪層將 View 存儲成 bitmap,它會占用普通內存;而硬繪層則將 View 存儲成紋理(Texture),占用 GPU 中的存儲。 需要注意的是,由於將 View 保存在圖層中,都會占用相應的內存,因此在動畫結束之後需要重新設置成LAYER_ TYPE_ NONE,釋放內存。

由於普通的 View 都處於主線程中,Android 除了繪制之外,在主線程中還需要處理用戶的各種點擊事件。很多情況,在主線程中還需要運行額外的用戶處理邏輯、輪詢消息事件等。 如果主線程過於繁忙,不能及時的處理和響應用戶的輸入,會讓用戶的體驗急劇降低。如果更嚴重的情況,當主線程延遲時間達到5s的時候,還會觸發 ANR(Application Not Responding)。 這樣當界面的繪制和動畫比較復雜,計算量比較大的情況,就不再適合使用 View 這種方式來繪制了。

Android 考慮到這種場景,提出了 SurfaceView 的機制。SurfaceView 能夠在非 UI 線程中進行圖形繪制,釋放了 UI 線程的壓力。SurfaceView 的使用方法一般是復寫一下三種方法:

   public void surfaceCreated(SurfaceHolder holder);
   public void surfaceChanged(SurfaceHolder holder, int format, int width,
                              int height);
   public void surfaceDestroyed(SurfaceHolder holder);

surfaceCreated 在 SurfaceView 被創建的時候調用, 一般在該方法中創建繪制線程,並啟動這個線程。

surfaceDestroyed 在 SurfaceView 被銷毀的時候調用,在該方法中設置標記位,讓繪制線程停止運行。

繪制子線程中,一般是一個 while 循環,通過判斷標記位來決定是否退出該子線程。 使用 sleep 函數來定時的調起繪制邏輯。 通過 mHolder.lockCanvas()來獲得 canvas,繪制完畢之後調用 mHolder.unlockCanvasAndPost(canvas);來上屏。 這裡特別要注意繪制線程和 surfaceDestroyed 中需要加鎖。否則會有 SurfaceView 被銷毀了,但是繪制子線程中還是持有對 Canvas 的引用,而導致 crash。下面是一個常用的框架:

private final Object mSurfaceLock = new Object();
private DrawThread mThread;
@Override
public void surfaceCreated(SurfaceHolder holder) {
    mThread = new DrawThread(holder);
    mThread.setRun(true);  
    mThread.start();
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
                           int height) {
    //這裡可以獲取SurfaceView的寬高等信息
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    synchronized (mSurfaceLock) {  //這裡需要加鎖,否則doDraw中有可能會crash
        mThread.setRun(false);
    }
}

private class DrawThread extends Thread {
    private SurfaceHolder mHolder;
    private boolean mIsRun = false;

    public DrawThread(SurfaceHolder holder) {
        super(TAG);
        mHolder = holder;
    }

    @Override
    public void run() {
        while(true) {
            synchronized (mSurfaceLock) {
                if (!mIsRun) {
                    return;
                }
                Canvas canvas = mHolder.lockCanvas();
                if (canvas != null) {
                    doDraw(canvas);  //這裡做真正繪制的事情
                    mHolder.unlockCanvasAndPost(canvas);
                }
            }
            Thread.sleep(SLEEP_TIME);
        }
    }

    public void setRun(boolean isRun) {
        this.mIsRun = isRun;
    }
}

Android 為繪制圖形提供了 Canvas 類,可以理解這個類是一塊畫布,它提供了在畫布上畫不同圖形的方法。它提供了一系列的繪制各種圖形的 API, 比如繪制矩形、圓形、橢圓等。對應的 API 都是 drawXXX的形式。

不規則的圖形的繪制比較特殊,它同於規則圖形已有繪制公式的情況,它有可能是任意的線條組成。Canvas 為畫不規則形狀,提供了 Path 這個類。通過 Path 能夠記錄各種軌跡,它可以是點、線、各種形狀的組合。通過 drawPath 這個方法即可繪制出任意圖形。

有了畫布 Canvas 類,提供了繪制各種圖形的工具之後,還需要指定畫筆的顏色,樣式等屬性,才能有效的繪圖。Android 提供了 Paint 這個類,來抽象畫筆。 通過 Paint 可以指定繪制的顏色,是否填充,如果處理交集等屬性。

動畫實現

既然是實戰,當然要有一個例子啦。 這裡以 TOS 裡面的錄音機的波形動效實現為例。 首先看一下設計獅童鞋給的視覺設計圖:

\

下面是動起來的效果圖:

看到這麼高大上的動效圖,不得不贊歎一下設計獅童鞋,但同時也深深的捏了把汗——這個動畫要咋實現捏。

粗略的看一下上面的視覺圖。 感覺像是多個正弦曲線組成。 每條正弦線好像中間高,兩邊低,應該有一個對稱的衰減系數。 同時有兩組上下對稱的正弦線,在對稱的正弦線中間采用漸變顏色來進行填充。然後看動效的效果圖,好像這個不規則的正弦曲線有一個固定的速率向前在運動。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPr+0wLTOqsHLyrXP1tXiuPa2r9CnzbyjrLu5tcOw0ba80tG+rbu5uPjAz8qmtcTEx7Xjv8nBr7XEyv3Rp9aqyra88cbwwLSho8/Cw+bKx9X9z9LH+s/ftcS5q8q9o7o8L3A+DQo8cD55PUFzaW6jqCZvbWVnYTt4KyZwaGk7o6krazwvcD4NCjxwPkEgtPqx7bXEysfV8bf5o6y21NOmtcSyqLflus2yqLnItcS437bIo6y8tCB5INbhyc+1xL7gwOujuyZvbWVnYTsgyse9x8vZtsijrLu7s8nGtcLKyscgMiZwaTtmo6zE3Lm7v9jWxrKo0M61xL/ttsijuyZwaGk7IMrHs/XKvM/gzrujrMTcubu+9rao1f3P0sf6z9+1xLP1yrwgeCDW4c671sOju2sgysfGq77go6zE3Lm7v9jWxtTaIHkg1uHJz7XExqvSxsG/PC9wPg0KPHA+zqrBy8Tcubu4/LzT1rG526Osvau5q8q9zbzQzruvtcTP1Mq+s/bAtKOs1eLA78e/wdLNxrz20ru49s341b6jujxhIGhyZWY9"https://www.desmos.com/calculator">https://www.desmos.com/calculator ,它能將輸入的公式轉換成坐標圖。這正是我們需要的。比如 sin(0.75πx - 0.5π) 對應的圖形是下圖:

\

與上面設計圖中的相比,還需要乘上一個對稱的衰減函數。 我們挑選了如下的衰減函數425/(4+x4):

將sin(0.75πx - 0.5π) 乘以這個衰減函數 425/(4+x4),然後乘以0.5。 最後得出了下圖:

看起來這個曲線與視覺圖中的曲線已經很像了,無非就是多加幾個算法類似,但是相位不同的曲線罷了。 如下圖:

看看,用了我們足(quan)夠(bu)強(wang)大(ji)的數學知識之後, 我們好像也創造出來了類似視覺稿中的波形了。

接下來,我們只需要在 SurfaceView 中使用 Path,通過上面的公式計算出一個個的點,然後畫直線連接起來就行啦! 於是我們得出了下面的實際效果(為了方便顯示,已將背景調成白色):

曲線畫出來了,然後要做的就是漸變色的填充了。 這也是視覺還原比較難實現的地方。

對於漸變填充,Android 提供了 LinearGradient 這個類。它需要提供起始點和終結點的坐標,以及起始點和終結點的顏色值:

public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,
             TileMode tile);

TileMode 包括了 CLAMP、REPEAT、MIRROR 三種模式。 它指定了,如果填充的區域超過了起始點和終結點的距離,顏色重復的模式。CLAMP 指使用終點邊緣的顏色,REPEAT 指重復的漸變,而MIRROR則指的是鏡像重復。

從 LinearGradient 的構造函數就可以預知,漸變填充的時候,一定要指定精確的起始點和終結點。否則如果漸變距離大於填充區域,會出現漸變不完整,而漸變距離小於填充區域則會出現多個漸變或填不滿的情況。如下圖所示:

\

圖中左邊是精確設置漸變起點和終點為矩形的頂部和底部; 圖中中間為設置的漸變起點為頂部,終點為矩形的中間; 右邊的則設置的漸變起點和終點都大於矩形的頂部和底部。代碼如下:

LinearGradient gradient = new LinearGradient(100, mHeight_2 - 200, 100, mHeight_2 + 200,
        line_1_start_color, region_1_end_color, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(100, mHeight_2 - 200, 300, mHeight_2 + 200, mPaint);

gradient = new LinearGradient(400, mHeight_2 - 200, 400, mHeight_2,
         line_1_start_color, region_1_end_color, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(400, mHeight_2 - 200, 600, mHeight_2 + 200, mPaint);

gradient = new LinearGradient(700, mHeight_2 - 400, 700, mHeight_2 + 400,
        line_1_start_color, region_1_end_color, Shader.TileMode.REPEAT);
mPaint.setShader(gradient);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(700, mHeight_2 - 200, 900, mHeight_2 + 200, mPaint);

對於矩形這種規則圖形進行漸變填充,能夠很容易設置漸變顏色的起點和終點。 但是對於上圖中的正弦曲線如果做到呢? 難道需要將一組正弦曲線的每個點上下連接,使用漸變進行繪制? 那樣計算量將會是非常巨大的!那又有其他什麼好的方法呢?

Paint 中提供了 Xfermode 圖像混合模式的機制。 它能夠控制繪制圖形與之前已經存在圖形的混合交疊模式。其中比較有用的是 PorterDuffXfermode 這個類。它有多種混合模式,如下圖所示:

\

這裡 canvas 原有的圖片可以理解為背景,就是 dst; 新畫上去的圖片可以理解為前景,就是 src。有了這種圖形混合技術,能夠完成各種圖形交集的顯示。

那我們是否可以腦洞大開一下,將上圖已經繪制好的波形圖,與漸變的矩形進行交集,將它們相交的地方畫出來呢。 它們相交的地方好像恰好就是我們需要的效果呢。

這樣,我們只需要先填充波形,然後在每組正弦線相交的封閉區域畫一個以波峰和波谷為高的矩形,然後將這個矩形染色成漸變色。以這個矩形與波形做出交集,選擇 SrcIn 模式,即能只顯示相交部分矩形的這一塊的顏色。 這個方案看起來可行,先試試。下面圖是沒有執行 Xfermode 的疊加圖, 從圖中可以看出,兩個正弦線中間的區域正是我們需要的!

下面是執行 SrcIn 模式混合之後的圖像:

神奇的事情出現了, 視覺圖中的效果被還原了。

我們再依葫蘆畫瓢,再繪制另外一組正弦曲線。 這裡需要注意的是,由於 Xfermode 中的 Dst 指的原有的背景,因此這裡兩組正弦線的混合會互相產生影響。 即第二組在調用 SrcIn 模式進行混合的時候,會將第一組的圖形進行剪切。如下圖所示:

因此在繪制的時候,必須將兩組正弦曲線分開單獨繪制在不同 Canvas 層上。 好在 Android 系統為我們提供了這個功能,Android 提供了不同 Canvas 層,以用於進行離屏緩存的繪制。我們可以先繪制一組圖形,然後調用 canvas.saveLayer 方法將它存在離屏緩存中,然後再繪制另外一組曲線。最後調用 canvas.restoreToCount(sc);方法恢復 Canvas,將兩屏混合顯示。最後的效果圖如下所示:

這裡總結一下繪制的順序:
1、計算出曲線需要繪制的點
2、填充出正弦線
3、在每組正弦線相交的地方,根據波峰波谷繪制出一個漸變線填充的矩形。並且設置圖形混合模式為 SrcIn

 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 

4、對正弦線進行描邊
5、離屏存儲 Canvas,再進行下一組曲線的繪制

靜態的繪制已經完成了。接下來就是讓它動起來了。 根據上面給出來的框架,在繪制線程中會定時執行 doDraw 方法。我們只需要在 doDraw 方法中每次將波形往前移動一個距離,即可達到讓波形往前移動的效果。具體對應到正弦公式中的 φ 值,每次只需要在原有值的基礎上修改這個值即能改變波形在 X 軸的位置。每次執行 doDraw 都會根據下面的計算方法重新計算圖形的初相值:

this.mPhase = (float) ((this.mPhase + Math.PI * mSpeed) % (2 * Math.PI));

在計算波形高度的時候,還可以乘以音量大小。即正弦公式中的 A 的值可以為 volume * 繪制的最大高度 * 425/(4+x4)。 這樣波形的振幅即能與音量正相關。波形可以隨著音量跳動大小。

動畫的優化

雖然上面已經實現了波形的動畫。但是如果以為工作已經結束了,那就真是太 sample,naive了。

現在手機的分辨率變的越來越大,一般都是1080p的分辨率。隨著分辨率的增加,圖形繪制所需要的計算量也越來越大(像素點多了)。這樣導致在某些低端手機中,或某些偽高端手機(比如某星S4)中,CPU 的計算能力不足,從而導致動畫的卡頓。 因此對於自繪動畫,可能還需要不斷的進行代碼和算法的優化,提高繪制的效率,盡量減少計算量。

自繪動畫優化的最終目的是減少計算量,降低 CPU 的負擔。為了達到這個目的,筆者總結歸納了以下幾種方法,如果大家有更多更好的方法,歡迎分享:

1、降低分辨率

在實際動畫繪制的過程中,如果對每個像素點的去計算(x,y)值,會導致大量的計算。但是這種密集的計算往往都是不需要的。 對於動畫,人的肉眼是有一定的容忍度的,在一定范圍內的圖形失真是無法察覺的,特別是那種一閃而過的東西更是如此。 這樣在實現的時候,可以都自己擬定一個比實際分辨率小很多的圖形密度,這個圖形密度上來計算 Y 值。然後將我們自己定義的圖形密度成比例的映射到真實的分辨率上。 比如上面繪制正弦曲線的時候,我們完全可以只計算100個點。然後將這60個點成比例的放在1024個點的X軸上。 這樣我們一下子便減少了接近10倍的計算量。這有點類似柵格化一副圖片。

由於采用了低密度的繪制,將這些低密度的點用直線連接起來,會產生鋸齒的現象,這樣同樣會對體驗產生影響。但是別怕,Android 已經為我們提供了抗鋸齒的功能。在 Paint 類中即可進行設置:

mPaint.setAntiAlias(true);

使用 Android 優化過了的抗鋸齒功能,一定會比我們每個點的去繪制效率更高。

通過動態調節自定義的繪制密度,在繪制密度與最終實現效果中找到一個平衡點(即不影響最後的視覺效果,同時還能最大限度的減少計算量),這個是最直接,也最簡單的優化方法。

2、減少實時計算量

我們知道在過去嵌入式設備中計算資源都是相當有限的,運行的代碼經常需要優化,甚至有時候需要在匯編級別進行。雖然現在手機中的處理器已經越來越強大,但是在處理動畫這種短時間間隔的大量運算,還是需要仔細的編寫代碼。 一般的動畫刷新周期是16ms,這也意味著動畫的計算需要盡可能的少做運算。

只要能夠減少實時計算量的事情,都應該是我們應該做的。那麼如何才能做到盡量少做實時運算呢? 一個比較重要的思維和方法是利用用空間來換取時間。一般我們在做自繪動畫的時候,會需要做大量的中間運算。而這些運算有可能在每次繪制定時到來的時候,產生的結果都是一樣的。這也意味著有可能我們重復的做出了需要冗余的計算。 我們可以將這些中間運算的結果,存儲在內存中。這樣下次需要的時候,便不再需要重新計算,只需要取出來直接使用即可。 比較常用的查表法即使利用這種空間換時間的方法來提高速度的。

具體針對本例而言, 在計算 425/(4+x4) 這個衰減系數的時候,對每個 X 軸上固定點來說,它的計算結果都是相同的。 因此我們只需要將每個點對應的 y 值存儲在一個數組中,每次直接從這個數組中獲取即可。這樣能夠節省出不少 CPU 在計算乘方和除法運算的計算量。 同樣道理,由於 sin 函數具有周期性,因此我們只需要將這個周期中的固定 N 個點計算出值,然後存儲在數組中。每次需要計算 sin 值的時候,直接從之前已經計算好的結果中找出近似的那個就可以了。 當然其實這裡計算 sin 不需要我們做這樣的優化,因為 Android 系統提供的 Math 方法庫中計算 sin 的方法肯定已經運用類似的原理優化過了。

CPU 一般都有一個特點,它在快速的處理加減乘運算,但是在處理浮點型的除法的時候,則會變的特別的慢,多要多個指令周期才能完成。因此我們還應該努力減少運算量,特別是浮點型的除法運算。 一般比較通用的做法是講浮點型的運算轉換成整型的運算,這樣對速度的提升也會比較明顯。 但是整型運算同時也意味著會丟失數據的精確度,這樣往往會導致繪制出來的圖形有鋸齒感。 之前有同事便遇到即使采用了 Android 系統提供的抗鋸齒方法,但是繪制出來的圖形鋸齒感還是很強烈,有可能就是數值計算中的精確度的問題,比如采用了不正確的整型計算,或者錯誤的四捨五入。 為了保證精確度,同時還能使用整型來進行運算,往往可以將需要計算的參數,統一乘上一個精確度(比如乘以100或者1000,視需要的精確范圍而定)取整計算,最後再將結果除以這個精確度。 這裡還需要注意整型溢出的問題。

3、減少內存分配次數

Android 在內存分配和釋放方面,采用了 JAVA 的垃圾回收 GC 模式。 當分配的內存不再使用的時候,系統會定時幫我們自動清理。這給我們應用開發帶來了極大的便利,我們從此不再需要過多的關注內存的分配與回收,也因此減少很多內存使用的風險。但是內存的自動回收,也意味著會消耗系統額外的資源。一般的 GC 過程會消耗系統ms級別的計算時間。在普通的場景中,開發者無需過多的關心內存的細節。但是在自繪動畫開發中,卻不能忽略內存的分配。

由於動畫一般由一個16ms的定時器來進行驅動,這意味著動畫的邏輯代碼會在短時間內被循環往復的調用。 這樣如果在邏輯代碼中在堆上創建過多的臨時變量,會導致內存的使用量在短時間穩步上升,從而頻繁的引發系統的GC行為。這樣無疑會拖累動畫的效率,讓動畫變得卡頓。

處理分析內存分配,減少不必要的分配呢, 首先我們需要先分析內存的分配行為。 對於Android內存的使用情況,Android Studio提供了很好用,直觀的分析工具。 為了更加直觀的表現內存分配的影響,在程序中故意創建了一些比較大的臨時變量。然後使用Memory Monitor工具得到了下面的圖:

\

並且在log中看到有頻繁的打印D/dalvikvm: GC_FOR_ALLOC freed 3777K, 18% free 30426K/36952K, paused 33ms, total 34ms

圖中每次漲跌的鋸齒意味著發生了一次GC,然後又分配了多個內存,這個過程不斷的往復。 從log中可以看到系統在頻繁的發起GC,並且每次GC都會將系統暫停33ms,這當然會對動畫造成影響。 當然這個是測試的比較極端的情況,一般來說,如果內存被更加穩定的使用的話,觸發GC的概率也會大大的降低,上面圖中的顛簸鋸齒出現到概率也會越低。

上面內存使用的情況,也被稱為內存抖動,它除了在周期性的調用過程中出現,另外一個高發場景是在for循環中分配、釋放內存。它影響的不僅僅是自繪動畫中,其他場景下也需要盡量避免。

從上圖中可以直觀的看到內存在一定時間段內分配和釋放的情況,得出是否內存的使用是否平穩。但是當出現問題之後,我們還需要借助 Allocation Tracker 這個工具來追蹤問題發生的原因,並最後解決它。Allocation Tracker 這個工具能夠幫助我們追蹤內存對象的分配和釋放情況,能夠獲取內存對象的來源。比如上面的例子,我們在一段時間內進行追蹤,可以得到如下圖:

從圖中我們可以看到大部分的內存分配都來自線程18 Thread 18,這也是我們的動畫的繪制線程。 從圖中可以看到主要的內存分配有以下幾個地方:
1、我們故意創建的臨時大數組
2、來自 getColor 函數, 它來自對 getResources().getColor()的調用,需要獲取從系統資源中獲取顏色資源。這個方法中會創建多個 StringBuilder 的變量
3、創建 Xfermode 的臨時變量,來自 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 這個調用。

4、創建漸變值的 LinearGradient gradient = new LinearGradient(getXPos(startX), startY, getXPos(startX), endY,
gradientStartColor, gradientEndColor, Shader.TileMode.REPEAT);

對於第2、3,這些變量完全不需要每次循環執行的時候,重復創建變量。 因為每次他們的使用都是固定的。可以考慮將它們從臨時變量轉為成員變量,在動畫初始化的同時也將這些成員變量初始化好。需要的時候直接調用即可。

而對於第4類這樣的內存分配,由於每次動畫中的波形形狀都不一樣,因此漸變色必現得重新創建並設值。因此這裡並不能將它作為成員變量使用。這裡是屬於必須要分配的。好在這個對象也不大,影響很小。

對於那些無法避免,每次又必須分配的大量對象,我們還能夠采用對象池模型的方式來分配對象。對象池來解決頻繁創建與銷毀的問題,但是這裡需要注意結束使用之後,需要手動釋放對象池中的對象。

經過優化的內存分配,會變得平緩很多。比如對於上面的例子。 去除上面故意創建的大量數組,以及優化了2、3兩個點之後的內存分配如下圖所示:

可以看出短時間內,內存並沒有什麼明顯的變化。並且在很長一段時間內都沒有觸發一次 GC

4、減少 Path 的創建次數

這裡涉及到對特殊規則圖形的繪制的優化。 Path 的創建也涉及到內存的分配和釋放,這些都是需要消耗資源的。並且對於越復雜的 Path,Canvas 在繪制的時候,也會更加的耗時。因此我們需要做的就是盡量優化 Path 的創建過程,簡化運算量。這一塊並沒有很多統一的標准方法,更多的是依靠經驗,並且將上面提到到的3點優化方法靈活運用。

首先 Path 類中本身即提供了數據結構重用的接口。它除了提供 reset 復位方法之外,還提供了 rewind 的方法。這樣每次動畫循環調用的時候,能夠做到不釋放之前已經分配的內存就能夠重用。這樣避免的內存的反復釋放和分配。特別是對於本例中,每次繪制的 Path 中的點都是一樣多的情況更加適用。

采用方法一種低密度的繪圖方法,同樣還能夠減少 Path 中線段的數量,這樣降低了 Path 構造的次數,同能 Canvas 在繪制 Path 的時候,由於 Path 變的簡單了,同樣能夠加快繪制速度。

特別的,對於本文中的波形例子。 視覺圖中給出來的效果圖,除了要用漸變色填充正弦線中間的區域之外。還需要對正弦線本身進行描邊。 同時一組正弦線中的上下兩根正弦線的顏色還不一樣。 這樣對於一組完整的正弦線的繪制其實需要三個步驟:
1、填充正弦線
2、描正弦線上邊沿
3、描正弦線下邊沿

如何很好的將這三個步驟組合起來,盡量減少 Path 的創建也很有講究。比如,如果我們直接按照上面列出來的步驟來繪制的話,首先需要創建一個同時包含上下正弦線的 Path,需要計算一遍上下正弦線的點,然後對這個 Path 使用填充的方式來繪制。 然後再計算一遍上弦線的點,創建只有上弦線的 Path,然後使用 Stroke 的模式來繪制,接著下弦線。 這樣我們將會重復創建兩邊 Path,並且還會重復一倍點坐標的計算量。

如果我們能采用上面步驟2中提到的,利用空間換取時間的方法。 首先把所有點位置都記在一個數組中,然後利用這些點來計算並繪制上弦線的 Path,然後保存下來;再計算和繪制下弦線的 Path 並保存。最後創建一個專門記錄填充區的 Path,利用 mPath.addPath();的功能,將之前的兩個 path 填充到該 Path 中。 這樣便能夠減少 Path 的計算量。同時將三個 Path 分別用不同的變量來記錄,這樣在下次循環到來的時候,還能利用 rewind 方法來進行內存重用。

這裡需要注意的是,Path 提供了 close的方法,來將一段線封閉。 這個函數能夠提供一定的方便。但是並不是每個時候都好用。有的時候,還是需要我們手動的去添加線段來閉合一個區域。比如下面圖中的情形,采用 close,就會導致中間有一段空白的區域:

\

5、優化繪制的步驟

什麼? 經過上面幾個步驟的優化,動畫還是卡頓?不要慌,這裡再提供一個精確分析卡頓的工具。 Android 還為我們提供了能夠追蹤監控每個方法執行時間的工具 TraceView。 它在 Android Device Monitor 中打開。比如筆者在開發過程中發現動畫有卡頓,然後用上面 TraceView 工具查看得到下圖:

\

發現 clapGradientRectAndDrawStroke 這個方法占用了72.1%的 CPU 時間,而這個方法中實際占用時間的是 drawPath。這說明此處的繪制存在明顯的缺陷與不合理,大部分的時間都用在繪制 clapGradientRectAndDrawStroke 上面了。那麼我們再看一下之前繪制的原理,為了能夠從矩形和正弦線之間剪切出交集,並顯示漸變區域。筆者做出了如下圖的嘗試:

首先繪制出漸變填充的矩形; 然後再將正弦線包裹的區域用透明顏色進行反向填充(白色區域),這樣它們交集的地方利用 SrcIn 模式進行剪切,這時候顯示出來便是白色覆蓋了矩形的區域(實際是透明色)加上它們未交集的地方(正弦框內)。這樣同樣能夠到達設計圖中給出的效果。代碼如下:

    mPath.rewind();
    mPath.addPath(mPathLine1);
    mPath.lineTo(getXPos(mDensity - 1), -mLineCacheY[mDensity - 1] + mHeight_2 * 2);
    mPath.addPath(mPathLine2);
    mPath.lineTo(getXPos(0), mLineCacheY[0]);

    mPath.setFillType(Path.FillType.INVERSE_WINDING);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setShader(null);
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    mPaint.setColor(getResources().getColor(android.R.color.transparent));
    canvas.drawPath(mPath, mPaint);
    mPaint.setXfermode(null);

雖然上面的代碼同樣也實現了效果,但是由於使用的反向填充,導致填充區域急劇變大。最後導致 canvas.drawPath(mPath, mPaint);調用占據了70%以上的計算量。

找到瓶頸點並知道原因之後,我們就能做出針對性的改進。 我們只需要調整繪制的順序,先將正弦線區域內做正向填充,然後再以 SrcIn 模式繪制漸變色填充的矩形。 這樣減少了需要繪制的區域,同時也達到預期的效果。

\

下面是改進之後 TraceView 的結果截圖:

從截圖中可以看到計算量被均分到不同的繪制方法中,已經沒有瓶頸點了,並且實測動畫也變得流暢了。 一般卡頓都能通過此種方法比較精確的找到真正的瓶頸點。

總結

本文主要簡單介紹了一下 Android 普通 View 和 SurfaceView 的繪制與動畫原理,然後介紹了一下錄音機波形動畫的具體實現和優化的方法。但是限於筆者的水平和經驗有限,肯定有很多纰漏和錯誤的地方。大家有更多更好的建議,歡迎一起分享討論,共同進步。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved