Skip to content

Commit 516d20f

Browse files
committed
b站弹幕源码分析
1 parent 5421083 commit 516d20f

10 files changed

+441
-0
lines changed

.DS_Store

0 Bytes
Binary file not shown.

bitmap/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[Fresco源码分析](Fresco源码分析/README.md)
2+
3+
[Bitmap基础](https://www.jianshu.com/p/0ebfeda17775)
4+
5+
[主流框架Bitmap复用实现分析(doing)](主流框架Bitmap复用实现分析.md)

extra/B站弹幕库源码分析.md

+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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+
![](picture/DanmakuFlameMaster工作原理图.png)
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+
![danmaku_view](picture/danmaku_view.png)
284+
![danmaku_surface_view](picture/danmaku_surface_view.png)
285+
![danmaku_texture_view](picture/danmaku_texture_view.png)
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

Comments
 (0)