Cocos源码分析(二)

本篇记录下Cocos引擎在原生平台的启动流程,由于原生和web 版本的运行机制完全不同,底层有c++ 编写,而js变成了类似胶水层的东西,所以在调试运行时,回到看到js所产生的一些列“指令”被发送到c++层,并由原生层执行后反馈给js。这些部分在后面会一点一点的剖析。

我用的是macOS输出版本,因为这个目标平台相对来说代码比较干净。运行流程在iOS和android(本人略讨厌NDK)平台相差不大。

程序执行开始于 AppDelegate.mm 中的 applicationDidFinishLaunching 函数,内容如下:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    NSRect rect = NSMakeRect(200, 200, 960, 640);
    _window = [[NSWindow alloc] initWithContentRect:rect
                                          styleMask:NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable
                                            backing:NSBackingStoreBuffered
                                              defer:NO];
    if (!_window) {
        NSLog(@"Failed to allocated the window.");
        return;
    }
    
    _window.title = @"Cocos creator 3D Game";
    
    ViewController* viewController = [[ViewController alloc] initWithSize: rect];
    _window.contentViewController = viewController;
    _window.contentView = viewController.view;
    [_window.contentView setWantsBestResolutionOpenGLSurface:YES];
    [_window makeKeyAndOrderFront:nil];
        
    _game = new Game(rect.size.width, rect.size.height);
    _game->init();

    [[NSNotificationCenter defaultCenter] addObserver:self 
        selector:@selector(windowWillMiniaturizeNotification)name:NSWindowWillMiniaturizeNotification 
        object:_window];
    [[NSNotificationCenter defaultCenter] addObserver:self 
        selector:@selector(windowDidDeminiaturizeNotification)name:NSWindowDidDeminiaturizeNotification 
        object:_window];
}

默认的窗口尺寸为960,640,代码中较为简单,仅是对窗口属性进行创建和基本设置。和cocos引擎相关的只有2行。

_game = new Game(rect.size.width, rect.size.height);
_game->init();

事例化Game对象,并调用其init方法。Game类暴露的接口也相当简单,其定义在 Game.h 中。

class Game : public cc::Application
{
public:
    /**
     * width and height in logical pixel unit
     */
    Game(int width, int height);
    virtual bool init() override;
    virtual void onPause() override;
    virtual void onResume() override;
};

我们来拆解分析一下构造函数的具体逻辑和init函数中都做了那些事情。

由于 Game 继承自 cc::Application ,同时也并未重写构造函数,所以我们需要查阅 cc::Application 源码。由于此次发布为macOS平台,源码在 cocos/platform/mac/Application-mac.mm 中定义。函数实现如下:

Application::Application(int width, int height) {
    Application::_instance = this;

    _viewLogicalSize.x = width;
    _viewLogicalSize.y = height;

    _scheduler = std::make_shared<Scheduler>();
    EventDispatcher::init();

#ifndef CC_USE_METAL
    _timer = [[MyTimer alloc] initWithApp:this fps:_fps];
#endif
    
    [[[[[NSApplication sharedApplication] delegate] getWindow] contentView] start];
}

这里主要是赋值窗口的尺寸,并且通过 make_shared 初始化 Scheduler。Scheduler调度器是非常核心关键的内容,后面会详细分析调度器。EventDispatcher 则是事件派发模块。

init 函数里面的操作相对较多,因为涉及到js层的东西,我们一步一步来。

第一步 调用 cc::Application::init();,其函数内容如下:

bool Application::init() {
    se::ScriptEngine *se = se::ScriptEngine::getInstance();
    se->addRegisterCallback(setCanvasCallback);

#ifndef CC_USE_METAL
    [_timer start];
#endif

    return true;
}

此时主要是注册Canvas的回调函数,而 setCanvasCallback 函数的实现如下:

bool setCanvasCallback(se::Object *global) {
    auto viewLogicalSize = Application::getInstance()->getViewLogicalSize();
    se::ScriptEngine *se = se::ScriptEngine::getInstance();
    char commandBuf[200] = {0};
    NSView *view = [[[[NSApplication sharedApplication] delegate] getWindow] contentView];
    sprintf(commandBuf, "window.innerWidth = %d; window.innerHeight = %d; window.windowHandler = 0x%" PRIxPTR ";",
            (int)(viewLogicalSize.x),
            (int)(viewLogicalSize.y),
            (uintptr_t)view);
    se->evalString(commandBuf);
    return true;
}

核心逻辑为每次窗体发生尺寸变化后,执行 commandBuf ,这是一段简单的js代码。

第二步 jsb_set_xxtea_key(""); 设置加密密钥。

第三步 jsb_init_file_operation_delegate 初始化js绑定中的文件操作委托函数。实现了 onGetDataFromFileonGetStringFromFileonGetFullPathonCheckFileExist 函数。

第四步 如果是调试环境,则开启远程debugger功能。

第五步 设置js异常回调,默认情况下会将js中的异常打印到控制台中,这里是一个宏CC_LOG_ERROR

第六步 注册所以引擎模块 jsb_register_all_modules,源码较长,但全部是调用 se->addRegisterCallback 方法的。

第七步 启动ScriptEngine,这里涉及到对v8的包装,后面进行详细分析。

第八步 这也算得上是最后一步了,通过本地文件,加载并执行js文件,分别是jsb-adapter/jsb-builtin.jsmain.js两个文件。

到此为止,所有原生层面的逻辑基本准备完毕,接下来主要是执行main.js中的逻辑。