美文网首页
Ejecta 源码解析

Ejecta 源码解析

作者: VernonVan | 来源:发表于2020-11-24 16:20 被阅读0次

    导语

    Ejecta is a fast, open source JavaScript, Canvas & Audio implementation for iOS (iPhone, iPod Touch, iPad) and tvOS (Apple TV). Think of it as a Browser that can only display a Canvas element.

    Ejecta 是 JavaScript Canvas 的一个 iOS 实现方案,同时支持了 Canvas 2D 和 WebGL,底层是用 OpenGL 进行渲染的,可以用来将原生 OpenGL 能力提供给前端自由地绘制内容、实现灵活的动画动效甚至小游戏等。

    效果展示

    官方示例

    类图

    类图

    关键类

    类名 简介 备注
    EJJavaScriptView UIView 的子类,负责展示渲染结果、派发触摸/运动等事件、执行 JS 方法、更新 RunLoop
    EJClassLoader EJJavaScriptView 持有,负责懒加载的方式初始化 EJBindingXXX 的各种子类并提供给 JS 使用 底层用的是 OC 的 Runtime 机制获取对应方法并绑定到 JS 类上
    EJBindingCanvas EJBindingBase 的子类,主要暴露了 width/height/getContext 等属性给 JS,持有了 2D/WebGL 的上下文 var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d');
    EJBindingCanvasContext2D EJBindingBase 的子类,暴露了 fillRect/strokeRect/drawImage 等一系列 Canvas 2D 的 API 给 JS 支持的方法列表可以看这里
    EJCanvasContext2DScreen EJCanvasContext2D 的子类,Canvas 2D 渲染工作的实际执行者
    EJBindingCanvasContextWebGL EJBindingBase 的子类,暴露了 bindBuffer/createFramebuffer/drawArrays 等一系列 Canvas 2D 的 API 给 JS,会支持操作 OpenGL 进行绘制 支持的方法列表可以看这里
    EJCanvasContextWebGLScreen EJCanvasContextWebGL 的子类,负责将 WebGL 的内容最终渲染上屏

    JavaScript 和 Obj-C 相互通信

    我们先来看一下 Web API 接口规范中使用 Canvas 2D 的示例

    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = 'green';
    ctx.fillRect(20, 10, 150, 100);
    

    怎么才能在 JS 调用 document.getElementById('canvas') 时调用到 Obj-C 代码呢,又怎么才能提供 getContext 方法给这个 canvas 对象呢?
    这里就需要用到 JavaScriptCore 的 JSClassCreate 方法了。我们看一下 Ejecta 的实现:

    JSClassDefinition classDef = kJSClassDefinitionEmpty;
    classDef.className = class_getName(class) + sizeof(EJ_BINDING_CLASS_PREFIX)-1;
    classDef.finalize = EJBindingBaseFinalize;
    classDef.staticValues = values;
    classDef.staticFunctions = functions;
    
    JSClassRef jsClass = JSClassCreate(&classDef);
    JSObjectRef obj = JSObjectMake( ctx, jsClass, NULL );
    JSObjectSetPrivate( obj, (void *)[instance retain] );
    

    通过 JSClassCreate 可以创建一个 JS 的类,还需要通过 staticFunctions 设置这个类的方法、staticValues 设置这个类的属性,最后通过 JSObjectSetPrivate 将这个类关联到 JS 上。
    Ejecta 这里比较巧妙的是通过 EJ_BIND_FUNCTION 这个宏来实现方法的绑定:

    #define EJ_BIND_FUNCTION(NAME, CTX_NAME, ARGC_NAME, ARGV_NAME) \
        \
        /* The C callback function for the exposed method and class method that returns it */ \
        static JSValueRef _func_##NAME( \
            JSContextRef ctx, \
            JSObjectRef function, \
            JSObjectRef object, \
            size_t argc, \
            const JSValueRef argv[], \
            JSValueRef* exception \
        ) { \
            id instance = (id)JSObjectGetPrivate(object); \
            JSValueRef ret = _EJ_CALL_BOUND_OBJC_FUNC(instance, _func_##NAME:argc:argv:, ctx, argc, argv); \
            return ret ? ret : ((EJBindingBase *)instance)->scriptView->jsUndefined; \
        } \
        __EJ_GET_POINTER_TO(_func_##NAME)\
        \
        /* The actual implementation for this method */ \
        - (JSValueRef)_func_##NAME:(JSContextRef)CTX_NAME argc:(size_t)ARGC_NAME argv:(const JSValueRef [])ARGV_NAME
    

    绑定 getContext 方法的代码如下:

    EJ_BIND_FUNCTION(getContext, ctx, argc, argv) {
        if( argc < 1 ) { return NULL; };
        NSString *type = JSValueToNSString(ctx, argv[0]);
        ...
    

    预处理器会将 EJ_BIND_FUNCTION(getContext, ctx, argc, argv) 展开成以下几个函数:

    1. static JSValueRef _func_getContext( ... )
    2. + (void *)_ptr_to_func_getContext
    3. - (JSValueRef)_func_getContext:(JSContextRef)CTX_NAME argc:(size_t)ARGC_NAME argv:(const JSValueRef [])ARGV_NAME

    staticFunctions 接受的参数是 JSObjectCallAsFunctionCallback 类型的 C++ 函数指针,这里传入函数 2,函数 2 简单地把函数 1 作为函数指针返回出去,而函数 1 则是通过 Runtime 的 objc_msgSend 调用 Obj-C 的函数 3 并传递对应的参数,这样我们就能在函数 3 中直接使用熟悉的 Obj-C 完成对应的逻辑。

    完成这一部分之后,其实还存在一个问题:我们需要手动将这些 _ptr_to_func_xxx 函数指针逐个添加到 staticFunctions 数组中。当需要绑定的函数很多的时候不仅繁琐,而且很容易出错。那么怎么才能将这一工作自动化呢?
    Ejecta 再一次使用了 Runtime 解决这个问题:

    - (EJLoadedJSClass *)loadJSClass:(id)class {
        Class base = EJBindingBase.class;
        for( Class sc = class; sc != base && [sc isSubclassOfClass:base]; sc = sc.superclass ) {
            u_int count;
            Method *methodList = class_copyMethodList(object_getClass(sc), &amp;count);
            for (int i = 0; i < count ; i++) {
                SEL selector = method_getName(methodList[i]);
                NSString *name = NSStringFromSelector(selector);
                if( [name hasPrefix:@"_ptr_to_func_"] ) {
                    [methods addObject:[name substringFromIndex:sizeof("_ptr_to_func_")-1] ];
                }
                free(methodList);
        }
         
        JSStaticFunction *functions = calloc( methods.count + 1, sizeof(JSStaticFunction) );
        for( int i = 0; i < methods.count; i++ ) {
            NSString *name = methods[i];
            SEL call = NSSelectorFromString([@"_ptr_to_func_" stringByAppendingString:name]);
            functions[i].callAsFunction = (JSObjectCallAsFunctionCallback)[class performSelector:call];
        }
        
        JSClassDefinition classDef = kJSClassDefinitionEmpty;
        classDef.staticFunctions = functions;
        JSClassRef jsClass = JSClassCreate(&classDef);
        ...
    }
    

    Ejecta 利用了 Runtime 的自省机制,用 class_copyMethodList 函数在运行时获取了类的所有方法,根据特定前缀 _ptr_to_func_ 将需要暴露给 JS 的方法自动地绑定到类上。相当巧妙的实现方式。

    OpenGL

    待补充

    相关文章

      网友评论

          本文标题:Ejecta 源码解析

          本文链接:https://www.haomeiwen.com/subject/sgznbktx.html