进入B站后笔者首先着手的就是哔哩哔哩国际版的弹幕重构。
哔哩哔哩国际版内部简称蓝版,这是因为其主题色位蓝色,这么做的目的是为了和国内的粉版区分。其能在海外的 App Store上下载到,以下是 APP 的介绍页面:

B站海外版应用

蓝版的代码最初是从粉版 fork 出来的,并且阉割了一些比较重的、不常用的功能。考虑到海外的网络情况和国内差的不是一点半点,因此本人(kyson.cn)在重构弹幕时还做了很大幅度的优化。这会在文章中慢慢分享给大家。

引言

首先给大家提个问题:如果让您设计一个弹幕,你会怎么着手去做这件事。这里我相信大家不约而同肯定会想到:github。
我们先罗列一下 github 上 star 数较多的几个弹幕库:

弹幕库名star 数地址
BarrageRenderer2.2khttps://github.com/unash/BarrageRenderer
DanmakuFlameMaster9.5khttps://github.com/bilibili/DanmakuFlameMaster

BarrageRenderer

BarrageRenderer 是个人开发者编写的,完成度还算挺高,可惜目前已经不维护了。查看 BarrageRenderer 代码可知,其关键逻辑还是更改 UIView 的 Frame:

#pragma mark - update
/// 每个刷新周期执行一次
- (void)update
{
    [_dispatcher dispatchSprites]; // 分发精灵
    for (BarrageSprite * sprite in _dispatcher.activeSprites) {
        [sprite updateWithTime:self.time];
    }
}

#pragma mark - update

- (void)updateWithTime:(NSTimeInterval)time
{
    _valid = !self.forcedInvalid && [self validWithTime:time];
    _view.frame = [self rectWithTime:time];
    if ([_view respondsToSelector:@selector(updateWithTime:)]) {
        [_view updateWithTime:time];
    }
}

DanmakuFlameMaster

DanmakuFlameMaster 简称 DFM,开源的只有 Android 版本,是哔哩哔哩早期使用的弹幕引擎。由于本人不是专业的 Android 开发,因此我不做过多分析。

本人(kyson.cn)在B站第一个任务就是负责将 DFM 下架,转而使用字幕/弹幕统一框架 Chronos。 关于 Chronos 我后面做详细介绍,这里先和大家报告一下 DFM 在 iOS 版本下的一些实现原理。

由于 iOS 版本的 DFM 没有开源,我这里不能将具体代码贴出,但我会将其实现原理大致告诉大家。

和 BarrageRenderer 有一些相似之处

我怀疑 BarrageRenderer 作者是否在B站工作过,他的设计理念,甚至一些方法名都和 DFM 一模一样。不信你看:

- (void)updateWithTime:(DFMTime)time {
   //此处省略一部分代码
}

这是核心的更新弹幕位置的函数。其他的部分相似的还有命名:DFMClock,设计思想也有一部分类似,因此看懂 BarrageRenderer 代码对理解 DFM 有一定帮助

DFM 不是基于 UIKit

这是和 BarrageRenderer 差异最明显的地方。DFM 基于更底层的 Metal/OpenGL,使用哪个 Render 组件取决于手机型号是否支持。

Chronos

不管是 BarrageRenderer 还是 DFM 都有一定的局限性:他们很难在 iOS 和 Android 实现统一,最多保证理念一致,但实现需要依靠各自平台库支持。这个时候一个大一统的方案呼之欲出,他就说 Chronos。
Chronos 脱胎于谷歌的 Khronos , 在 B 站内部被封装成了 Framework,接受 TS 注入,所以内部应该是有个 JSC 容器。如此一来,我们只需要把 js 逻辑写好,其他部分交给 chronos 来绘制即可。Android 和 iOS 各自安装一份 Chronos 库文件相对来说就容易的多。

架构

chronos 有多层构成,业务层无需关注底层引擎,只需要写对应业务,实现业务和底层隔离,并且能跨平台。

架构图

底层实现

既然能解释运行 ts,那么肯定里面有 jsc。大概的底层实现如下:

底层实现

具体 API 有如下这些:

具体 API

我们来总结一下这份 API:

+------------------+------------------------------------------+--------------------------------------------------+
| 模块类别         | 关键类(Class / Interface)              | 职责说明                                         |
+------------------+------------------------------------------+--------------------------------------------------+
| 核心渲染         | Scene, Node, LabelNode, SpriteNode,      | 构建 2D 场景图,管理视觉元素层级、变换、绘制     |
|                  | ShapeNode, EffectNode                    |                                                  |
+------------------+------------------------------------------+--------------------------------------------------+
| 动画系统         | Action, MoveBy, RotateTo, Sequence,      | 提供可组合的动画指令,支持时间轴控制与缓动       |
|                  | BezierPath, EmitterNode                  |                                                  |
+------------------+------------------------------------------+--------------------------------------------------+
| 物理系统         | PhysicsWorld, PhysicsBody,               | 模拟刚体动力学,支持重力、碰撞、关节等行为       |
|                  | PhysicsJoint                             |                                                  |
+------------------+------------------------------------------+--------------------------------------------------+
| 资源管理         | Image, Font, Texture, Resources          | 加载、缓存、释放图片/字体等资源,避免重复加载    |
+------------------+------------------------------------------+--------------------------------------------------+
| 交互与输入       | TouchEvent, Sensor, Window               | 处理触摸、加速度计、陀螺仪等用户输入事件         |
+------------------+------------------------------------------+--------------------------------------------------+
| 高级图形能力     | Shader, RenderTexture, CustomCommand     | 支持自定义着色器、离屏渲染、GPU 特效             |
+------------------+------------------------------------------+--------------------------------------------------+
| 并行计算         | Worker                                   | 在独立线程执行 JS 逻辑,避免阻塞主线程           |
+------------------+------------------------------------------+--------------------------------------------------+
| 工具与辅助       | Vec2, Color, Rect, Math                  | 提供数学运算、颜色处理、几何结构等基础工具       |
+------------------+------------------------------------------+--------------------------------------------------+

手搓一个

为了模仿他的实现,我们可以手搓一个。

目录结构:

ChronosMiniDemo/
├── ChronosMiniDemo.xcodeproj/       ← Xcode 自动生成
└── ChronosMiniDemo/
    ├── AppDelegate.swift
    ├── ViewController.swift
    ├── SceneDelegate.swift          ← 若用 SceneDelegate
    ├── DanmakuBridge.h
    ├── DanmakuBridge.m
    └── main.bundle.js

文件 1:DanmakuBridge.h

// ChronosMiniDemo/DanmakuBridge.h
#import <Foundation/Foundation.h>

@interface DanmakuBridge : NSObject
- (void)setupInContext:(JSGlobalContextRef)ctx;
@end

文件 2:DanmakuBridge.m


// ChronosMiniDemo/DanmakuBridge.m
#import "DanmakuBridge.h"
#import <JavaScriptCore/JavaScriptCore.h>

// 全局保存 JS 回调(简化版)
static JSValueRef g_clickCallback = NULL;
static JSGlobalContextRef g_context = NULL;

static void addDanmaku(JSContextRef ctx, JSObjectRef function,
                       JSObjectRef thisObject, size_t argumentCount,
                       const JSValueRef arguments[], JSValueRef* exception) {
    if (argumentCount < 2) return;

    JSStringRef textJS = JSValueToStringCopy(ctx, arguments[0], NULL);
    JSStringRef colorJS = JSValueToStringCopy(ctx, arguments[1], NULL);

    char textBuf[256], colorBuf[256];
    JSStringGetUTF8CString(textJS, textBuf, sizeof(textBuf));
    JSStringGetUTF8CString(colorJS, colorBuf, sizeof(colorBuf));

    NSString *text = [NSString stringWithUTF8String:textBuf];
    NSString *color = [NSString stringWithUTF8String:colorBuf];

    NSLog(@"✅ [Native] 收到弹幕: %@, 颜色: %@", text, color);

    // 模拟 1.5 秒后点击
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (g_clickCallback) {
            JSStringRef clickText = JSStringCreateWithUTF8CString(textBuf);
            JSValueRef args[] = { JSValueMakeString(ctx, clickText) };
            JSObjectCallAsFunction(ctx, (JSObjectRef)g_clickCallback, NULL, 1, args, NULL);
            JSStringRelease(clickText);
        }
    });

    JSStringRelease(textJS);
    JSStringRelease(colorJS);
}

static void onDanmakuClick(JSContextRef ctx, JSObjectRef function,
                           JSObjectRef thisObject, size_t argumentCount,
                           const JSValueRef arguments[], JSValueRef* exception) {
    if (argumentCount > 0) {
        if (g_clickCallback) {
            JSValueUnprotect(ctx, g_clickCallback);
        }
        g_clickCallback = arguments[0];
        JSValueProtect(ctx, g_clickCallback);
        g_context = ctx;
        NSLog(@"✅ [Native] 已注册弹幕点击回调");
    }
}

@implementation DanmakuBridge

- (void)setupInContext:(JSGlobalContextRef)ctx {
    JSObjectRef bridgeObj = JSObjectMake(ctx, NULL, NULL);

    JSObjectSetProperty(ctx, bridgeObj,
                        JSStringCreateWithUTF8CString("addDanmaku"),
                        JSObjectMakeFunctionWithCallback(ctx, NULL, addDanmaku),
                        kJSPropertyAttributeReadOnly, NULL);

    JSObjectSetProperty(ctx, bridgeObj,
                        JSStringCreateWithUTF8CString("onDanmakuClick"),
                        JSObjectMakeFunctionWithCallback(ctx, NULL, onDanmakuClick),
                        kJSPropertyAttributeReadOnly, NULL);

    JSObjectSetProperty(ctx,
                        JSContextGetGlobalObject(ctx),
                        JSStringCreateWithUTF8CString("nativeBridge"),
                        bridgeObj,
                        kJSPropertyAttributeNone, NULL);
}

@end

文件 3:main.bundle.js(TS 编译结果)

// ChronosMiniDemo/main.bundle.js
(function() {
    'use strict';

    // 添加弹幕
    nativeBridge.addDanmaku("前方高能!", "#FF6600");

    // 监听点击
    nativeBridge.onDanmakuClick(function(text) {
        console.log("[JS] 用户点击了弹幕: " + text);
    });

})();

文件 4:ViewController.swift(替换原有)

// ChronosMiniDemo/ViewController.swift
import UIKit
import JavaScriptCore

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .systemBackground
        
        let label = UILabel(frame: CGRect(x: 20, y: 100, width: 300, height: 50))
        label.text = "查看控制台输出!"
        label.textAlignment = .center
        self.view.addSubview(label)

        // 启动 JS 引擎
        runDanmakuEngine()
    }

    private func runDanmakuEngine() {
        let context = JSGlobalContextCreate(nil)
        let bridge = DanmakuBridge()
        bridge.setupInContext(context!)

        // 加载 JS
        if let jsPath = Bundle.main.path(forResource: "main.bundle", ofType: "js"),
           let jsCode = try? String(contentsOfFile: jsPath, encoding: .utf8) {
            let jsStr = JSStringCreateWithCFString(jsCode as CFString)
            JSEvaluateScript(context, jsStr, nil, nil, 0, nil)
            JSStringRelease(jsStr)
        }

        JSGlobalContextRelease(context)
    }
}

最后看效果:
效果

其他方案

  • 腾讯的 PAG 可以实现酷炫的动画效果,简单的弹幕当然不在话下
  • gltf

引用

awesome-danmaku

B站弹幕库DanmakuFlameMaster源码浅析