对于视频网站来说弹幕是一个十分常见的功能, 目前业界比较出名的弹幕库是B站的DanmakuFlameMaster(不过很久没有更新了),这个弹幕库的功能还是十分完善和稳定的,它里面的弹幕主要分为两种:
- 视频播放时用户实时发送的
- 视频加载时服务端下发的弹幕集合
由于整个弹幕库涉及到的逻辑还是非常多的, 本文主要分析一下用户发送一条从右向左滚动的弹幕的实现逻辑(不涉及视频弹幕时间同步等相关逻辑):
下图是用户发送一条弹幕时
DanmakuFlameMaster的大致工作逻辑图:
涉及到的各个类大致的作用
- R2LDanmaku: 一个弹幕对象, 里面包含x、y坐标, 缓存的- Bitmap等属性
- DanmakuView: 用来承载弹幕显示的- ViewGroup, 除了它之外还有- DanmakuSurfaceView、- DanmakuTextureView
- DrawHandler: 一个绑定了异步- HandlerThread的- Handler, 控制整个弹幕的显示逻辑
- CacheManagingDrawTask: 维护需要绘制的弹幕列表, 控制弹幕缓存逻辑
- DrawingCacheHolder: 弹幕缓存的实现,缓存的是- Bitmap, 与- BaseDanmaku绑定
- DanmakuRenderer: 对弹幕做一些过滤、碰撞检测、测量、布局、缓存等工作
- Displayer: 持有- Canvas画布, 绘制弹幕
在向DanmakuView中添加弹幕时会触发弹幕的显示流程:
DanmakuView.java
public void addDanmaku(BaseDanmaku item) {
    if (handler != null) {
        handler.addDanmaku(item);
    }
}DrawHandler调度引起DanmakuView的渲染
- 将弹幕添加到CacheManagingDrawTask的弹幕集合danmakuList中
- CacheManagingDrawTask.CacheManager创建弹幕缓存- DrawingCache
- 通过Choreographer来不断渲染DanmakuView
第一步其实就是把弹幕添加到一个集合中,这里就不细看了,直接看DrawingCache.DrawingCacheHolder的创建
创建弹幕缓存DrawingCacheHolder
其实这里的缓存说白了就是一个Bitmap对象, 因为DanmakuFlameMaster的弹幕绘制的实现是 : 先把弹幕画在一个Bitmap上, 然后再把Bitmap绘制在Canvas上
CacheManagingDrawTask.CacheManager里面有一个HandlerThread,他会异步创建DrawingCache.DrawingCacheHolder,不过在创建DrawingCache前,会先尝试从缓存池中复用(找有没有可以复用的Bitmap):
byte buildCache(BaseDanmaku item, boolean forceInsert) {
    ...
    DrawingCache cache = null;
    // 找有没有可以完全复用的弹幕,文字,宽,高,颜色等都相同
    BaseDanmaku danmaku = findReusableCache(item, true, mContext.cachingPolicy.maxTimesOfStrictReusableFinds); //完全复用
    if (danmaku != null) {
        cache = (DrawingCache) danmaku.cache;
    }
    if (cache != null) {
        ...
        cache.increaseReference();  //增加引用, 同屏上完全相同的弹幕时可以复用同一个缓存的
        item.cache = cache;
        mCacheManager.push(item, 0, forceInsert);
        return RESULT_SUCCESS;
    }
    // 找有没有差不多可以复用的弹幕
    danmaku = findReusableCache(item, false, mContext.cachingPolicy.maxTimesOfReusableFinds);
    if (danmaku != null) {
        cache = (DrawingCache) danmaku.cache;
    }
    if (cache != null) {
        danmaku.cache = null;
        cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache, mContext.cachingPolicy.bitsPerPixelOfCache);  //redraw
        item.cache = cache;
        mCacheManager.push(item, 0, forceInsert);
        return RESULT_SUCCESS;
    }
    ...
    cache = mCachePool.acquire();        //直接创建出来一个弹幕
    cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache, mContext.cachingPolicy.bitsPerPixelOfCache);
    item.cache = cache;
    boolean pushed = mCacheManager.push(item, sizeOf(item), forceInsert);
    ....
}上面这个方法其实主要分为3步:
- 寻找完全可以复用的弹幕,即内容、颜色等完全相同的, 同屏上完全相同的弹幕时可以复用同一个缓存的
- 寻找差不多可以复用的,这里的差不多其实是指找到一个比要画的弹幕大的弹幕(当然要大在一定范围内的)
- 没有缓存的话就创建一个
上面2、3两步都要走一个核心方法DanmakuUtils.buildDanmakuDrawingCache():
DrawingCache buildDanmakuDrawingCache(BaseDanmaku danmaku, IDisplayer disp, DrawingCache cache, int bitsPerPixel) {
    ...
    cache.build((int) Math.ceil(danmaku.paintWidth), (int) Math.ceil(danmaku.paintHeight), disp.getDensityDpi(), false, bitsPerPixel);
    DrawingCacheHolder holder = cache.get();
    if (holder != null) {
        ...
        ((AbsDisplayer) disp).drawDanmaku(danmaku, holder.canvas, 0, 0, true);     //直接把内容画上去
        ...
    }
    return cache;
}即先build,然后draw:
DrawingCache.build():
public void buildCache(int w, int h, int density, boolean checkSizeEquals, int bitsPerPixel) {
    boolean reuse = checkSizeEquals ? (w == width && h == height) : (w <= width && h <= height);
    if (reuse && bitmap != null) {
        bitmap.eraseColor(Color.TRANSPARENT);
        canvas.setBitmap(bitmap);
        recycleBitmapArray(); //一般没什么用
        return;
    }
    ...
    bitmap = NativeBitmapFactory.createBitmap(w, h, config);
    if (density > 0) {
        mDensity = density;
        bitmap.setDensity(density);
    }
    if (canvas == null){
        canvas = new Canvas(bitmap);
        canvas.setDensity(density);
    }else
    canvas.setBitmap(bitmap);
}其实就是如果这个DrawingCache中有Bitmap的话,那么就擦干净。如果没有Bitmap,那么就在native heap上创建一个Bitmap,这个Bitmap会和DrawingCache.DrawingCacheHolder的canvas管关联起来。
这里在native heap上创建Bitmap会减小java heap的压力,避免OOM
AbsDisplayer.drawDanmaku()
这个方法的调用逻辑挺长的,就不把源码展开分析了,其实最终是通过DrawingCacheHolder.canvas把弹幕画在了DrawingCacheHolder.bitmap上:
SimpleTextCacheStuffer.java
@Override
public void drawDanmaku(BaseDanmaku danmaku, Canvas canvas...) {
    ...
    drawBackground(danmaku, canvas, _left, _top);
    ...
    drawText(danmaku, lines[0], canvas, left, top - paint.ascent(), paint, fromWorkerThread);
    ...
}上面build和draw两步做的事简单来说就是: 在异步线程中给Danmaku准备好一个渲染完成的Bitmap
ok, 走完上面这些步骤,其实一个绘制完成的弹幕的Bitmap就已经就绪了,接下来就是把这个Bitmap画到真正显示在平面上的画布Canvas上了
通过Choreographer来不断渲染DanmakuView
在最开始就已经知道DrawHandler用来控制整个弹幕逻辑,它会通过Choreographer来引起DanmakuView的渲染(draw):
private void updateInChoreographer() {
    ...
    Choreographer.getInstance().postFrameCallback(mFrameCallback);
    ...
    d = mDanmakuView.drawDanmakus();
    ...
}mFrameCallback其实就是个套娃,即不断调用updateInChoreographer,mDanmakuView.drawDanmakus()其实是一个抽象方法,对于DanmakuView来说, 它会调用到View.postInvalidateCompat(),即触发DanmakuView.onDraw(), 从这里之后其实又有很复杂的逻辑, 也不把源码一一展开了, 最终调用到DanmakuRenderer.accept():
//main thread
public int accept(BaseDanmaku drawItem) {
    ...
    // measure
    if (!drawItem.isMeasured()) {
        drawItem.measure(disp, false);
    }  
    ...
    // layout  算x, y坐标
    mDanmakusRetainer.fix(drawItem, disp, mVerifier);
    ...
    drawItem.draw(disp);
}measure()这里就不看了,其实就是根据弹幕内容测量应该占多大空间; mDanmakusRetainer.fix()最终会调用到R2LDanmaku.layout() :
public class R2LDanmaku extends BaseDanmaku {
    @Override
    public void layout(IDisplayer displayer, float x, float y) {
        if (mTimer != null) {
            long currMS = mTimer.currMillisecond;
            long deltaDuration = currMS - getActualTime();
            if (deltaDuration > 0 && deltaDuration < duration.value) {
                this.x = getAccurateLeft(displayer, currMS);   // 根据时间进度, 和当前显示器的宽度,来确定当前显示的x坐标
                if (!this.isShown()) {
                    this.y = y;
                    this.setVisibility(true);
                }
                mLastTime = currMS;
                return;
            }
            mLastTime = currMS;
        }
        ...
    }
}y坐标其实是由更上一个层的类确定好的, R2LDanmaku.layout主要是确定x坐标的逻辑,他的核心算法是 : 根据时间进度,和当前显示器的宽度,来确定当前显示的x坐标
接下来看怎么绘制一个弹幕的, 这里其实会调用到AndroidDisplayer.draw()
public int draw(BaseDanmaku danmaku) {
    boolean cacheDrawn = sStuffer.drawCache(danmaku, canvas, left, top, alphaPaint, mDisplayConfig.PAINT);
    int result = IRenderer.CACHE_RENDERING;
    if (!cacheDrawn) {
        ...
        drawDanmaku(danmaku, canvas, left, top, false); // 绘制bitmap
        result = IRenderer.TEXT_RENDERING;
    }   
}首先这里的canvas是DanmakuView.onDraw(canvas)的canvas, sStuffer.drawCache()其实就是把前面画好的Bitmap画在这个Canvas上, 如果没有现存的Bitmap可以去画,直接把画到Canvas上。
其实这里几乎90%的情况下都会走到sStuffer.drawCache()中
到这里就简单的分析完了整个实现流程,上面讲的可能不是很详细,不过基本流程都讲到了
DanmakuSurfaceView
单独开辟一个Surface来处理弹幕的绘制操作,即绘制操作是可以在子线程(DrawHandler),不会造成主线程的卡顿
public long drawDanmakus() {
    ...
    Canvas canvas = mSurfaceHolder.lockCanvas();
    ...
    RenderingState rs = handler.draw(canvas);
    mSurfaceHolder.unlockCanvasAndPost(canvas);
    ...
    return dtime;
}DanmakuTextureView
直接继承自TextureView, TextureView与View和SurfaceView的不同之处是 :
- 不同于SurfaceView, 它可以像普通View那样能被缩放、平移,也能加上动画
- 不同于普通View的硬件加速渲染, TextureView不具有Display List,它们是通过一个称为Layer Renderer的对象以Open GL纹理的形式来绘制的, 不过依然要同步于主线程的绘制操作
简单的性能分析
将DanmakuFlameMaster的Demo运行1分钟后,通过CPU Memory Profiler可以看到 : DanmakuView的Graphics占用内存比较多 , 其实主要原因是因为View硬件加速渲染时大量纹理由CPU同步到GPU消耗了大量的内存
那么如何优化呢?
个人感觉可以在现有的基础上使用GLSurfaceView或者GLTextureView通过Open GL来完成弹幕的渲染。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/20136,转载请注明出处。



评论0