Ejecta 源码解析

导语

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

待补充

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容