Bullets are a very common function for video websites. At present, DanmakuFlameMaster of STATION B is the most famous ammunition library in the industry (but it has not been updated for a long time). The function of this ammunition library is very perfect and stable, and there are two main types of bullets in it:

  1. The user sends the video in real time while it is playing
  2. A collection of bullets delivered by the server during video loading

Since the whole bullet-screen library involves a lot of logic, this paper mainly analyzes the realization logic of users sending a right-to-left scrolling bullet-screen (excluding related logic such as time synchronization of video bullet-screen):

Below is the general working logic diagram of DanmakuFlameMaster when the user sends a barrage:

The general role of each class involved

  • R2LDanmaku: a barrage object, which contains x and y coordinates, cachedBitmapAttributes such as
  • DanmakuView: used to carry the barrage displayViewGroupIn addition to itDanmakuSurfaceView,DanmakuTextureView
  • DrawHandler: One binding is asynchronousHandlerThreadtheHandlerTo control the display logic of the whole barrage
  • CacheManagingDrawTask: Maintains the bullet-screen list to be drawn and controls the bullet-screen cache logic
  • DrawingCacheHolder: implementation of barrage cache, cache isBitmap, andBaseDanmakuThe binding
  • DanmakuRenderer: Do some filtering, collision detection, measurement, layout, cache, etc
  • DisplayerHeld:CanvasCanvas, draw barrage

When adding barrage to DanmakuView, the display process of barrage will be triggered:

DanmakuView.java

public void addDanmaku(BaseDanmaku item) {
    if (handler != null) {
        handler.addDanmaku(item);
    }
}
Copy the code

The DrawHandler scheduler causes the DanmakuView to render

  1. Adds a barrage toCacheManagingDrawTaskBarrage collectiondanmakuListIn the
  2. CacheManagingDrawTask.CacheManagerCreate a barrage cacheDrawingCache
  3. throughChoreographerTo keep renderingDanmakuView

Actually the first step is to barrage is added to a collection, don’t look here, look directly DrawingCache. DrawingCacheHolder creation

Create a barrage cacheDrawingCacheHolder

In fact, the cache here is basically oneBitmapObject, becauseDanmakuFlameMasterThe realization of bullet screen drawing is: first paint the bullet screen in aBitmapGo, and then goBitmapPainted onCanvason

CacheManagingDrawTask. There is a HandlerThread CacheManager, he will create DrawingCache asynchronous. DrawingCacheHolder, but before creating DrawingCache, Will first try to reuse from the cache pool (see if there are any bitmaps that can be reused):

byte buildCache(BaseDanmaku item, boolean forceInsert) { ... DrawingCache cache = null; BaseDanmaku danmaku = findReusableCache(item,true, mContext.cachingPolicy.maxTimesOfStrictReusableFinds); // Full reuseif(danmaku ! = null) { cache = (DrawingCache) danmaku.cache; }if(cache ! = null) { ... cache.increaseReference(); // Add references to item.cache = cache; // Add references to item.cache = cache; mCacheManager.push(item, 0, forceInsert);returnRESULT_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);returnRESULT_SUCCESS; }... cache = mCachePool.acquire(); / / directly created a barrage cache. = DanmakuUtils buildDanmakuDrawingCache (item, mDisp, cache, mContext.cachingPolicy.bitsPerPixelOfCache); item.cache = cache; boolean pushed = mCacheManager.push(item, sizeOf(item), forceInsert); . }Copy the code

The above method is actually divided into three steps:

  1. Search for fully reusable barrage, that is, the same cache can be reused when the content and color of the barrage are completely the same as those on the screen
  2. Look for something that is almost reusable. By “almost” I mean to find a barrage that is larger than the one to be drawn (but within a certain range).
  3. Create one if you don’t have one

The above two steps 2 and 3 to go a core method 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); // Draw the content directly... }return cache;
}
Copy the code

Build, then 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(); // Do you like itreturn; }... 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);
}
Copy the code

If there is a Bitmap in the DrawingCache, erase it. If no Bitmap, then on the native heap to create a Bitmap, the Bitmap will and DrawingCache DrawingCacheHolder canvas tube.

Here innative heapTo create aBitmapWill reducejava heapPressure to avoid OOM

AbsDisplayer.drawDanmaku()

This method invocation logic is quite long, is not the source code analysis, it is ultimately through DrawingCacheHolder. Canvas draw barrage in the 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); . }Copy the code

What the build and draw steps above do is simply: prepare Danmaku with a rendered Bitmap in an asynchronous thread

Ok, after the above steps, in fact, a Bitmap of the drawn barrage is ready. The next step is to draw the Bitmap to the Canvas that is actually displayed on the plane

throughChoreographerTo keep renderingDanmakuView

It has been known from the beginning that DrawHandler is used to control the entire barrage logic, and it will use Choreographer to cause DanmakuView to render (draw):

private void updateInChoreographer() {... Choreographer.getInstance().postFrameCallback(mFrameCallback); . d = mDanmakuView.drawDanmakus(); . }Copy the code

MFrameCallback is in fact a doll that constantly invoke updateInChoreographer, mDanmakuView. DrawDanmakus () is an abstract method, For DanmakuView, it will be called to the View. The postInvalidateCompat (), which triggers DanmakuView. Ontouch (), after here actually have very complex logic, and did not take the source code opened one by one, Finally, danmakurenderer.accept () is called:

//main thread
public int accept(BaseDanmaku drawItem) {
    ...
    // measure
    if(! drawItem.isMeasured()) { drawItem.measure(disp,false); }... // layout calculate x and Y coordinates mDanmakusRetainer. Fix (drawItem, disp, mVerifier); . drawItem.draw(disp); }Copy the code

Measure () is to measure how much space should be occupied according to the content of bullet screen. Mdanmakusretainer.fix () will eventually call 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); // Determine the x coordinate of the current display according to the time schedule and the width of the current displayif(! this.isShown()) { this.y = y; this.setVisibility(true);
                }
                mLastTime = currMS;
                return; } mLastTime = currMS; }... }}Copy the code

Y coordinates are actually determined by a higher layer of the class, R2ldanmaku.layout is mainly to determine the logic of x coordinates, his core algorithm is: according to the time progress, and the width of the current display, to determine the current display of X coordinates

Now let’s see how to draw a barrage. This will actually call 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); // Render bitmap result = irenderer.text_rendering; }}Copy the code

First of all, the canvas here is the canvas of DanmakuView.ondraw (canvas). Sstuffer.drawcache () actually draws the previously drawn Bitmap on this canvas. If there is no existing Bitmap to draw, Let me draw it directly on the Canvas.

In fact, it happens almost 90% of the timesStuffer.drawCache()In the

Here is a simple analysis of the entire implementation process, the above may not be very detailed, but the basic process is talked about

DanmakuSurfaceView

A separate Surface is opened to deal with the drawing operation of bullets, that is, the drawing operation can be done in the sub-thread (DrawHandler), without causing the main thread to lag

public long drawDanmakus() {... Canvas canvas = mSurfaceHolder.lockCanvas(); . RenderingState rs = handler.draw(canvas); mSurfaceHolder.unlockCanvasAndPost(canvas); .return dtime;
}
Copy the code

DanmakuTextureView

Directly inheriting from TextureView, TextureView differs from View and SurfaceView in this way:

  • Unlike SurfaceView, it can be zoomed, panned, and animated just like a regular View
  • Unlike the hardware-accelerated rendering of regular Views, TextureView does not have Display lists, they are done through a method calledLayer RendererThe object toOpen GLIn the form of a texture, but still synchronized with the main draw operation

Simple performance analysis

After running the DanmakuFlameMaster Demo for 1 minute, you can see from the CPU Memory Profiler: DanmakuView Graphics takes up a lot of memory, in fact, the main reason is that the View hardware accelerated rendering of a large amount of texture synchronization from CPU to GPU consumes a lot of memory

So how do you optimize?

Personally, I feel that I can use GLSurfaceView or GLTextureView to complete the rendering of barrage through Open GL on the existing basis.

For more articles on Android, see The Android Advanced Program