|
| 1 | + |
| 2 | +弹幕是一个十分常见的功能,本文将以[DanmakuFlameMaster](https://github.com/bilibili/DanmakuFlameMaster)基础, 分析弹幕的实现。 |
| 3 | + |
| 4 | +>关于弹幕的类型 |
| 5 | +
|
| 6 | +`DanmakuFlameMaster`中显示的弹幕主要分为两种: |
| 7 | + |
| 8 | +1. 视频播放时用户实时发送的 |
| 9 | +2. 视频加载时服务端下发的弹幕集合 |
| 10 | + |
| 11 | +# 弹幕整体工作流程 |
| 12 | + |
| 13 | +>本文只看用户实时发送的弹幕相关逻辑(其他逻辑还有很多),下面用户发送一条弹幕时`DanmakuFlameMaster`的大致工作逻辑图: |
| 14 | +
|
| 15 | + |
| 16 | + |
| 17 | +>涉及到的各个类大致的作用 |
| 18 | +
|
| 19 | +- `Danmaku` : 描述一个弹幕对象 |
| 20 | +- `DanmakuView` : 用来承载弹幕显示的View, 除了它之外还有`DanmakuSurfaceView`、`DanmakuTextureView` |
| 21 | +- `DrawHandler` : 在子线程中控制整个弹幕的显示逻辑,比如暂停、播放等;负责线程的切换 |
| 22 | +- `CacheManagingDrawTask` : 维护弹幕列表,弹幕缓存逻辑 |
| 23 | +- `DrawingCacheHolder` : 弹幕缓存的实现,缓存的是Bitmap |
| 24 | +- `Displayer` : 控制弹幕的显示 |
| 25 | + |
| 26 | +整个工作逻辑的入口是: |
| 27 | + |
| 28 | +>DanmakuView.java |
| 29 | +``` |
| 30 | +public void addDanmaku(BaseDanmaku item) { |
| 31 | + if (handler != null) { |
| 32 | + handler.addDanmaku(item); |
| 33 | + } |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +## 1. DrawHandler调度引起DanmakuView的渲染 |
| 38 | + |
| 39 | +其实这一步主要分为两个点: |
| 40 | + |
| 41 | +1. 添加到弹幕集合`danmakuList`中 |
| 42 | +2. `CacheManagingDrawTask.CacheManager`创建弹幕缓存`DrawingCacheHolder` |
| 43 | +3. 通过`Choreographer`来不断渲染`DanmakuView` |
| 44 | + |
| 45 | +第一步其实就是添加到一个集合中,这里就不细看了,直接看`DrawingCacheHolder`的创建 |
| 46 | + |
| 47 | +### 创建弹幕缓存`DrawingCacheHolder` |
| 48 | + |
| 49 | +`CacheManagingDrawTask.CacheManager`里面有一个`HandlerThread`,他会发起`DrawingCacheHolder`的异步创建: |
| 50 | + |
| 51 | +>下面这个方法包含着整个弹幕的复用逻辑 |
| 52 | +
|
| 53 | +``` |
| 54 | +private byte buildCache(BaseDanmaku item, boolean forceInsert) { |
| 55 | +
|
| 56 | + if (!item.isMeasured()) { //测量弹幕的宽高,保证缓存的Bitmap大小正确 |
| 57 | + item.measure(mDisp, true); |
| 58 | + } |
| 59 | +
|
| 60 | + DrawingCache cache = null; |
| 61 | + try { |
| 62 | + // 找有没有可以完全复用的弹幕,文字,宽,高,颜色,。。。。。 |
| 63 | + BaseDanmaku danmaku = findReusableCache(item, true, mContext.cachingPolicy.maxTimesOfStrictReusableFinds); //完全复用 |
| 64 | + if (danmaku != null) { |
| 65 | + Log.d(Test.TAG, "fit cache ! strict mode : true"); |
| 66 | + cache = (DrawingCache) danmaku.cache; |
| 67 | + } |
| 68 | + if (cache != null) { |
| 69 | + ... |
| 70 | + mCacheManager.push(item, 0, forceInsert); |
| 71 | + return RESULT_SUCCESS; |
| 72 | + } |
| 73 | +
|
| 74 | + // 找有没有差不多可以复用的弹幕 |
| 75 | + danmaku = findReusableCache(item, false, mContext.cachingPolicy.maxTimesOfReusableFinds); |
| 76 | + if (danmaku != null) { |
| 77 | + cache = (DrawingCache) danmaku.cache; |
| 78 | + } |
| 79 | + if (cache != null) { |
| 80 | + danmaku.cache = null; |
| 81 | + cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache, mContext.cachingPolicy.bitsPerPixelOfCache); //redraw |
| 82 | + item.cache = cache; |
| 83 | + mCacheManager.push(item, 0, forceInsert); |
| 84 | + return RESULT_SUCCESS; |
| 85 | + } |
| 86 | + ... |
| 87 | + cache = mCachePool.acquire(); //直接创建出来一个弹幕 |
| 88 | + cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache, mContext.cachingPolicy.bitsPerPixelOfCache); |
| 89 | + item.cache = cache; |
| 90 | + boolean pushed = mCacheManager.push(item, sizeOf(item), forceInsert); |
| 91 | + .... |
| 92 | + } |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +上面这个方法其实主要分为3步: |
| 97 | + |
| 98 | +1. 寻找完全可以复用的弹幕,即内容、颜色等完全相同的 |
| 99 | +2. 寻找差不多可以复用的,这里的差不多其实是指找到一个比要画的弹幕大的弹幕(当然要大在一定范围内的) |
| 100 | +3. 实在找不到就创建 |
| 101 | + |
| 102 | +上面2、3两步都要走一个核心方法`DanmakuUtils.buildDanmakuDrawingCache()`: |
| 103 | + |
| 104 | +``` |
| 105 | +DrawingCache buildDanmakuDrawingCache(BaseDanmaku danmaku, IDisplayer disp, DrawingCache cache, int bitsPerPixel) { |
| 106 | + ... |
| 107 | + cache.build((int) Math.ceil(danmaku.paintWidth), (int) Math.ceil(danmaku.paintHeight), disp.getDensityDpi(), false, bitsPerPixel); |
| 108 | + DrawingCacheHolder holder = cache.get(); |
| 109 | + if (holder != null) { |
| 110 | + ... |
| 111 | + //直接把内容画上去 |
| 112 | + ((AbsDisplayer) disp).drawDanmaku(danmaku, holder.canvas, 0, 0, true); |
| 113 | + ... |
| 114 | + } |
| 115 | + return cache; |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +即先`build`,然后`draw`: |
| 120 | + |
| 121 | +>`DrawingCache.build()`: |
| 122 | +
|
| 123 | +``` |
| 124 | +public void buildCache(int w, int h, int density, boolean checkSizeEquals, int bitsPerPixel) { |
| 125 | + boolean reuse = checkSizeEquals ? (w == width && h == height) : (w <= width && h <= height); |
| 126 | + if (reuse && bitmap != null) { |
| 127 | + bitmap.eraseColor(Color.TRANSPARENT); |
| 128 | + canvas.setBitmap(bitmap); |
| 129 | + recycleBitmapArray(); //一般没什么用 |
| 130 | + return; |
| 131 | + } |
| 132 | + ... |
| 133 | + bitmap = NativeBitmapFactory.createBitmap(w, h, config); |
| 134 | +
|
| 135 | + if (density > 0) { |
| 136 | + mDensity = density; |
| 137 | + bitmap.setDensity(density); |
| 138 | + } |
| 139 | + if (canvas == null){ |
| 140 | + canvas = new Canvas(bitmap); |
| 141 | + canvas.setDensity(density); |
| 142 | + }else |
| 143 | + canvas.setBitmap(bitmap); |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +其实就是如果这个`cache`中有`Bitmap`的话,那么就擦干净。如果没有`Bitmap`,那么就在`native heap`上创建一个`Bitmap`,这个`Bitmap`会和`DrawingCacheHolder`的`canvas`管关联起来。 |
| 148 | + |
| 149 | +**这里在`native heap`上创建`Bitmap`会减小`java heap`的压力,避免OOM** |
| 150 | + |
| 151 | +>AbsDisplayer.drawDanmaku() |
| 152 | +
|
| 153 | +这个方法其实调用的逻辑还是比较长的,就不一一分析了,其实最终是通过`DrawingCacheHolder.canvas`把弹幕画在了`DrawingCacheHolder.bitmap`上: |
| 154 | + |
| 155 | +>SimpleTextCacheStuffer.java |
| 156 | +``` |
| 157 | +@Override |
| 158 | +public void drawDanmaku(BaseDanmaku danmaku, Canvas canvas...) { |
| 159 | + ... |
| 160 | + drawBackground(danmaku, canvas, _left, _top); |
| 161 | + ... |
| 162 | + drawText(danmaku, lines[0], canvas, left, top - paint.ascent(), paint, fromWorkerThread); |
| 163 | + ... |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +其实上面两步做的事就是 : **在异步线程中给`Danmaku`准备好一个渲染完成的`Bitmap`** |
| 168 | + |
| 169 | + |
| 170 | +### 通过`Choreographer`来不断渲染`DanmakuView` |
| 171 | + |
| 172 | +在最开始就已经知道`DrawHandler`用来控制整个弹幕逻辑,它会通过`Choreographer`来引起`DanmakuView`的渲染: |
| 173 | + |
| 174 | +``` |
| 175 | +private void updateInChoreographer() { |
| 176 | + ... |
| 177 | + Choreographer.getInstance().postFrameCallback(mFrameCallback); |
| 178 | + ... |
| 179 | + d = mDanmakuView.drawDanmakus(); |
| 180 | + ... |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +`mFrameCallback`其实就是个套娃,即不断调用`updateInChoreographer`,`mDanmakuView.drawDanmakus()`其实是一个抽象方法,它会调用到`View.postInvalidateCompat()`,即触发`DanmakuView.onDraw()`, 从这里之后其实又有很复杂的逻辑,这里就不继续跟了,其实接下来的调用流程会调用到`DanmakuRenderer.accept()`: |
| 185 | + |
| 186 | +``` |
| 187 | +//main thread |
| 188 | +public int accept(BaseDanmaku drawItem) { |
| 189 | + ... |
| 190 | + // measure |
| 191 | + if (!drawItem.isMeasured()) { |
| 192 | + drawItem.measure(disp, false); |
| 193 | + } |
| 194 | + ... |
| 195 | + // layout 算x, y坐标 |
| 196 | + mDanmakusRetainer.fix(drawItem, disp, mVerifier); |
| 197 | + ... |
| 198 | + drawItem.draw(disp); |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +`measure()`这里就不看了,其实就是根据弹幕内容测量应该占多大空间; ` mDanmakusRetainer.fix()`最终会调用到`Danmaku.layout()`, `Danmaku`其实是一个抽象类,这里我们看一下从右滚到左的弹幕时怎么计算自己的坐标的: |
| 203 | + |
| 204 | +``` |
| 205 | +public class R2LDanmaku extends BaseDanmaku { |
| 206 | + @Override |
| 207 | + public void layout(IDisplayer displayer, float x, float y) { |
| 208 | + if (mTimer != null) { |
| 209 | + long currMS = mTimer.currMillisecond; |
| 210 | + long deltaDuration = currMS - getActualTime(); |
| 211 | + if (deltaDuration > 0 && deltaDuration < duration.value) { |
| 212 | + this.x = getAccurateLeft(displayer, currMS); // 根据时间进度, 和当前显示器的宽度,来确定当前显示的x坐标 |
| 213 | + if (!this.isShown()) { |
| 214 | + this.y = y; |
| 215 | + this.setVisibility(true); |
| 216 | + } |
| 217 | + mLastTime = currMS; |
| 218 | + return; |
| 219 | + } |
| 220 | + mLastTime = currMS; |
| 221 | + } |
| 222 | + ... |
| 223 | + } |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +y坐标其实是由更上一个层的类确定好的, 这里其实主要是x坐标的逻辑,他的核心算法其实就是**根据时间进度,和当前显示器的宽度,来确定当前显示的x坐标** |
| 228 | + |
| 229 | +接下来看怎么绘制一个弹幕的, 这里其实会调用到`AndroidDisplayer.draw()` |
| 230 | + |
| 231 | +``` |
| 232 | +public int draw(BaseDanmaku danmaku) { |
| 233 | + |
| 234 | + boolean cacheDrawn = sStuffer.drawCache(danmaku, canvas, left, top, alphaPaint, mDisplayConfig.PAINT); |
| 235 | + int result = IRenderer.CACHE_RENDERING; |
| 236 | + if (!cacheDrawn) { |
| 237 | + ... |
| 238 | + drawDanmaku(danmaku, canvas, left, top, false); // 绘制bitmap |
| 239 | + result = IRenderer.TEXT_RENDERING; |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +首先这里的`canvas`是`DanmakuView.onDraw(canvas)`的`canvas` |
| 245 | + |
| 246 | +`sStuffer.drawCache()`其实就是把前面画好的`Bitmap`画在这个`Canvas`上, 如果没有现存的`Bitmap`可以去画,那么就先画这个弹幕画到`Canvas`上。 |
| 247 | + |
| 248 | +**其实这里几乎90%的情况下都会走到`sStuffer.drawCache()`中** |
| 249 | + |
| 250 | +>OK到这里就简单的分析完了整个实现流程,上面讲的可能不是很详细,不过基本流程都讲到了。(其实这里不好去讲的很细,因为代码还是很复杂的。。。详细讲的话写起来太多了。。。) |
| 251 | +
|
| 252 | + |
| 253 | +# DanmakuSurfaceView |
| 254 | + |
| 255 | +其实就是单独开辟一个Surface来处理弹幕的绘制操作,即绘制操作是可以在子线程(DrawHandler),不会造成主线程的卡顿 |
| 256 | + |
| 257 | +``` |
| 258 | +public long drawDanmakus() { |
| 259 | + ... |
| 260 | + Canvas canvas = mSurfaceHolder.lockCanvas(); |
| 261 | + ... |
| 262 | + RenderingState rs = handler.draw(canvas); |
| 263 | +
|
| 264 | + mSurfaceHolder.unlockCanvasAndPost(canvas); |
| 265 | +
|
| 266 | + ... |
| 267 | + return dtime; |
| 268 | +} |
| 269 | +``` |
| 270 | + |
| 271 | +# DanmakuTextureView |
| 272 | + |
| 273 | +直接继承自TextureView, TextureView与View和SurfaceView的不同之处是 : |
| 274 | + |
| 275 | +- 不同于SurfaceView, 它可以像普通View那样能被缩放、平移,也能加上动画 |
| 276 | +- 不同于普通View的硬件加速渲染, TextureView不具有Display List,它们是通过一个称为Layer Renderer的对象以Open GL纹理的形式来绘制的 |
| 277 | + |
| 278 | + |
| 279 | +# DanmakuView/DanmakuSurfaceView/DanmakuTextureView对比 |
| 280 | + |
| 281 | +`DanmakuFlameMaster`的Demo运行1分钟后,CPU Memory Profiler的统计情况 : |
| 282 | + |
| 283 | + |
| 284 | + |
| 285 | + |
| 286 | + |
| 287 | +上面DanmakuView的Graphics占用内存比较多的原因应该是 :View硬件加速渲染大量纹理由CPU同步到GPU所消耗的内存 |
| 288 | + |
| 289 | +- DanmakuView占用的内存(Graphics内存)比较高, 这主要是因为由于View的绘制在Android 4.0之后默认都会开启硬件加速,在大量弹幕的情况下会出现大量Bitmap纹理上传(CPU把数据同步给GPU),这部分数据其实就是放在Graphics内存中 |
| 290 | +- DanmakuSurfaceView是纯软绘制,绘制的操作在子线程,不过由于它是独立于Activity的View Tree,因此在使用场景上可能比较受限 |
| 291 | +- DanmakuTextureView想较于DanmakuSurfaceView,可以直接放到Activity的View Tree中, 不过它只可以在支持硬件加速的Window中使用 |
| 292 | + |
| 293 | + |
| 294 | + |
| 295 | + |
0 commit comments