前言
人生的第一篇技術(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畫到畫布上亏狰,
src
和dst
作用就是將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)卤橄。
原因就在于
- 我們沒(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é)
- 如果消息隊(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)步驟园骆,有興趣可以直接看源碼舔痪。
具體步驟為
- 創(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ò)這張圖可以很清楚的看到中間那些鋸齒寿弱。
現(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)存都很平滑皇忿。
現(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é)束了,我們可以回顧下步驟喘批。
- 繪制圖片
- 異步繪制一組圖片
- 使用
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)步酵紫。