美文网首页WebView
iOS源码补完计划--JavaScriptCore原理探究

iOS源码补完计划--JavaScriptCore原理探究

作者: kirito_song | 来源:发表于2017-12-14 17:28 被阅读688次

    从时间线来看、前端交互分为三个阶段。

    本帖不是用法教学贴

    主要针对JavaScriptCore工作原理进行探究

    首先、借助一套JSC开源源码(地址)。我们可以更直观的解析JSC。

    JS调用OC

    一个最简单的注册调用
    • oc
      - (void)configMyJSCore {
      
          //设置js环境
          JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
          self.context = context;    
          
          self.context[@"Log"] = ^(NSString *msg){
              NSLog(@"%@",msg);
          };
      }
    
    • js
    <body>
        <h1>WebViewJavascriptBridge Demo</h1>
        <button type="button"  onclick="JSCallOC1()">JS调用OC方法1</button>
    </body>
    <script type="text/javascript">
        function JSCallOC1 () {
            Log('JSCallOC1');
        }
    </script>
    

    点击button打印:

    2018-01-10 15:18:47.687046+0800 test[10151:397569] JSCallOC1
    

    那么、一行一行看呗。

    self.context[@"Log"] = ^(NSString *msg){
          NSLog(@"%@",msg);
    };
    

    JSContext是啥。字典么?肯定不是。
    但是在JSContext.h文件中。有这样两个方法。

    @interface JSContext (SubscriptSupport)
    
    /*!
    @method
    @abstract Get a particular property on the global object.
    @result The JSValue for the global object's property.
    */
    - (JSValue *)objectForKeyedSubscript:(id)key;
    
    /*!
    @method
    @abstract Set a particular property on the global object.
    */
    - (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;
    
    @end
    

    在全局对象上设置一个属性、以及获得一个属性。看着有点意思、把上面的代码换成这个写法试试。

    [self.context setObject:^(NSString *msg){
            NSLog(@"%@",msg);
    } forKeyedSubscript:@"Log"];
    

    一切正常。于是、源码出场。

    - (JSValue *)objectForKeyedSubscript:(id)key
    {
        return [self globalObject][key];
    }
    
    - (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key
    {
        [self globalObject][key] = object;
    }
    

    里面对[self globalObject]对象的某属性进行了一次赋值/取值。

    • globalObject是什么呢

    从文档可以看出、是window对象(如果没有window也代表一个作用域)。

    源码中

    - (JSValue *)globalObject
    {
        return [JSValue valueWithJSValueRef:JSContextGetGlobalObject(m_context) inContext:self];
    }
    
    /*!
    @method
    @abstract Creates a JSValue, wrapping its C API counterpart.
    @param value
    @param context
    @result The Objective-C API equivalent of the specified JSValueRef.
    */
    + (JSValue *)valueWithJSValueRef:(JSValueRef)value inContext:(JSContext *)context;
    

    该方法在某个Context作用域中、查询并返回一个包装成JSValue的JS对象。
    这里、就是在self.context的作用于中、查找并返回了一个GlobalObject对象。
    具体一点、在oc和js中分别打印:

    //oc中
    NSLog(@"oc---%@",[self.context globalObject]);
    //js中
    Log('js---'+window);
    

    结果:

    test[10409:454602] oc---[object Window]
    test[10409:454752] js---[object Window]
    

    进一步确认是不是同一个window?

    //oc中
    JSValue * document = [[self.context globalObject]objectForKeyedSubscript:@"document"];
    NSString * title = [[document objectForKeyedSubscript:@"title"] toString];
    NSLog(@"oc---%@",title);
    //js中
    Log('js---'+window.document.title);
    

    结果:

    test[10473:462274] oc---页面title
    test[10473:462411] js---页面title
    

    一模一样。

    所以。刚才提到的:

    里面对[self globalObject]对象的某属性进行了一次赋值/取值。
    

    实际上就是对html的window对象的某属性进行了赋值/取值。
    而H5中window对象的属性是什么、就是顶层变量、也叫全局变量。

    window.property = '顶层变量';
    alert(property);
    
    WechatIMG240.jpeg

    再深入一些、这个属性如何被添加的呢?

    ***JSValue.h***
    - (void)setValue:(id)value forProperty:(NSString *)property;
     
    ***JSValue.m***
    ***
    * Sets the value of the named property in the JavaScript object value.
    * Calling this method is equivalent to using the subscript operator with a string subscript in JavaScript. Use it to set or create fields or properties in JavaScript objects.
      Parameters    
    ***
    - (void)setValue:(id)value forProperty:(NSString *)propertyName
    {
        JSValueRef exception = 0;
        JSObjectRef object = JSValueToObject([_context     JSGlobalContextRef], m_value, &exception);
        if (exception) {
            [_context notifyException:exception];
            return;
        }
        //通过nsstr创建一个jsstr
        JSStringRef name = JSStringCreateWithCFString((CFStringRef)propertyName);
        JSObjectSetProperty([_context JSGlobalContextRef], object, name, objectToValue(_context, value), 0, &exception);
        JSStringRelease(name);
        if (exception) {
            [_context notifyException:exception];
            return;
        }
    }
    

    方法的解释大概翻译是

      调用此方法相当于在JavaScript中使用子脚本操作符。使用它来设置或创建JavaScript对象中的字段或属性。
    

    再具体一点:

    void JSObjectSetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSPropertyAttributes     attributes, JSValueRef* exception)
    {
        if (!ctx) {
            ASSERT_NOT_REACHED();
            return;
        }
        ExecState* exec = toJS(ctx);
        APIEntryShim entryShim(exec);
    
        JSObject* jsObject = toJS(object);
        Identifier name(propertyName->identifier(&exec->vm()));
        //将oc绑定的方法转化成js对象
        JSValue jsValue = toJS(exec, value);
    
        if (attributes && !jsObject->hasProperty(exec, name)) {
            PropertyDescriptor desc(jsValue, attributes);
            jsObject->methodTable()->defineOwnProperty(jsObject, exec, name, desc, false);
        } else {
            PutPropertySlot slot(jsObject);
            //向被注入的对象的methodTable中存入该方法、说明、名字等参数。
            jsObject->methodTable()->put(jsObject, exec, name, jsValue, slot);
        }
    
        if (exec->hadException()) {
            if (exception)
                *exception = toRef(exec, exec->exception());
            exec->clearException();
        }
    }
    

    将oc注册的方法、转化成js对象后、存入被注入对象的方法table中(忽然感觉很想runtime的机制了~)。


    OC调用JS

    本质上都是这个方法:

    JSValue *value1 = [self.jsContext evaluateScript:@"hello()"];
    
    ***JSContext.h***
    /*!
    @methodgroup Evaluating Scripts
    */
    /*!
    @method
    @abstract Evaluate a string of JavaScript code.
    @param script A string containing the JavaScript code to evaluate.
    @result The last value generated by the script.
    */
    - (JSValue *)evaluateScript:(NSString *)script;
    
    作用:尝试执行一段js代码。

    在具体一点的代码:

    ***JSBase.cpp***
    JSValueRef JSEvaluateScript(JSContextRef ctx, JSStringRef script, JSObjectRef thisObject, JSStringRef sourceURL, int startingLineNumber, JSValueRef* exception)
    {
        if (!ctx) {
            ASSERT_NOT_REACHED();
            return 0;
        }
        ExecState* exec = toJS(ctx);
        APIEntryShim entryShim(exec);
    
        JSObject* jsThisObject = toJS(thisObject);
    
        // evaluate sets "this" to the global object if it is NULL
        JSGlobalObject* globalObject = exec->vmEntryGlobalObject();
        SourceCode source = makeSource(script->string(), sourceURL->string(), TextPosition(OrdinalNumber::fromOneBasedInt(startingLineNumber), OrdinalNumber::first()));
    
        JSValue evaluationException;
        JSValue returnValue = evaluate(globalObject->globalExec(), source, jsThisObject, &evaluationException);
    
        if (evaluationException) {
            if (exception)
                *exception = toRef(exec, evaluationException);
            return 0;
        }
    
        if (returnValue)
            return toRef(exec, returnValue);
    
        // happens, for example, when the only statement is an empty (';') statement
        return toRef(exec, jsUndefined());
    }
    ***completion.cpp***
    JSValue evaluate(ExecState* exec, const SourceCode& source, JSValue thisValue, JSValue* returnedException)
    {
        JSLockHolder lock(exec);
        RELEASE_ASSERT(exec->vm().identifierTable == wtfThreadData().currentIdentifierTable());
        RELEASE_ASSERT(!exec->vm().isCollectorBusy());
    
        CodeProfiling profile(source);
    
        ProgramExecutable* program = ProgramExecutable::create(exec, source);
        if (!program) {
            if (returnedException)
                *returnedException = exec->vm().exception();
    
            exec->vm().clearException();
            return jsUndefined();
        }
    
        if (!thisValue || thisValue.isUndefinedOrNull())
            thisValue = exec->vmEntryGlobalObject();
        JSObject* thisObj = jsCast<JSObject*>(thisValue.toThis(exec, NotStrictMode));
        JSValue result = exec->interpreter()->execute(program, exec, thisObj);
    
        if (exec->hadException()) {
            if (returnedException)
                *returnedException = exec->exception();
    
            exec->clearException();
            return jsUndefined();
        }
    
        RELEASE_ASSERT(result);
        return result;
    }
    
    }    
    

    讲道理翻到这我已经没啥耐心逐字逐句的撸了~毕竟这个开源包是不能运行的。
    简单概括一下:

    • 通过上下文获取JS全局对象。
    • 将全局对象、JS代码(转换成JsStr之后)等。交由虚拟机进行搜索并获得上下文中可执行列表。
    • 执行可执行列表获得返回值。(具体的执行代码可以自己去源码里挑战一下)

    由于源码的包直接从开源的WebKit中获取。缺少项目文件、无法进行编译。
    所以并不保证这篇文章具有100%的正确性。
    只是因为网上没找到源码解析相关的帖子、又很好奇。只能自己动手。
    如有错误、还望指正。


    最后

    本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果不吝赐教小弟更加感谢。

    相关文章

      网友评论

        本文标题:iOS源码补完计划--JavaScriptCore原理探究

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