Android性能優(yōu)化15 --- 大圖做幀動(dòng)畫卡??jī)?yōu)化幀動(dòng)畫之 SurfaceView滑動(dòng)窗口式幀復(fù)用

對(duì)比圖片解析速度

對(duì)于素材在 100k 以下的幀動(dòng)畫,上一篇的逐幀解析方案完全能夠勝任。但如果素材是幾百k,時(shí)間性能就不如預(yù)期。
掘友“小前鋒”問:“你的方案有測(cè)試過大圖嗎?比如1024768px”*

在逐幀解析SurfaceView上試了下這個(gè)大小的幀動(dòng)畫,雖然播放過程很連續(xù),但 600ms 的幀動(dòng)畫被放成了 1s。因?yàn)轭A(yù)定義的每幀播放時(shí)間被解碼時(shí)間拉長(zhǎng)了。

有沒有比BitmapFactory.decodeResource()更快的解碼方式?

于是乎對(duì)比了各種圖片解碼的速度,其中包括BitmapFactory.decodeStream()、BitmapFactory.decodeResource()、并分別將圖片放到res/raw、res/drawable、及assets,還在 GitHub 上發(fā)現(xiàn)了RapidDecoder這個(gè)庫(kù)(興奮不已?。?。自定義了測(cè)量函數(shù)執(zhí)行時(shí)間的工具類:

public class MethodUtil {
  //測(cè)量并打印單次函數(shù)執(zhí)行耗時(shí)
  public static long time(Runnable runnable) {
  long start = SystemClock.elapsedRealtime();
  runnable.run();
  long end = SystemClock.elapsedRealtime();
  long span = end - start;
  Log.v(“ttaylor”, “MethodUtil.time()” + " time span = " + span + " ms");
  return span;
  }
}

public class NumberUtil {
  private static long total;
  private static int times;
  private static String tag;

  //統(tǒng)計(jì)并打印多次執(zhí)行時(shí)間的平均值
  public static void average(String tag, Long l) {
    if (!TextUtils.isEmpty(tag) && !tag.equals(NumberUtil.tag)) {
      reset();
      NumberUtil.tag = tag;
  }
  times++;
  total += l;
  int average = total / times ;
  Log.v(“ttaylor”, "Average.average() " + NumberUtil.tag + " average = " + average);
}

  private static void reset() {
    total = 0;
    times = 0;
  }
} 

經(jīng)多次測(cè)試取平均值,執(zhí)行時(shí)間最長(zhǎng)的是BitmapFactory.decodeResource(),最短的是用BitmapFactory.decodeStream()解析assets圖片,后者只用了前者一半時(shí)間。而RapidDecoder庫(kù)的時(shí)間介于兩者之間(失望至極~),不過它提供了一種邊解碼邊繪制的技術(shù)號(hào)稱比先解碼再繪制要快,還沒來得及試。

雖然將解碼時(shí)間減半了,但解碼一張 1MB 圖片還是需要 60+ms,仍不能滿足時(shí)間性能要求。

獨(dú)立解碼線程

現(xiàn)在的矛盾是 圖片解析速度 慢于 圖片繪制速度,如果解碼和繪制在同一個(gè)線程串行的進(jìn)行,那解碼勢(shì)必會(huì)拖慢繪制效率。
可不可以將解碼圖片放在一個(gè)單獨(dú)的線程中進(jìn)行?
在上一篇FrameSurfaceView的基礎(chǔ)上新增了獨(dú)立解碼線程:

public class FrameSurfaceView extends BaseSurfaceView {
    …
    //獨(dú)立解碼線程
    private HandlerThread decodeThread;
    //解碼算法寫在這里面
    private DecodeRunnable decodeRunnable;

    //播放幀動(dòng)畫時(shí)啟動(dòng)解碼線程
    public void start() {
        decodeThread = new HandlerThread(DECODE_THREAD_NAME);
        decodeThread.start();
        handler = new Handler(decodeThread.getLooper());
        handler.post(decodeRunnable);
    }

    private class DecodeRunnable implements Runnable {

        @Override
        public void run() {
            //在這里解碼
        }
    }
} 

這樣一來,基類中有獨(dú)立的繪制線程,而子類中有獨(dú)立的解碼線程,解碼速度不再影響繪制速度。

新的問題來了:圖片被解碼后存放在哪里?

生產(chǎn)者 & 消費(fèi)者

存放解碼圖片的容器,會(huì)被兩個(gè)線程訪問,繪制線程從中取圖片(消費(fèi)者),解碼線程往里存圖片(生產(chǎn)者),需考慮線程同步。第一個(gè)想到的就是LinkedBlockingQueue,于是乎在FrameSurfaceView中新增了大小為 1 的阻塞隊(duì)列及存取操作:

public class FrameSurfaceView extends BaseSurfaceView {
    …
    //解析隊(duì)列:存放已經(jīng)解析幀素材
    private LinkedBlockingQueue decodedBitmaps = new LinkedBlockingQueue<>(1);
    //記錄已繪制的幀數(shù)
    private int frameIndex ;

    //存解碼圖片
    private void putDecodedBitmap(int resId, BitmapFactory.Options options) {
        Bitmap bitmap = decodeBitmap(resId, options);
        try {
            decodedBitmaps.put(bitmap);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //取解碼圖片
    private Bitmap getDecodedBitmap() {
        Bitmap bitmap = null;
        try {
            bitmap = decodedBitmaps.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    //解碼圖片
    private Bitmap decodeBitmap(int resId, BitmapFactory.Options options) {
        options.inScaled = false;
        InputStream inputStream = getResources().openRawResource(resId);
        return BitmapFactory.decodeStream(inputStream, null, options);
    }

    private void drawOneFrame(Canvas canvas) {
        //在繪制線程中取解碼圖片并繪制
        Bitmap bitmap = getDecodedBitmap();
        if (bitmap != null) {
        canvas.drawBitmap(bitmap, srcRect, dstRect, paint);
        }
        frameIndex++;
    }

    private class DecodeRunnable implements Runnable {
        private int index;
        private List bitmapIds;
        private BitmapFactory.Options options;

        public DecodeRunnable(int index, List bitmapIds, BitmapFactory.Options options) {
            this.index = index;
            this.bitmapIds = bitmapIds;
            this.options = options;
        }

        @Override
        public void run() {
            //在解碼線程中解碼圖片
            putDecodedBitmap(bitmapIds.get(index), options);
            index++;
            if (index < bitmapIds.size()) {
                handler.post(this);
            } else {
                index = 0;
            }
        }
    }
} 
  • 繪制線程在每次繪制之前調(diào)用阻塞的take()從解析隊(duì)列的隊(duì)頭拿幀圖片,解碼線程不斷地調(diào)用阻塞的put()往解析隊(duì)列的隊(duì)尾存幀圖片。
  • 雖然assets目錄下的圖片解析速度最快,但res/raw目錄的速度和它相差無幾,為了簡(jiǎn)單起見,這里使用了openRawResource讀取res/raw中的圖片。
  • 雖然解碼和繪制分別在不同線程,但如果存放解碼圖片容器大小為 1 ,繪制進(jìn)程必須等待解碼線程,繪制速度還是會(huì)被解碼速度拖累,看似互不影響的兩個(gè)線程,其實(shí)相互牽制。

滑動(dòng)窗口機(jī)制 & 預(yù)解析

為了讓速度不同的生產(chǎn)者和消費(fèi)者更流暢的協(xié)同工作,必須為速度較快的一方提供緩沖。
就好像 TCP 擁塞控制中的滑動(dòng)窗口機(jī)制,發(fā)送方產(chǎn)生報(bào)文的速度快于接收方消費(fèi)報(bào)文的速度,遂發(fā)送方不必等收到前一個(gè)報(bào)文的確認(rèn)再發(fā)送下一個(gè)報(bào)文。

對(duì)于當(dāng)前 case ,需要將存放圖片容器增大,并在幀動(dòng)畫開始前預(yù)解析前幾幀存入解析隊(duì)列。

public class FrameSurfaceView extends BaseSurfaceView {
    …
    //下一個(gè)該被解析的素材索引
    private int bitmapIdIndex;
    //幀動(dòng)畫素材容器
    private List bitmapIds = new ArrayList<>();
    //大小為3的解析隊(duì)列
    private LinkedBlockingQueue decodedBitmaps = new LinkedBlockingQueue<>(3);

    //傳入幀動(dòng)畫素材
    public void setBitmapIds(List bitmapIds) {
        if (bitmapIds == null || bitmapIds.size() == 0) {
            return;
        }
        this.bitmapIds = bitmapIds;
        preloadFrames();
    }

    //預(yù)解析前幾幀
    private void preloadFrames() {
        //解析一幀并將圖片入解析隊(duì)列
        putDecodedBitmap(bitmapIds.get(bitmapIdIndex++), options);
        putDecodedBitmap(bitmapIds.get(bitmapIdIndex++), options);
    }
} 

獨(dú)立解碼線程、滑動(dòng)窗口機(jī)制、預(yù)加載都已 code 完畢。運(yùn)行一把代碼(坐等驚喜~)。

居然流暢的播起來了!興奮的我忍不住播了好幾次。。。打開內(nèi)存監(jiān)控一看(頭頂豎下三條線),一夜回到解放前:每播放一次,內(nèi)存中就會(huì)新增 N 個(gè)Bitmap對(duì)象(N為幀動(dòng)畫總幀數(shù))。

原來重構(gòu)過程中,將解碼時(shí)的幀復(fù)用邏輯去掉了。當(dāng)前 case 中,幀復(fù)用也變得復(fù)雜起來。

復(fù)用隊(duì)列

當(dāng)解碼和繪制是在一個(gè)線程中串行進(jìn)行,且只有一幀被復(fù)用,只需這樣寫代碼就能實(shí)現(xiàn)幀復(fù)用:

private void drawOneFrame(Canvas canvas) {
    frameBitmap = BitmapFactory.decodeResource(getResources(), bitmaps.get(bitmapIndex), options);
    //復(fù)用上一幀Bitmap的內(nèi)存
    options.inBitmap = frameBitmap;
    canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);
    bitmapIndex++;
}  
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容