background

As an important branch of mobile client technology, dynamic has been actively explored by the industry. Currently, popular dynamic solutions in the industry, such as Facebook’s React Native and Alibaba’s Weex, all adopt front-end DSL solutions. However, their smooth operation on iOS system depends on one person behind them: JavaScriptCore (JSCore) builds a bridge between Objective-C (OC) and JavaScript (JS). Whether it is these popular dynamic solutions, WebView Hybrid solutions, or the previously popular JSPatch, JSCore has played an important role. As an iOS developer, understanding JSCore is becoming an essential skill.

Start with the browser

After iOS 7, JSCore was made available to developers as a system-level Framework by Apple. JSCore is an important part of WebKit, Apple’s browser engine, which has been around for years. If you want to go back to the origin and explore the mystery of JSCore, you should start from the birth of JS language and its most important host -Safari browser.

A brief history of JavaScript

JavaScript was created in 1995 by Brendan Eich of Netscape, which at the time dominated the browser market.

Twenty years ago, the experience of browsing the web was very poor, because the browser at that time was almost only capable of displaying pages, and there was no interactive logic processing ability with users. So even if a required field is empty, it needs to be verified by the server, and the response is not given until the result is returned. In addition, the network speed at that time is very slow, maybe half a minute later, the return result is telling you that a required field is not filled in. So Brendan spent ten days writing JavaScript that was interpreted and executed by the browser, and since then the browser has had some basic interactive processing capabilities and form data validation capabilities.

And Brendan probably didn’t expect it, more than 20 years later. JS, the dynamic scripting language that explains execution, has not only become the “orthodoxy” of the front-end world, but has also invaded the back-end development world, ranking among the top three programming languages, behind Python and Java. How to interpret and execute JS is the core technology of each engine. Some of the most popular JS engines currently on the market are Google V8 (for Android and Google Chrome) and JSCore (for iOS and Safari).

WebKit

We use browsers every day for work and play. The core of what makes a browser work is the browser kernel. Every browser has its own kernel, and Safari’s kernel is WebKit. WebKit was born in 1998 and opened source by Apple in 2005. Google Blink was also developed on WebKit’s branch.

WebKit consists of several important modules. We can have an overall understanding of WebKit through the following figure:

To put it simply, WebKit is a page rendering and logic processing engine. Front-end engineers take HTML, JavaScript and CSS as inputs and output Web pages that we can see and operate after WebKit processing. As you can see from the figure above, WebKit consists of four parts boxed in. The main ones are WebCore and JSCore (or any other JS engine), which we’ll cover in two small chapters. In addition, WebKit Embedding API is responsible for the interaction between browser UI and WebKit, while WebKit Ports make WebKit more easily ported to various operating systems and platforms and provide some interfaces to call Native Library. For example, at the rendering level, Safari is handled by CoreGraphics on iOS, and Webkit is handled by Skia on Android.

WebCore

In the WebKit composition diagram above, we can see that only WebCore is red. This is because up to now,WebKit has many branches and major manufacturers have also carried out a lot of optimization transformation, but WebCore is the part shared by all WebKit. WebCore is the most coded part of WebKit and the most core rendering engine in WebKit. Let’s take a look at the entire WebKit rendering process:

First, the browser locates a bunch of HTML, CSS, and JS resource files through the URL, and feeds the resource files to WebCore through the loader (the implementation of this loader is also complicated, so I won’t go into details here). Then the HTML Parser will parse the HTML into a DOM tree, and the CSS Parser will parse the CSS into a CSSOM tree. Finally, the two trees are combined to generate the final required rendering tree. After layout, the rendering tree is rendered and output to the screen through the rendering interface of specific WebKit Ports, which becomes the final Web page presented to the user.

JSCore

An overview of the

Finally, the subject of this episode, JSCore. JSCore is the default JS engine built into WebKit. It is the default JS engine built into WebKit because many browser engines based on the WebKit branch develop their own JS engines, most notably Chrome’s V8. These JS engines all have the same mission, which is to interpret and execute JS scripts. As can be seen from the above rendering flow chart, there is a correlation between JS and DOM tree, because the main function of JS script in browser is to manipulate and interact with DOM tree. Again, let’s take a look at how it works:

As you can see, interpreted languages are much simpler in terms of flow than statically compiled languages that generate syntax trees and then link, load and generate executable files. The box on the right of this flowchart is the JSCore component: Lexer, Parser, LLInt, and THE JIT component (the JIT component is highlighted in orange because not all JSCore components have a JIT component). Next, we will introduce each part of the workflow, which is divided into three parts: lexical analysis, grammatical analysis and interpretation execution.

PS: Strictly speaking, there is no compiled or interpreted type of language, because language is only some abstract definitions and constraints, and does not require specific implementation and execution. Here, JS is an “interpreted language”, but JS is generally interpreted and executed dynamically by THE JS engine, rather than the attributes of the language itself.

Lexical analysis — Lexer

Lexical analysis is easy to understand. It is the process of breaking a piece of source code we have written into Token sequences. This process is also called lexical segmentation. At JSCore, lexical analysis is done by lexers (some compilers or interpreters call word segmentation Scanner).

Here’s a very simple C expression:

sum = 3 + 2; 
Copy the code

Tokenization yields the following table:

The element Tag type
sum identifier
= The assignment operator
3 digital
+ Addition operator
2 digital
; End of the statement

This is the result of lexical analysis, but lexical analysis does not focus on the relationship between each Token, whether or not it matches, but merely separates them and waits for grammatical analysis to “string” the tokens together. Lexical analysis functions are usually called by parsers. In JSCore, the Lexer code is mainly concentrated in parser/ lexer. h, lexer. CPP.

Parsing — Parser

Like human language, when we speak, we follow conventions, communication habits that follow a certain grammar to say word after word. That’s analogous to a computer language, where a computer has to understand both a computer language and the syntax of a statement. For example, the following JS statement:

var sum = 2 + 3;
var a = sum + 5;
Copy the code

Parser parses the token sequences generated after Lexer analysis and generates a corresponding abstract syntax tree (AST). What does the tree look like? One site recommended here is esprima Parser, which generates the AST we need immediately by typing in JS statements. For example, the above statement generates a tree like this:

After that, ByteCodeGenerator generates the JSCore bytecode from the AST to complete the syntax parsing step.

Explain execution — LLInt and JIT

JS source code through lexical analysis and syntax analysis of these two steps, into bytecode, in fact, through any program language must go through the steps – compilation. However, unlike OC code that we compile and run, JS does not generate object code or executable files stored in memory or hard disk after compilation. The resulting instruction bytecode is immediately interpreted and executed line by line by JSCore virtual machine.

Running instruction ByteCode is a very core part of JS engine, and the optimization of JS engine is mainly focused on it. JSByteCode interpretation execution is a very complex system, especially with the addition of OSR and multi-level JIT technology, the whole interpretation execution becomes more and more efficient, and the whole ByteCode execution has a good balance between low latency and high throughput: Low-latency LLInt interprets ByteCode execution, and when repeated calls or recursions occur, loops and other conditions will be switched to the JIT via OSR to interpret ByteCode execution (depending on the trigger condition, it will be dynamically interpreted into a different JIT) to speed up the execution. As this part of the content is more complex, and not the focus of this article, so only a brief introduction, not in-depth discussion.

JSCore notable features

In addition to the above parts, JSCore has several noteworthy features.

Register-based instruction set structure

JSCore uses registrie-based instruction set structure, which is more efficient than stack-based instruction set structure (such as some JVM implementations) because there is no need to push the results of operations on and off the stack frequently. However, this architecture also causes a higher memory overhead. In addition, there is also a problem of poor portability, because the virtual registers in virtual machines need to match the REGISTERS of the CPU in real machines, and the real CPU registers may be insufficient.

Register-based instruction set structures are usually three-address or two-address instruction sets, for example:

i = a + b; // add I, a, b; // Add the values in register A and register B to register ICopy the code

In the instruction set of three addresses, the operation process is to put A and B into two registers respectively, and then sum the values of these two registers and store them in the third register. This is the three-address instruction operation.

Stack-based instruction sets are generally zero-address instruction sets, because their operations do not rely on specific registers, but use the stack of operands and specific operators to complete the entire operation.

Single thread mechanism

It is worth noting that the whole JS code is executed in a thread, it is not like OC, Java and other languages we use, in their own execution environment can apply for multiple threads to deal with some time-consuming tasks to prevent blocking the main thread. JS code itself does not have the ability to multithread tasks. But why does JS also have multithreaded asynchrony? Powerful event-driven mechanism is the key to enable JS to handle multithreading.

Event-driven mechanism

As mentioned earlier, JS was created to allow browsers to have some interactive, logical processing capabilities. The interaction between JS and browser is realized through events. For example, when the browser detects a user click, it will send a click event to inform THE JS thread to deal with the event. Through this feature, we can also make JS asynchronous programming, simply speaking, when the time-consuming task is encountered, JS can throw the task to a WebWorker provided by the JS host to deal with. When the worker thread finishes processing, it sends a message to let the JS thread know that the task has been completed, and executes the corresponding event handler on the JS thread. (Note, however, that since the worker thread and JS thread are not in the same runtime environment, they do not share a scope, so the worker thread cannot manipulate window and DOM.)

The communication mechanism between JS threads and worker threads, as well as browser events, is called EventLoop, similar to iOS Runloop. It has two concepts, a Call Stack and a Task Queue. When the worker thread completes the asynchronous Task, it pushes the message to the Task Queue, which is the callback function at registration time. When the Call Stack is empty, the main thread takes a message from the Task Queue into the Call Stack to execute, and the JS main thread repeats this action until the message Queue is empty.

The above diagram roughly describes the event-driven mechanism of JSCore, and the whole JS program actually runs like this. This is similar to the idle iOS Runloop. When a port-based Source event wakes up the Runloop, it processes all Source events in the current queue. JS event-driven, and message queue is actually “similar”. It is because of the existence of worker thread and event-driven mechanism that JS has multithreaded asynchronous ability.

In the iOS JSCore

After iOS7, Apple wrapped JSCore in WebKit in Objective-C and made it available to all iOS developers. JSCore framework provides the ability to call JS programs for Swift, OC and C language apps. We can also use JSCore to insert custom objects into the JS environment.

There are several places in iOS where JSCore can be used, such as JSCore wrapped in UIWebView, JSCore wrapped in WKWebView, and JSCore provided by the system. In fact, even though they are both JSCore, there are many differences between them. As the language evolves, there are more and more JS hosts, a variety of browsers, and even Node.js (which runs on V8), which is common on the server side. JSCore has evolved into different versions depending on the use scenario and the WebKit team’s own continuous optimization. In addition to the old version of JSCore, there’s Nitro (SquirrelFish) in Safari, WKWebView, announced in 2008, and more. In this article, we mainly introduce the JSCore Framework of iOS system.

The iOS official documentation of JSCore is very simple, in fact, the main purpose of JSCore is to provide App with the ability to call JS scripts. We started with a peek through the JSCore Framework’s 15 open header files, as shown below:

At first glance, there are many concepts. However, apart from some common header files and some very detailed concepts, there are not many commonly used concepts. The author thinks it is necessary to understand only four concepts: JSVM, JSContext, JSValue and JSExport. Since many articles have been written about these concepts, this article tries to explain them from different perspectives (such as principles, extension and contrast).

JSVirtualMachine

An instance of JSVirtualMachine (hereinafter referred to as JSVM) represents a self-contained JS runtime environment, or a set of RESOURCES required to run JS. This class has two main uses: supporting concurrent JS calls and managing memory for bridge objects between JS and Native.

JSVM is the first concept we will learn. JSVM provides the underlying resources for JavaScript execution, and from the literal translation of the class name, a JSVM represents a JS virtual machine, we also mentioned the concept of virtual machine above, so we first discuss what is a virtual machine. Let’s start by looking at the (probably) most famous virtual machine, the JVM (Java Virtual Machine). The JVM does two main things:

  1. The first thing it does is take the Bytecodes generated by the JavaC compiler (which are essentially the JVM’s virtual machine instructions) and generate the machine instructions that each machine needs to make the Java program executable (see figure below).
  2. In the second step, the JVM is responsible for the memory management, GC, interface between Java programs and Native (i.e. C,C++), and so on.

In terms of functions, a high-level language VIRTUAL machine is mainly divided into two parts, one is the interpreter part, which is used to run bytecodes generated by high-level language compilation, and the other part is the Runtime Runtime, which is responsible for memory space development and management during the Runtime. In fact, JSCore is often thought of as a JS-optimized virtual machine that does something similar to the JVM, except that it does more of the work of compiling JS source code into bytecode than statically compiled Java.

Since JSCore is considered a virtual machine, what is JSVM? In essence, JSVM is an abstract JS virtual machine that developers can manipulate directly. In the App, we can run multiple JSVMS to perform different tasks. And each JSContext (described in the next section) is subordinate to a JSVM. However, it is important to note that each JSVM has its own separate heap space, and GC can only handle objects inside JSVM (the next section explains the JS GC mechanism briefly). Therefore, values cannot be passed between different JSVMS.

Also of note, in the previous section, we mentioned the single threaded JS mechanism. This means that in a JSVM, only one thread can run JS code, so we cannot use JSVM for multi-threading JS tasks. If we need to multithread the JS task scenario, we need to generate multiple JSVMS at the same time, so as to achieve the purpose of multithreading.

JS GC mechanism

JS also doesn’t require us to manage memory manually. JS memory management uses the Tracing Garbage Collection mechanism. Different from OC reference counting, Tracing Garbage Collection is a reference chain maintained from GCRoot (Context). Once the reference chain cannot reach an object node, the object will be reclaimed. As shown below:

JSContext

A JSContext represents a JS execution environment. We can create a JSContext to call JS scripts, access some JS-defined values and functions, and provide an interface for JS to access Native objects and methods.

JSContext is one of the concepts that we use a lot when we actually use JSCore. Context is a concept we’ve all seen in more or less other development scenarios, and it’s most often translated as “Context.” So what is context? For example, in an article, we see a sentence: “He ran out as fast as he could.” But if we don’t look at the context, we don’t know what the sentence really means: Who ran out? Who is he? Why did he run?

Writing a programming language that a computer understands is similar to writing an essay; we need a “context” to run any statement. Such as the introduction of previous external variables, global variables, function definitions, allocated resources, and so on. With this information, we can execute each line of code accurately.

Similarly, JSContext is the execution environment of JS language, all JS code execution must be in a JSContext, the same is true in WebView, we can obtain the JSContext of WebView by KVC. It is very simple to run a piece of JS code through JSContext, as in the following example:

    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"var a = 1; var b = 2;"];
    NSInteger sum = [[context evaluateScript:@"a + b"] toInt32]; //sum=3Copy the code

With the evaluateScript API, we can execute JS code in OC with JSContext. Its return value is the last value generated in JS, wrapped in a JSValue belonging to the current JSContext (described in the next section).

We can also use KVC to stuff JSContext with a number of global objects or functions:

    JSContext *context = [[JSContext alloc] init];
		context[@"globalFunc"] =  ^() {
        NSArray *args = [JSContext currentArguments];
        for (id obj in args) {
            NSLog(@"Got argument :%@", obj); }}; context[@"globalProp"] = @"Global variable string";
   [context evaluateScript:@"globalFunc(globalProp)"]; // Console output: "Got arguments: global variable string"Copy the code

This is a very useful and important feature, and there are many well-known jScore-enabled frameworks such as JSPatch that use this feature to achieve some very clever things. We’re not going to talk about what we can do with it here, but how it actually works. In the JSContext API, there is one notable read-only property — globalObject of type JSValue. It returns the global object that is currently executing JSContext, for example in WebKit, JSContext would return the current Window object. And this global object is actually the core of the JSContext, when we’re evaluating and assigning to the JSContext via KVC, we’re actually interacting with this global object, almost everything is inside the global object, so to speak, JSContext is just a shell of a globalObject. For both examples, this article takes the globalObject of the Context and converts it into an OC object, as shown below:

You can see that this globalObject holds all the variables and functions, which reinforces the above statement (why globalObject corresponds to OC objects is of type NSDictionary is explained in the next section). So we can also draw another conclusion, JS so-called global variables, global functions are nothing but global object properties and functions.

It is also worth noting that each JSContext is subordinate to a JSVM. We can obtain the JSVM of the current JSContext binding via the read-only property of JSContext — virtualMachine. JSContext and JSVM are many-to-one. A JSContext can only be bound to one JSVM, but a JSVM can hold multiple JsContexts at the same time. As mentioned above, each JSVM only has one thread to execute JS code at the same time, so in summary, the process of simply running JS code through JSCore and obtaining return value in Native layer is roughly as follows:

JSValue

A JSValue instance is a reference pointer to a JS value. We can use the JSValue class to convert between OC and JS underlying data types. We can also use this class to create JS objects that wrap Native custom classes, or JS objects that implement JS methods provided by Native methods or blocks.

In the JSContext section, we touched on a number of variables of type JSValue. In the JSContext section, we learned that we can easily manipulate JS global objects by KVC, and can directly obtain the return value of the result of the JS code execution (and each JS value exists in an execution environment, that is, each JSValue exists in a JSContext. This is the scope of JSValue), all because JSCore helps us to use JSValue in the underlying automatic OC and JS type conversion.

JSCore offers the following 10 types of swaps:

   Objective-C type  |   JavaScript type
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock 		   |   Function object 
          id         |   Wrapper object 
        Class        | Constructor object
Copy the code

It also provides a corresponding interchange API (excerpt) :

+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context;
+ (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context;
- (NSArray *)toArray;
- (NSDictionary *)toDictionary;
Copy the code

Before talking about type conversion, we first understand the JS language variable types. According to ECMAScript, there are two types of values in JS. One is a primitive type value, which refers to a simple piece of data. The second type is reference-type values, which refer to objects that may be composed of multiple values. Basic type values include “undefined”, “nul”, “Boolean”, “Number”, “String” (yes, String is also a basic type), and other reference types. There shouldn’t be much to say about the first five basic types of swaps. We’ll focus on the interchangeability of reference types:

NSDictionary <–> Object

In the previous section, we converted JSContext’s globalObject to an OC object and found it to be of type NSDictionary. To understand this transformation, let’s take a brief look at the object-oriented nature of THE JS language. In JS, an object is an instance of a reference type. Unlike OC and Java, which we are familiar with, an object is not an instance of a class because there is no concept of a class in JS. ECMA defines an object as an unordered collection of properties that can contain base values, objects, or functions. From this definition, we can see that objects in JS are unordered key-value pairs, similar to NSDictionary in OC, or HashMap in Java.

	var person = { name: "Nicholas",age: 17}; NSDictionary *person = @{@"name": @"Nicholas"The @"age": @}; // The person dictionary in OCCopy the code

In the example code above, I used a similar approach to create objects in JS (called object literals in JS) and NSDictionary in OC, which I believe makes it easier to understand both transformations.

NSBlock <–> Function Object

In the example in the previous section, I assigned a “globalFunc” Block to JSContext, which can be called directly in JS code as a function. I can also use the “typeof” keyword to determine globalFunc’s type in JS:

    NSString *type = [[context evaluateScript:@"typeof globalFunc"] toString]; //typeThe value of"function"
Copy the code

Using this example, we can also see that the Block object passed into JS has been converted to type “function”. The concept of “Function Object” can be somewhat obscure to us developers who are used to writing traditional object-oriented languages. In fact, the JS language, in addition to the basic type, is reference type. A Function is actually an object of type “Function”; each Function name is actually a reference to a Function object. For example, we can define a function in JS like this:

	var sum = function(num1,num2){
		return num1 + num2; 
	}
Copy the code

We can also define a function like this (not recommended) :

	var sum = new Function("num1"."num2"."return num1 + num2");
Copy the code

In the second way, we can intuitively understand that a Function is also an object. Its constructor is Function, and the Function name is just a pointer to the object. NSBlock is a class that wraps a pointer to a Function, and JSCore converts a Function Object into an NSBlock Object, which is appropriate.

JSExport

Implementing the JSExport protocol opens OC classes and their instance methods, class methods, and properties to JS calls.

In addition to the special types of conversions mentioned in the previous section, we are left with the NSDate type, and the conversions to the ID and class types need to be clarified. The NSDate type is unnecessary, so we’ll focus on the latter two conversions in this section.

In general, if we want to use OC classes and objects in a JS environment, we need them to implement the JSExport protocol to determine the attributes and methods exposed to the JS environment. For example, we need to expose a Person class and the method that gets the name to the JS environment:

@protocol PersonProtocol <JSExport> - (NSString *)fullName; //fullName is used to concatenate firstName and lastName, and return the fullName @end@interface JSExportPerson: NSObject <PersonProtocol> - (NSString *)sayFullName; //sayFullName @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; @endCopy the code

We can then pass an instance of JSExportPerson into JSContext and execute the fullName method directly:

    JSExportPerson *person = [[JSExportPerson alloc] init];
    context[@"person"] = person;
    person.firstName = @"Di";
    person.lastName =@"Tang";
    [context evaluateScript:@"log(person.fullName())"]; // Call the Native method to print the full name of the Person instance [Context evaluateScript:@"person.sayFullName())"]; // Prompt TypeError,'person.sayFullName' is undefined
Copy the code

This is a very simple example of using JSExport, but note that we can only call methods that are open in the object’s JSExport. If the “sayFullName” method is not open in the object’s JSExport, it will return TypeError. This method is not defined in the JS environment.

With JSExport out of the way, let’s look at our initial problem. When an OC object is passed into the JS environment, it becomes a JSWrapperObject. So what is a JSWrapperObject? Some clues can be found in the JSCore source code. First of all, we can find such a method in JSValue of JSCore:

@method @abstract Create a JSValue by converting an Objective-C object. @discussion The resulting JSValue retains the provided Objective-C object. @param value The Objective-C object to be converted. @result The new JSValue. */ + (JSValue  *)valueWithObject:(id)valueinContext:(JSContext *)context;
Copy the code

The API can pass in an OC object of any type and return a JSValue holding that OC object. This process must involve the interchange of OC objects to JS objects, so we just need to analyze the source of this method (based on the branch analysis). Because the source code implementation is too long, we just need to focus on the core code. In JSContext, we have a “wrapperForObjCObject” method, which in fact calls the “JSWrapperMap” method, Here’s the answer to all the questions:

// take an object and return a JSValue - (JSValue *)jsWrapperForObject:(id)object { There is a special jsWrapper JSC::JSObject* jsWrapper = m_cachedJSWrappers. Get (object);if (jsWrapper)
        return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context]; JSValue *wrapper; // If the object is a class object, the constructor of classInfo is returned as the actual Valueif (class_isMetaClass(object_getClass(object)))
        wrapper = [[self classInfoForClass:(Class)object] constructor];
    else{// For normal instance objects, the corresponding classInfo is responsible for generating the corresponding JSWrappper and retaining the corresponding OC object, Prototype JSObjCClassInfo* classInfo = [self classForClass :[Object class]]; wrapper = [classInfo wrapperForObject:object]; } JSC::ExecState*exec= toJS([m_context JSGlobalContextRef]); // Write the wrapper value to the JS environment jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec); M_cachedJSWrappers. Set (object, jsWrapper);return wrapper;
}
Copy the code

As we create the “JSWrapperObject” object, we use JSWrapperMap to create the corresponding JSObjCClassInfo for each incoming object. This is a very important class, which has the Prototype and Constructor of the JS object corresponding to this class. JSObjCClassInfo then generates a JSWrapper object for the OC object. This JSWrapper object has all the information needed for a JS object (i.e. Prototype and Constructor) and a pointer to the OC object. After that, write the jsWrapper object to the JS environment and use it in the JS environment. This is what “JSWrapperObject” looks like. As mentioned above, the constructor object is generated in the JS environment if the class is passed in. This is also easy to see from the source code, which returns the constructor property directly when a class is detected (the class itself is an object). This is what the “constructor object” really is, which is essentially a constructor.

OC objects have their own inheritance relationship. How do you describe this inheritance relationship in JS environment? The second question is, how do the methods and properties of JSExport get called in the JS environment?

So let’s start with the first question, how do we solve inheritance? In JS, inheritance is implemented through a chain of prototypes, so what is a prototype? A stereotype object is a normal object and is an instance of a constructor. All objects generated by this constructor share the same object. When an object’s property value is searched and the result is not found, the object’s prototype object is searched to see if the property exists, thus achieving the purpose of encapsulation. Let’s take a quick look at the Person prototype object:

// The prototype object is an ordinary object and an instance of the Person constructor. All instances of the Person constructor share this prototype object. Person.prototype = { name:'tony stark',
   age: 48,
   job: 'Iron Man',
   sayName: function() { alert(this.name); }}Copy the code

The prototype chain is the key to achieve inheritance in JS. Its essence is to rewrite the prototype object of the constructor and link the prototype object of another constructor. Finding the properties of an object in this way will go all the way down the prototype chain, achieving inheritance. Let’s take a quick look at an example:

	function mammal (){}
 	mammal.prototype.commonness = function(){
   		alert('All mammals breathe with their lungs.');
 	}; 

	function Person() {} Person.prototype = new mammal(); // When the prototype chain is generated, instances of Person can also access the Commonness property. Prototype. name ='tony stark';
	Person.prototype.age  = 48;
	Person.prototype.job  = 'Iron Man';
	Person.prototype.sayName = function() { alert(this.name); } var person1 = new Person(); person1.commonness(); / / the pop-up'All mammals breathe with their lungs.'
	person1.sayName(); // 'tony stark'
Copy the code

And when we were in the generated object classinfo (see the specific code “allocateConstructorAndPrototypeWithSuperClassInfo”), also generates classinfo of the parent. JSContext provides a prototype for each OC class that implements JSExport. The NSObject class, for example, will have the corresponding Object Prototype in JS. For other OC classes, the corresponding Prototype is created, and its internal property [Prototype] points to the Prototype created for the parent of the OC class. In the example above, person. prototype is assigned to an instance object. This is the link process for the prototype.

After the first question, let’s look at the second question. How does JSExport expose OC methods to the JS environment? The answer to this question also appears when we generate the object’s classInfo:

        Protocol *exportProtocol = getJSExportProtocol();
        forEachProtocolImplementingProtocol(m_class, exportProtocol, ^(Protocol *protocol){
            copyPrototypeProperties(m_context, m_class, protocol, prototype);
            copyMethodsToObject(m_context, m_class, protocol, NO, constructor);
        });
Copy the code

For each property and method declared in JSExport, classInfo stores the corresponding property and method in Prototype and Constructor. We can then retrieve the actual SEL from the setter and getter methods generated by the concrete methodName and PropertyName. Finally, you can get the methods and properties in JSExport properly accessed. So JSExport is simply responsible for labeling these methods, storing them in a map (prototype and constructor are essentially a map) with a methodName key and SEL value. Then you can get the SEL with methodName and call it. This explains why in the example above, calling a method that is not open in JSExport shows undefined because the generated object does not have this key.

conclusion

JSCore provides iOS apps with a runtime environment and resources where JS can explain execution. For our actual development, the main two classes are JSContext and JSValue. JSContext provides an interface to call each other, and JSValue provides a bridge conversion of data types to call each other. Make it possible for JS to execute Native methods and for Native to call back JS and vice versa.

With JSCore, we can do a lot of imaginative things. All Hybrid development based on JSCore is basically based on the principle of the above figure to achieve mutual invocation, the only difference is that the specific implementation methods and uses are different. All other solutions are just workarounds that can be quickly mastered if the basic process is understood correctly.

Some extended reading

JSPatch objects and methods do not implement the JSExport protocol. How do JS call OC methods?

JS does not call OC through JSExport. There are many problems in the implementation of JSExport. We need to write the Native class first and implement the JSExport protocol, which itself cannot meet the needs of “Patch”.

So JSPatch takes a different approach and uses OC’s Runtime message forwarding mechanism to do this, as shown in this simple JSPatch call code:

require('UIView') 
var view = UIView.alloc().init() 
Copy the code
  1. Require generates UIView variables in the global scope to indicate that this object is an OCClass.

  2. Alloc () is changed to._c(‘alloc’) using the re to close the method, and _methodFunc() is called to pass the class name, object, and MethodName to the OC through Native methods defined in the Context.

  3. The CallSelector method of the OC is called. The underlying invocation is based on the class name, method name, and object from the JS environment.

JSPatch communication is not through JSExport protocol, but with the help of JSCore Context and JSCore type conversion and OC message forwarding mechanism to complete dynamic call, the implementation idea is really clever.

How do implementations of the bridge method interact with JSCore?

There are two common bridge method calls in the market:

  1. Through the UIWebView delegate methods: shouldStartLoadWithRequest handles requests to bridge JS. JSRequest takes methodName, which is called through the WebViewBridge class. After execution, the WebView will be used to execute the JS callback method, of course, is actually called in the WebView JSContext to execute JS, complete the call callback process.

  2. Via UIWebView’s Delegate method: In webViewDidFinishLoadwebViewDidFinishLoad UIWebView JSContext, by means of KVC ready and then through the JSContext setting bridge method calls for JS environment.

The resources

  1. JavaScript Advanced Programming
  2. Webkit Architecture
  3. Virtual Machine Talk 1: The interpreter…
  4. Daming: In-depth analysis of WebKit
  5. JSCore-Wiki
  6. JSCore in iOS

Author’s brief introduction

Tang Di, Senior engineer of Meituan Dianping. He joined Yuan Meituan in 2017. Currently, he is the main developer of takeout iOS team, mainly responsible for mobile terminal infrastructure construction, dynamic and other related promotion work, and committed to improving the efficiency and quality of mobile terminal RESEARCH and development.

recruitment

Meituan takeout is looking for senior/senior engineers and technical experts in Android, iOS and FE based in Beijing, Shanghai and chengdu. We welcome interested students to send their resumes to chenhang03#meituan.com.