Android PicturePlayerView 基于TextureView的圖片播放器

前言

人生的第一篇技術(shù)文章,干了幾年程序員了踪古,從來(lái)都是看人家的,這幾天突然萌發(fā)了寫文章的念頭券腔,文筆不行請(qǐng)多多見(jiàn)諒伏穆。

現(xiàn)在做直播都需要做大禮物,然后UI扔給我一堆圖纷纫,要我放起來(lái)枕扫。最先想到的就是直接播放Gif,但是發(fā)現(xiàn)Android上沒(méi)有專門播放Gif的控件辱魁,我又找Gilde烟瞧,發(fā)現(xiàn)是可以播放了但是特別卡,要知道UI給我的圖有好幾MB全是高清的染簇,還不能壓縮丟色彩燕刻,最后沒(méi)辦法只能自己寫了。

代碼已上傳至Github

好吧剖笙,我現(xiàn)在發(fā)現(xiàn)了lottie-android卵洗,簡(jiǎn)直是痛哭流涕%>_<%

**首先附上2張效果圖,雖然我無(wú)恥的引用了lottie的Logo **


底不透明版

底透明版

正文

一開(kāi)始做這個(gè)控件的時(shí)候我用的是SurfaceView弥咪,但是我發(fā)現(xiàn)我無(wú)法將它放到中間的某一層过蹂,因?yàn)樗鼡碛歇?dú)立的繪圖表面,所以最終選用了TextureView聚至,需要注意的是TextureView必須在硬件加速開(kāi)啟的窗口中酷勺。如果你對(duì)它不熟悉的話可以參考《Android TextureView簡(jiǎn)易教程》

首先看一些關(guān)鍵的方法
  • setOpaque(boolean):該方法用于設(shè)置TextureView是否不透明扳躬。
  • lockCanvas():鎖定畫布脆诉,如果在不解除鎖定的情況下再次調(diào)用將返回null甚亭。
  • unlockCanvasAndPost(Canvas):解鎖畫布同時(shí)提交,在這句執(zhí)行完之后才可以再次調(diào)用lockCanvas()击胜。
  • canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint):將Bitmap畫到畫布上亏狰,srcdst作用就是將bitmap里的src區(qū)域畫到canvas里的dst區(qū)域。
  • canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR):這句的作用是清空畫布偶摔,也許你可以在View的onDraw()里試試這句暇唾,你會(huì)發(fā)現(xiàn)整個(gè)APP都是黑的o(╯□╰)o。
接下來(lái)讓我們先實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的構(gòu)造
//這是一個(gè)最簡(jiǎn)單的構(gòu)造辰斋,然而它什么都做不了策州,當(dāng)然我們可以把它蓋到任何層的上面,因?yàn)樗峭该鞯?public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {

    public PicturePlayerView(Context context) {
        this(context, null);
    }

    public PicturePlayerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PicturePlayerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        setOpaque(false);//設(shè)置背景透明宫仗,記住這里是[是否不透明]
        setSurfaceTextureListener(this);//設(shè)置監(jiān)聽(tīng)
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        //當(dāng)TextureView初始化時(shí)調(diào)用够挂,事實(shí)上當(dāng)你的程序退到后臺(tái)它會(huì)被銷毀,你再次打開(kāi)程序的時(shí)候它會(huì)被重新初始化
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        //當(dāng)TextureView的大小改變時(shí)調(diào)用
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        //當(dāng)TextureView被銷毀時(shí)調(diào)用
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        //當(dāng)TextureView更新時(shí)調(diào)用藕夫,也就是當(dāng)我們調(diào)用unlockCanvasAndPost方法時(shí)
    }
}

現(xiàn)在我們創(chuàng)建一個(gè)了PicturePlayerView下硕,接下來(lái)我們需要考慮如何將圖片繪制到TextureView上。

將圖片繪制到TextureView需要分2步走
  • 第一步:讀取圖片到內(nèi)存中
  • 第二步:將內(nèi)存中的圖片畫到畫布上汁胆,這里在畫完之后需要釋放Bitmap
    首先實(shí)現(xiàn)第一步:這里提供2種方法梭姓,為了方便在下面的代碼中將采用第二種,從Assets讀取圖片
//從本地讀取圖片嫩码,這里的path必須是絕對(duì)地址
private Bitmap readBitmap(String path) throws IOException {
    return BitmapFactory.decodeFile(path);
}
//從Assets讀取圖片
private Bitmap readBitmap(String path) throws IOException {
    return BitmapFactory.decodeStream(getResources().getAssets().open(path));
}

然后是第二步:

//將圖片畫到畫布上誉尖,圖片將被以寬度為比例畫上去
private void drawBitmap(Bitmap bitmap) {
    Canvas canvas = lockCanvas(new Rect(0, 0, getWidth(), getHeight()));//鎖定畫布
    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空畫布
    Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
    Rect dst = new Rect(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
    canvas.drawBitmap(bitmap, src, dst, mPaint);//將bitmap畫到畫布上
    unlockCanvasAndPost(canvas);//解鎖畫布同時(shí)提交
}

好了現(xiàn)在我們知道怎么讀取圖片和怎么將圖片畫到畫布上,但實(shí)際上我們擁有的是一組圖片铸题,并且在實(shí)際中需要將它們?cè)谝欢〞r(shí)間內(nèi)以一定的間隔播放出來(lái)铡恕。

很明顯TextureView比起正常的View的優(yōu)勢(shì)就是可以在異步將圖片畫到畫布上,我們可以創(chuàng)建一個(gè)異步線程丢间,然后通過(guò)SystemClock.sleep()這個(gè)函數(shù)在每畫完一幀都暫停一定時(shí)間探熔,這樣就實(shí)現(xiàn)了一個(gè)完整的過(guò)程。

完整代碼請(qǐng)看PicturePlayerView1

public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {

    private Paint mPaint;//畫筆
    private Rect mSrcRect;
    private Rect mDstRect;

    private int mPlayFrame;//當(dāng)前播放到那一幀烘挫,總幀數(shù)相關(guān)

    private String[] mPaths;//圖片絕對(duì)地址集合
    private int mFrameCount;//總幀數(shù)
    private long mDelayTime;//播放幀間隔

    private PlayThread mPlayThread;

    //... 省略構(gòu)造方法

    private void init() {
        setOpaque(false);//設(shè)置背景透明诀艰,記住這里是[是否不透明]

        setSurfaceTextureListener(this);//設(shè)置監(jiān)聽(tīng)

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//創(chuàng)建畫筆
        mSrcRect = new Rect();
        mDstRect = new Rect();
    }

    //... 省略SurfaceTextureListener的方法

    //開(kāi)始播放
    @Override
    public void start(String[] paths, long duration) {
        this.mPaths = paths;
        this.mFrameCount = paths.length;
        this.mDelayTime = duration / mFrameCount;

        //開(kāi)啟線程
        mPlayThread = new PlayThread();
        mPlayThread.start();
    }

    private class PlayThread extends Thread {
        @Override
        public void run() {
            try {
                while (mPlayFrame < mFrameCount) {//如果還沒(méi)有播放完所有幀
                    Bitmap bitmap = readBitmap(mPaths[mPlayFrame]);
                    drawBitmap(bitmap);
                    recycleBitmap(bitmap);
                    mPlayFrame++;
                    SystemClock.sleep(mDelayTime);//暫停間隔時(shí)間
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private Bitmap readBitmap(String path) throws IOException {
        return BitmapFactory.decodeStream(getResources().getAssets().open(path));
    }

    private void drawBitmap(Bitmap bitmap) {
        Canvas canvas = lockCanvas();//鎖定畫布
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空畫布
        mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());//這里我將2個(gè)rect抽離出去,防止重復(fù)創(chuàng)建
        mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
        canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//將bitmap畫到畫布上
        unlockCanvasAndPost(canvas);//解鎖畫布同時(shí)提交
    }

    private static void recycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }
}

上面的代碼實(shí)現(xiàn)一個(gè)完整的播放過(guò)程饮六,但實(shí)際運(yùn)行起來(lái)會(huì)有一定的問(wèn)題其垄,如果你運(yùn)行過(guò)就會(huì)發(fā)現(xiàn)Fps只有15-16幀,與我們要求的25幀相差甚遠(yuǎn)卤橄。

原因就在于

  1. 我們沒(méi)有計(jì)算readBitmap()等方法消耗的時(shí)間绿满。
  • sleep()實(shí)際上是不精準(zhǔn)的。

第一點(diǎn)我們之后再解決窟扑,先說(shuō)第二點(diǎn)喇颁,實(shí)際上在第一次開(kāi)發(fā)這個(gè)控件的時(shí)候我用的也是sleep()漏健,但是后來(lái)發(fā)現(xiàn)它跳動(dòng)的幅度很大。

比如我們以25幀為例橘霎,那么每幀的時(shí)間間隔應(yīng)該為40ms蔫浆,但在實(shí)際運(yùn)行中它有可能阻塞30ms,也有可能是50ms茎毁,當(dāng)然也有可能是40ms克懊,一開(kāi)始我也不明白忱辅,后來(lái)看到了這篇文章《Sleep函數(shù)的真正用意》七蜘,我才明白在非實(shí)時(shí)系統(tǒng)中是不可能有方法能完全精準(zhǔn)的阻塞的

之后我通過(guò)查看ValueAnimator的源碼最終發(fā)現(xiàn)了Choreographer墙懂,這里可以參考《Choreographer源碼解析》橡卤,研究了部分源碼后我發(fā)現(xiàn)它是利用Handler.sendMessageAtTime(long uptimeMillis)這個(gè)函數(shù)來(lái)控制時(shí)間,這個(gè)函數(shù)接收一個(gè)時(shí)間函數(shù)损搬,通過(guò)SystemClock.uptimeMillis()獲得碧库,事實(shí)上我們?cè)?code>Handler調(diào)用的send()函數(shù)大部分最終都會(huì)走到sendMessageAtTime()方法。

在這里有一篇非常好的文章《聊一聊Android的消息機(jī)制》巧勤,它里面就寫明了嵌灰。

Looper關(guān)心的細(xì)節(jié)

  1. 如果消息隊(duì)列里目前沒(méi)有合適的消息可以摘取,那么不能讓它所屬的線程“傻轉(zhuǎn)”颅悉,而應(yīng)該使之阻塞沽瞭。
  • 隊(duì)列里的消息應(yīng)該按其“到時(shí)”的順序進(jìn)行排列,最先到時(shí)的消息會(huì)放在隊(duì)頭剩瓶,也就是mMessages域所指向的消息驹溃,其后的消息依次排開(kāi)。
  • 阻塞的時(shí)間最好能精確一點(diǎn)兒延曙,所以如果暫時(shí)沒(méi)有合適的消息節(jié)點(diǎn)可摘時(shí)豌鹤,要考慮鏈表首個(gè)消息節(jié)點(diǎn)將在什么時(shí)候到時(shí),所以這個(gè)消息節(jié)點(diǎn)距離當(dāng)前時(shí)刻的時(shí)間差枝缔,就是我們要阻塞的時(shí)長(zhǎng)布疙。
  • 有時(shí)候外界希望隊(duì)列能在即將進(jìn)入阻塞狀態(tài)之前做一些動(dòng)作,這些動(dòng)作可以稱為idle動(dòng)作愿卸,我們需要兼顧處理這些idle動(dòng)作拐辽。一個(gè)典型的例子是外界希望隊(duì)列在進(jìn)入阻塞之前做一次垃圾收集。

看了這篇文章我是茅塞頓開(kāi)擦酌,事實(shí)上我通過(guò)測(cè)試發(fā)現(xiàn)俱诸,同樣是40ms,sendMessageAtTime()能保證間隔在39ms-41ms之間(正常情況下赊舶,如果手機(jī)卡頓就說(shuō)不準(zhǔn)了
)睁搭,所以我自己實(shí)現(xiàn)了一個(gè)Scheduler赶诊,這里我就不展開(kāi)了,就講下實(shí)現(xiàn)步驟园骆,有興趣可以直接看源碼舔痪。

具體步驟為

  1. 創(chuàng)建一個(gè)Thread
  • 初始化Looper锌唾,這里可以直接繼承HandlerThread锄码。
  • 創(chuàng)建一個(gè)Handler
  • 通過(guò)SystemClock.uptimeMillis()取得時(shí)間晌涕,然后向Handler發(fā)送消息滋捶。
  • 接收到消息后判斷是否結(jié)束,如果未結(jié)束則將當(dāng)前的時(shí)間加上間隔時(shí)間(比如40ms)后繼續(xù)發(fā)送消息余黎,不斷進(jìn)行循環(huán)過(guò)程重窟。
通過(guò)sendMessageAtTime()實(shí)現(xiàn)的播放器

完整代碼請(qǐng)看PicturePlayerView2

public class PicturePlayerView extends TextureView implements TextureView.SurfaceTextureListener {

    private Paint mPaint;//畫筆
    private Rect mSrcRect;
    private Rect mDstRect;

    private String[] mPaths;//圖片絕對(duì)地址集合

    private Scheduler mScheduler;

    //... 省略構(gòu)造方法

    private void init() {
        setOpaque(false);//設(shè)置背景透明,記住這里是[是否不透明]

        setSurfaceTextureListener(this);//設(shè)置監(jiān)聽(tīng)

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//創(chuàng)建畫筆
        mSrcRect = new Rect();
        mDstRect = new Rect();
    }

    //... 省略SurfaceTextureListener的方法

    //開(kāi)始播放
    @Override
    public void start(String[] paths, long duration) {
        this.mPaths = paths;

        //開(kāi)啟線程
        mScheduler = new Scheduler(duration, paths.length,
                new FrameUpdateListener());
        mScheduler.start();
    }

    private Bitmap readBitmap(String path) throws IOException {
        return BitmapFactory.decodeStream(getResources().getAssets().open(path));
    }

    private class FrameUpdateListener implements OnFrameUpdateListener {
        @Override
        public void onFrameUpdate(long frameIndex) {
            try {
                Bitmap bitmap = readBitmap(mPaths[(int) frameIndex]);
                drawBitmap(bitmap);
                recycleBitmap(bitmap);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void drawBitmap(Bitmap bitmap) {
        Canvas canvas = lockCanvas();//鎖定畫布
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空畫布
        mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());//這里我將2個(gè)rect抽離出去惧财,防止重復(fù)創(chuàng)建
        mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
        canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//將bitmap畫到畫布上
        unlockCanvasAndPost(canvas);//解鎖畫布同時(shí)提交
    }

    private static void recycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }
}

現(xiàn)在我們通過(guò)sendMessageAtTime()解決了第二步巡扇,如果你運(yùn)行過(guò)Demo就會(huì)發(fā)現(xiàn),現(xiàn)在已經(jīng)在大部分情況下都能保持在25fps左右垮衷。

好了厅翔,現(xiàn)在我們可以來(lái)解決第一個(gè)問(wèn)題,盡管現(xiàn)在在大部分情況下能保持25fps搀突,但是如果機(jī)子較差刀闷,或者運(yùn)行程序過(guò)多,你會(huì)發(fā)現(xiàn)還是不能保持25fps描姚,當(dāng)然如果機(jī)子實(shí)在太卡涩赢,連drawBitmap()這一步所花費(fèi)的時(shí)間都要超過(guò)40ms,那是實(shí)在沒(méi)有任何辦法轩勘,但如果應(yīng)該盡量去除多余的花費(fèi)筒扒,讓時(shí)間盡可能的讓給drawBitmap()

我們要如何做呢绊寻?很明顯我們需要新建一個(gè)線程將readBitmap()移到新線程中執(zhí)行花墩,然后通過(guò)一個(gè)緩存數(shù)組(多線程之間需要加鎖)進(jìn)行交互。

分離線程實(shí)現(xiàn)

完整代碼請(qǐng)看PicturePlayerView3

public class PicturePlayerView3 extends TextureView implements TextureView.SurfaceTextureListener {
    private static final int MAX_CACHE_NUMBER = 12;//這是代表讀取最大緩存幀數(shù)澄步,因?yàn)橐粡垐D片的大小有width*height*4這么大冰蘑,內(nèi)存吃不消

    private Paint mPaint;//畫筆
    private Rect mSrcRect;
    private Rect mDstRect;

    private List<Bitmap> mCacheBitmaps;//緩存幀集合

    private int mReadFrame;//當(dāng)前讀取到那一幀,總幀數(shù)相關(guān)

    private String[] mPaths;//圖片絕對(duì)地址集合
    private int mFrameCount;//總幀數(shù)

    private ReadThread mReadThread;
    private Scheduler mScheduler;

    //... 省略構(gòu)造方法

    private void init() {
        setOpaque(false);//設(shè)置背景透明村缸,記住這里是[是否不透明]

        setSurfaceTextureListener(this);//設(shè)置監(jiān)聽(tīng)

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//創(chuàng)建畫筆
        mSrcRect = new Rect();
        mDstRect = new Rect();

        mCacheBitmaps = Collections.synchronizedList(new ArrayList<Bitmap>());//多線程需要加鎖
    }

    //... 省略SurfaceTextureListener的方法

    //開(kāi)始播放
    @Override
    public void start(String[] paths, long duration) {
        this.mPaths = paths;
        this.mFrameCount = paths.length;

        //開(kāi)啟線程
        mReadThread = new ReadThread();
        mReadThread.start();
        mScheduler = new Scheduler(duration, mFrameCount,
                new FrameUpdateListener());
    }

    private class ReadThread extends Thread {
        @Override
        public void run() {
            try {
                while (mReadFrame < mFrameCount) {//并且沒(méi)有讀完則繼續(xù)讀取
                    if (mCacheBitmaps.size() >= MAX_CACHE_NUMBER) {//如果讀取的超過(guò)最大緩存則暫停讀取
                        SystemClock.sleep(1);
                        continue;
                    }

                    Bitmap bmp = readBitmap(mPaths[mReadFrame]);
                    mCacheBitmaps.add(bmp);

                    mReadFrame++;

                    if (mReadFrame == 1) {//讀取到第一幀后在開(kāi)始調(diào)度器
                        mScheduler.start();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private Bitmap readBitmap(String path) throws IOException {
        return BitmapFactory.decodeStream(getResources().getAssets().open(path));
    }

    private class FrameUpdateListener implements OnFrameUpdateListener {
        @Override
        public void onFrameUpdate(long frameIndex) {
            if (mCacheBitmaps.isEmpty()) {//如果當(dāng)前沒(méi)有幀祠肥,則直接跳過(guò)
                return;
            }

            Bitmap bitmap = mCacheBitmaps.remove(0);//獲取第一幀同時(shí)從緩存里刪除
            drawBitmap(bitmap);
            recycleBitmap(bitmap);
        }
    }

    private void drawBitmap(Bitmap bitmap) {
        Canvas canvas = lockCanvas();//鎖定畫布
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空畫布
        mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());//這里我將2個(gè)rect抽離出去,防止重復(fù)創(chuàng)建
        mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
        canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//將bitmap畫到畫布上
        unlockCanvasAndPost(canvas);//解鎖畫布同時(shí)提交
    }

    private static void recycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }
}

到現(xiàn)在為至我們已經(jīng)成功的實(shí)現(xiàn)了一個(gè)圖片播放器梯皿。

但是你以為已經(jīng)已經(jīng)結(jié)束了嗎仇箱?

怎么可能O厮 !剂桥!

事實(shí)上現(xiàn)在還有一個(gè)比較嚴(yán)重的問(wèn)題忠烛,這個(gè)問(wèn)題在很大程度上會(huì)影響整個(gè)app的性能。
這個(gè)問(wèn)題就是內(nèi)存抖動(dòng)权逗,什么是內(nèi)存抖動(dòng)美尸?

如果你對(duì)內(nèi)存抖動(dòng)不了解的話,可以通過(guò)《Android App解決卡頓慢之內(nèi)存抖動(dòng)及內(nèi)存泄漏(發(fā)現(xiàn)和定位)》或者Google的官方文檔翻譯《Android性能優(yōu)化典范》了解斟薇。

我引用文章的一句話

  • 內(nèi)存抖動(dòng)是指在短時(shí)間內(nèi)有大量的對(duì)象被創(chuàng)建或者被回收的現(xiàn)象师坎。

意思就是你在循環(huán)中或者onDraw()被頻繁運(yùn)行的方法中去創(chuàng)建對(duì)象,結(jié)果導(dǎo)致頻繁的gc奔垦,而gc會(huì)導(dǎo)致線程卡頓屹耐,如果你在onDraw()或者onLayout()方法中去創(chuàng)建對(duì)象尸疆,AS應(yīng)該會(huì)提示你(Avoid object allocations during draw/layout operations (preallocate and reuse instead))椿猎。

我們可以通過(guò)一張圖來(lái)觀察它到它的現(xiàn)象,通過(guò)這張圖可以很清楚的看到中間那些鋸齒寿弱。


內(nèi)存抖動(dòng)

現(xiàn)在我們需要著手解決這個(gè)問(wèn)題犯眠,如何解決?通過(guò)上面2篇文章我們可以知道要解決這個(gè)問(wèn)題必須盡可能的減少創(chuàng)建對(duì)象症革,去復(fù)用之前已經(jīng)創(chuàng)建的對(duì)象筐咧,這一點(diǎn)我們可以通過(guò)創(chuàng)建對(duì)象池解決,可是我們要如何才能復(fù)用Bitmap噪矛?

其實(shí)Google已經(jīng)給出了解決方案《Managing Bitmap Memory》或者你可以看這個(gè)知乎的回答《Android Bitmap inBitmap 圖片復(fù)用量蕊?》

在BitmapFactory.Options對(duì)象中有個(gè)inBitmap屬性艇挨,如果你設(shè)置inBitmap等于某個(gè)Bitmap(當(dāng)然這里有限制残炮,上面的文章已經(jīng)講的很清楚了),你在用這個(gè)BitmapFactory.Options去加載Bitmap缩滨,它就會(huì)復(fù)用這塊內(nèi)存势就,如果這個(gè)Bitmap在繪制中,你有可能會(huì)看見(jiàn)撕裂現(xiàn)象脉漏。
我們要做的就是創(chuàng)建一個(gè)Bitmap對(duì)象池苞冯,將已經(jīng)畫完的Bitmap放回對(duì)象池,當(dāng)我們要讀取的時(shí)候侧巨,從對(duì)象池中獲取合適的對(duì)象賦予inBitmap舅锄。

最終效果如下,我們可以明顯的看到鋸齒已經(jīng)消失司忱,整個(gè)播放過(guò)程內(nèi)存都很平滑皇忿。


內(nèi)存平滑.jpg
現(xiàn)在我們要開(kāi)始實(shí)現(xiàn)碉怔,先看下BitmapFactory.Options里我們使用的主要屬性
  • inBitmap:如果該值不等于空,則在解碼時(shí)重新使用這個(gè)Bitmap禁添。
  • inMutable:Bitmap是否可變的撮胧,如果設(shè)置了inBitmap,該值必須為true老翘。
  • inPreferredConfig:指定解碼顏色格式芹啥。
  • inJustDecodeBounds:如果設(shè)置為true,將不會(huì)將圖片加載到內(nèi)存中铺峭,但是可以獲得寬高墓怀。
  • inSampleSize:圖片縮放的倍數(shù),如果設(shè)置為2代表加載到內(nèi)存中的圖片大小為原來(lái)的2分之一卫键,這個(gè)值總是和inJustDecodeBounds配合來(lái)加載大圖片傀履,在這里我直接設(shè)置為1,這樣做實(shí)際上是有問(wèn)題的莉炉,如果圖片過(guò)大很容易發(fā)生OOM钓账。
readBitmap方法修改如下
private Bitmap readBitmap(String path) throws IOException {
    InputStream is = getResources().getAssets().open(path);//這里需要以流的形式讀取
    BitmapFactory.Options options = getReusableOptions(is);//獲取參數(shù)設(shè)置
    Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
    is.close();
    return bmp;
}

//實(shí)現(xiàn)復(fù)用,
private BitmapFactory.Options getReusableOptions(InputStream is) throws IOException {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    options.inSampleSize = 1;
    options.inJustDecodeBounds = true;//這里設(shè)置為不將圖片讀取到內(nèi)存中
    is.mark(is.available());
    BitmapFactory.decodeStream(is, null, options);//獲得大小
    options.inJustDecodeBounds = false;//設(shè)置回來(lái)
    is.reset();
    Bitmap inBitmap = getBitmapFromReusableSet(options);
    options.inMutable = true;
    if (inBitmap != null) {//如果有符合條件的設(shè)置屬性
        options.inBitmap = inBitmap;
    }
    return options;
}

//從復(fù)用池中尋找合適的bitmap
private Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
    if (mReusableBitmaps.isEmpty()) {
        return null;
    }
    int count = mReusableBitmaps.size();
    for (int i = 0; i < count; i++) {
        Bitmap item = mReusableBitmaps.get(i);
        if (ImageUtil.canUseForInBitmap(item, options)) {//尋找符合條件的bitmap
            return mReusableBitmaps.remove(i);
        }
    }
    return null;
}

上面的ImageUtil是一個(gè)工具類絮宁,用于判斷是否符合梆暮。

然后我們將這段代碼替換上去。

完整代碼請(qǐng)看PicturePlayerView4

public class PicturePlayerView3 extends TextureView implements TextureView.SurfaceTextureListener {

    private static final int MAX_CACHE_NUMBER = 12;//這是代表讀取最大緩存幀數(shù)绍昂,因?yàn)橐粡垐D片的大小有width*height*4這么大啦粹,內(nèi)存吃不消
    private static final int MAX_REUSABLE_NUMBER = MAX_CACHE_NUMBER / 2;//這是代表讀取最大復(fù)用幀數(shù)

    private Paint mPaint;//畫筆
    private Rect mSrcRect;
    private Rect mDstRect;

    private List<Bitmap> mCacheBitmaps;//緩存幀集合
    private List<Bitmap> mReusableBitmaps;

    private int mReadFrame;//當(dāng)前讀取到那一幀,總幀數(shù)相關(guān)

    private String[] mPaths;//圖片絕對(duì)地址集合
    private int mFrameCount;//總幀數(shù)

    private ReadThread mReadThread;
    private Scheduler mScheduler;

    //... 省略構(gòu)造方法

    private void init() {
        setOpaque(false);//設(shè)置背景透明窘游,記住這里是[是否不透明]

        setSurfaceTextureListener(this);//設(shè)置監(jiān)聽(tīng)

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);//創(chuàng)建畫筆
        mSrcRect = new Rect();
        mDstRect = new Rect();

        //多線程需要加鎖
        mCacheBitmaps = Collections.synchronizedList(new ArrayList<Bitmap>());
        mReusableBitmaps = Collections.synchronizedList(new ArrayList<Bitmap>());
    }

    //... 省略SurfaceTextureListener的方法

    //開(kāi)始播放
    @Override
    public void start(String[] paths, long duration) {
        this.mPaths = paths;
        this.mFrameCount = paths.length;

        //開(kāi)啟線程
        mReadThread = new ReadThread();
        mReadThread.start();
        mScheduler = new Scheduler(duration, mFrameCount,
                new FrameUpdateListener(),
                new FrameListener());
    }

    private class ReadThread extends Thread {
        @Override
        public void run() {
            try {
                while (mReadFrame < mFrameCount) {//并且沒(méi)有讀完則繼續(xù)讀取
                    if (mCacheBitmaps.size() >= MAX_REUSABLE_NUMBER) {//如果讀取的超過(guò)最大緩存則暫停讀取
                        SystemClock.sleep(1);
                        continue;
                    }

                    Bitmap bmp = readBitmap(mPaths[mReadFrame]);
                    mCacheBitmaps.add(bmp);

                    mReadFrame++;

                    if (mReadFrame == 1) {//讀取到第一幀后在開(kāi)始調(diào)度器
                        mScheduler.start();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private Bitmap readBitmap(String path) throws IOException {
        InputStream is = getResources().getAssets().open(path);//這里需要以流的形式讀取
        BitmapFactory.Options options = getReusableOptions(is);//獲取參數(shù)設(shè)置
        Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
        is.close();
        return bmp;
    }

    //實(shí)現(xiàn)復(fù)用
    private BitmapFactory.Options getReusableOptions(InputStream is) throws IOException {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        options.inSampleSize = 1;
        options.inJustDecodeBounds = true;//這里設(shè)置為不將圖片讀取到內(nèi)存中
        is.mark(is.available());
        BitmapFactory.decodeStream(is, null, options);//獲得大小
        options.inJustDecodeBounds = false;//設(shè)置回來(lái)
        is.reset();
        Bitmap inBitmap = getBitmapFromReusableSet(options);
        options.inMutable = true;
        if (inBitmap != null) {//如果有符合條件的設(shè)置屬性
            options.inBitmap = inBitmap;
        }
        return options;
    }

    //從復(fù)用池中尋找合適的bitmap
    private Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        if (mReusableBitmaps.isEmpty()) {
            return null;
        }
        int count = mReusableBitmaps.size();
        for (int i = 0; i < count; i++) {
            Bitmap item = mReusableBitmaps.get(i);
            if (ImageUtil.canUseForInBitmap(item, options)) {//尋找符合條件的bitmap
                return mReusableBitmaps.remove(i);
            }
        }
        return null;
    }

    private void addReusable(Bitmap bitmap) {
        if (mReusableBitmaps.size() >= MAX_REUSABLE_NUMBER) {//如果超過(guò)則將其釋放
            recycleBitmap(mReusableBitmaps.remove(0));
        }
        mReusableBitmaps.add(bitmap);
    }

    private class FrameUpdateListener implements OnFrameUpdateListener {
        @Override
        public void onFrameUpdate(long frameIndex) {
            if (mCacheBitmaps.isEmpty()) {//如果當(dāng)前沒(méi)有幀唠椭,則直接跳過(guò)
                return;
            }

            Bitmap bitmap = mCacheBitmaps.get(0);//獲取第一幀
            drawBitmap(bitmap);

            addReusable(mCacheBitmaps.remove(0));//必須在畫完之后在刪除,不然會(huì)出現(xiàn)畫面撕裂
        }
    }

    //當(dāng)播放線程停止時(shí)回調(diào)忍饰,用處是結(jié)束時(shí)釋放Bitmap
    private class FrameListener extends OnSimpleFrameListener {

        @Override
        public void onStop() {
            try {
                mReadThread.join();//等待播放線程結(jié)束
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int count = mCacheBitmaps.size();
            for (int i = 0; i < count; i++) {
                ImageUtil.recycleBitmap(mCacheBitmaps.get(i));
            }
            mCacheBitmaps.clear();
            count = mReusableBitmaps.size();
            for (int i = 0; i < count; i++) {
                ImageUtil.recycleBitmap(mReusableBitmaps.get(i));
            }
            mReusableBitmaps.clear();
        }
    }

    private void drawBitmap(Bitmap bitmap) {
        Canvas canvas = lockCanvas();//鎖定畫布
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);// 清空畫布
        mSrcRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight());//這里我將2個(gè)rect抽離出去贪嫂,防止重復(fù)創(chuàng)建
        mDstRect.set(0, 0, getWidth(), bitmap.getHeight() * getWidth() / bitmap.getWidth());
        canvas.drawBitmap(bitmap, mSrcRect, mDstRect, mPaint);//將bitmap畫到畫布上
        unlockCanvasAndPost(canvas);//解鎖畫布同時(shí)提交
    }

    private static void recycleBitmap(Bitmap bitmap) {
        if (bitmap != null && !bitmap.isRecycled()) {
            bitmap.recycle();
        }
    }
}

結(jié)尾

事實(shí)上到這里文章就已經(jīng)結(jié)束了,我們可以回顧下步驟喘批。

  1. 繪制圖片
  • 異步繪制一組圖片
  • 使用Handler.sendMessageAtTime()替代SystemClock.sleep()撩荣,使動(dòng)畫更流暢
  • 分離線程,盡可能的將時(shí)間交給繪制這一步
  • 解決內(nèi)存抖動(dòng)問(wèn)題

核心基本都在這里了饶深,其實(shí)還有一些其他的附加功能餐曹,比如暫停恢復(fù)播放敌厘、循環(huán)播放台猴,當(dāng)然它們都不是重點(diǎn)我就不寫了,這些都在PicturePlayerView,有興趣可以研究下饱狂。

當(dāng)然曹步,我也想研究下用GLTextureView實(shí)現(xiàn)下,看看效率會(huì)不會(huì)更高休讳。

題外話

以前看別人寫的文章都以為會(huì)挺輕松讲婚,真正自己寫起來(lái)才發(fā)現(xiàn)真的是難,這篇文章寫的也不盡我滿意俊柔,真的筹麸,如果大家有什么建議或者意見(jiàn)都一定要提出來(lái)。

下一篇文章我可能會(huì)寫關(guān)于圖片操作控件(我可能會(huì)分為一系列)雏婶,類似StickerView的控件物赶,當(dāng)然我和他用ImageView實(shí)現(xiàn)的方式會(huì)有些不一樣。

最后留晚,希望希望下篇文章能有所進(jìn)步酵紫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市错维,隨后出現(xiàn)的幾起案子奖地,更是在濱河造成了極大的恐慌,老刑警劉巖需五,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹉动,死亡現(xiàn)場(chǎng)離奇詭異轧坎,居然都是意外死亡宏邮,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門缸血,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蜜氨,“玉大人,你說(shuō)我怎么就攤上這事捎泻§祝” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵笆豁,是天一觀的道長(zhǎng)郎汪。 經(jīng)常有香客問(wèn)我,道長(zhǎng)闯狱,這世上最難降的妖魔是什么煞赢? 我笑而不...
    開(kāi)封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮哄孤,結(jié)果婚禮上照筑,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好凝危,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布波俄。 她就那樣靜靜地躺著,像睡著了一般蛾默。 火紅的嫁衣襯著肌膚如雪懦铺。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天支鸡,我揣著相機(jī)與錄音阀趴,去河邊找鬼。 笑死苍匆,一個(gè)胖子當(dāng)著我的面吹牛刘急,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播浸踩,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼叔汁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了检碗?” 一聲冷哼從身側(cè)響起据块,我...
    開(kāi)封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎折剃,沒(méi)想到半個(gè)月后另假,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怕犁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年边篮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奏甫。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡戈轿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阵子,到底是詐尸還是另有隱情思杯,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布挠进,位于F島的核電站色乾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏领突。R本人自食惡果不足惜暖璧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一玄捕、第九天 我趴在偏房一處隱蔽的房頂上張望捐腿。 院中可真熱鬧混稽,春花似錦、人聲如沸艰毒。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)滑负。三九已至,卻和暖如春至会,著一層夾襖步出監(jiān)牢的瞬間离咐,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工奉件, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宵蛀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓县貌,卻偏偏與公主長(zhǎng)得像术陶,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子煤痕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容