preface

The previous company was mainly engaged in mobile terminal H5 development, but related technology and supporting system have been very mature, it is difficult to get access to the system behind.

I have also made some scattered H5 pages in my current company and have some relevant practices. Instead, they are exposed to more things because of poor infrastructure and systems.

I just started React Native recently, so I made up for some related knowledge and also reviewed some things in the development of H5.

Native App

Before talking about Hybrid apps, we have to talk about Native apps, which is the most traditional mobile terminal development technology.

In iOS and Android, the official development languages are OC/Swift and Java/Kotlin. Apps developed using these languages are generally called native apps.

advantages

Native applications generally offer good experience and high performance. You can download resources to the local PC in advance and open them quickly.

In addition, native applications can directly call the system camera, address book, album and other functions, but also access to local resources, powerful.

Generally, you need to develop apps, and native apps should be the first choice.

disadvantages

The biggest disadvantage of native apps is that they don’t support dynamic updates, which is why many big companies don’t fully use native development.

Think about it. What if there is a serious problem online?

Once bug fixes have been made on the client side, it takes several days to relaunch and submit to the app store for approval.

If a new version of the App is released, how should users update it? The answer is no. They may have to download the entire App again, but in reality they may only update one line of text, which is more than worth the loss.

In addition, the most troublesome part is to be compatible with older versions of the App. Let’s say we have a list page that was originally paginated, and the interface returns paginated data. The product says this is a bad experience, we need to switch to full load, so the interface needs to be made full load.

But once the interface is switched to full, the old client still has a paging request interface, and this can cause problems. Therefore, interfaces have to be compatible across versions.

Web App

A Web App is an App that runs in a browser with the help of front-end HTML5 technology. Simply speaking, it is a Web site.

Because it is run in the browser, so natural support cross-platform, a set of code is even easy to support mobile and PC side does not need to be installed in the phone, online version is also relatively easy.

The downside is obvious: you can only use the features provided by the browser, not some of the features on the phone. For example, cameras, contacts, albums and so on, very limited.

Depending on the network, the page loading speed is limited and the experience is poor. Limited by the performance of the browser DOM, some scenarios, such as long lists, are difficult to experience natively. At the same time, because there is no fixed entry on the phone like the client, it will lead to low user stickiness.

Hybrid App

Hybrid apps are some development modes between Native and Web, generally called Hybrid development.

To put it simply, a Hybrid is a shell App. The whole App is still native and needs to be downloaded and installed to the mobile phone. However, the pages opened in the App can be both Web and native.

The H5 page will run in a Native container called WebView. We can simply understand it as opening a Chrome browser in the App and opening a Tab in the browser to load the ONLINE or local H5 page. It is also possible to open multiple WebViews to load multiple pages.

advantage

Hybrid App has the advantages of both Native and Web, and its development mode is relatively flexible. Both can do dynamic update, there are bugs directly update online H5 page on the line. You can also use THE JS Bridge to call the system’s camera, photo albums and other functions, which are not limited to the browser.

Due to the advantages of H5, Hybrid also supports cross-platform. As long as there is a WebView, a set of code can easily cross multiple platforms including iOS, Android, Web, small program and fast application.

disadvantages

The main disadvantages are those of Web apps, which are slow to load.

At the same time, due to the performance of the Web, it is still impossible to achieve the same experience in long lists and other scenarios.

Of course, loading speed can be optimized, such as offline packages. You can download a packed ZIP file (including JS, CSS, images and other resource files) into the App in advance, and the App decompresses JS and CSS files by itself. In this way, the local resources of App are accessed each time, and the loading speed can be improved qualitatively.

If the file has been updated, the client will pull the remote version and compare it with the local version. If the version has been updated, the client will pull the difference part of the file and patch it to the original file with binary DIff algorithm. In this way, the hot update can be achieved.

However, the cost is also relatively high, which requires not only a file difference at the server, but also a hot update publishing platform provided by the company.

WebKit

WebView is a control that displays the interface in Android. It is generally used to display the Web interface. As we said earlier, you can think of WebView as the Chrome browser you’re using.

How does the browser parse the rendered HTML and CSS and render it onto the page?

This is part of a classic interview question: What happens from URL input to presentation?

Simply put, the browser takes the HTML text in response and parses the HTML into a DOM tree, parses the CSS into a CSSOM tree, and combines the two to create a render tree. Use the Skia graphics library in Chrome to render the interface. Skia is also the rendering engine of Flutter.

Take a look at this classic:

PS: One thing that distinguishes Flutter from React Native is that it uses Skia to draw the interface instead of compiling it into a Native component for the system to render.

In addition to parsing HTML, the browser also needs to provide a JavaScript runtime, which is what the V8 engine is known to do.

Its kernel

As we can see from the above, a browser needs at least one engine for rendering HTML and one engine for running JavaScript.

Of course, all of this is done by the browser kernel. Major browsers now use the WebKit kernel.

WebKit was born out of Apple’s Safari browser. Google later created the Chromium Project based on WebKit and released the familiar Chrome browser on top of it.

The WebKit kernel structure is shown below.

From top to bottom, the WebKit embedded interface is provided for browser invocation, which may vary from browser implementation to browser implementation.

The HTML and CSS parsing part is done by WebCore, which is the core rendering engine of WebKit and the consistent part of all major browsers. It generally includes HTML and CSS interpreter, DOM, rendering tree and other functions.

WebKit uses JavaScriptCore as the JS engine by default, which can be replaced. JavaScriptCore is also the default engine in React Native.

Due to the low performance of JavaScriptCore, Google chose V8 as the JS engine in Chrome.

WebKit Ports is the unshared part, which becomes portable due to platform differences and depending on libraries. It mainly involves network, video decoding, picture decoding, audio decoding and other functions.

WebView also uses the WebKit kernel. It uses V8 as JS engine in Android and JavaScriptCore as JS engine in iOS.

Since there are two engines that render the DOM and manipulate JS, when we manipulate the DOM with JS, the JS engine gets access to the DOM by calling the bridge method. This is where the performance penalty of communication between the two engines comes in, which is why frequent DOM manipulation can lead to poor performance.

React and Vue frameworks are optimized at this level, modifying only the different PARTS of the DOM rather than replacing the DOM entirely.

In the iOS JavaScriptCore

JavaScriptCore is the default JAVASCRIPT engine used by the WebKit kernel. Since WebView is explained, let’s introduce the JavaScriptCore framework in iOS.

The JavaScriptCore framework in iOS is a JavaScriptCore framework encapsulated in OC.

It provides the ability to call THE JS operating environment and OC and JS to call each other, mainly including JSVM, JSContext, JSValue, JSExport four parts (in fact, just want to talk about JSVM).

JSVM

JSVM full name is JSVirtualMachine, in simple terms is JS virtual machine. So what is a virtual machine?

Taking the JVM as an example, there are generally several steps required to run a Java program:

  1. Java source files (.java files) are compiled into bytecode files (.class files, which are binary bytecode files), which are the JVM’s “machine language.” Javac.exe can be considered a Java compiler.
  2. Java interpreters are used to interpret programs that execute Java compiler compilations. Java.exe can simply be thought of as a Java interpreter.

So the JVM is a virtual machine that can run Java bytecode. In addition to running Java bytecode, it does things like memory management, GC, and so on.

JSVM provides the RUNTIME environment of JS, and also provides memory management. Each JSVM has only one thread, and if you want to execute multiple threads, you create multiple JSVMS that have their own separate GC, so objects cannot be passed between multiple JSVMS.

JS source code through lexical analysis and syntax analysis of these two steps, into bytecode, this step is compiled.

However, unlike Java 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 JSVM.

The bytecode

Bytecode is intermediate code that has been compiled, independent of a particular machine code, and needs to be translated by the interpreter to become machine code.

In V8, bytecodes were not introduced in the early stages, but the source code was simply compiled directly into machine code to run, because they felt that bytecodes and bytecodes would slow down execution.

But later V8 introduced bytecode again. Why?

Early V8 compiled JS to binary machine code, but compilation took up a significant amount of time. If it is the same page, it will have to be recompiled every time it is opened, which will greatly reduce efficiency.

Chrome has introduced binary caches, which store binary code in memory or on hard disk so it can be used the next time you open the browser.

However, binary code has a particularly high memory footprint, about thousands of times that of JS code. As a result, if used on mobile devices (phones), the already small memory capacity will be further occupied, resulting in performance degradation.

Bytecode, however, takes up far less space than machine code. As a result, the V8 team had to introduce bytecode again.

JIT

In addition, there is another concept that everyone is familiar with: JIT (just-in-time compilation).

Just-in-time compilation: Also called real-time compilation, just-in-time compilation. A technique that compiles bytecode to native machine code at run time, translating source code sentence by sentence, but caching the translated code to reduce performance degradation. This technique is used to improve the performance of virtual machines.

Simply put, a piece of code is compiled before it is executed. Again, take the JVM.

  1. JVM interpretation process:

Java code -> compile bytecode -> interpreter interprets execution

  1. JIT compilation process:

Java code -> compile bytecode -> compile machine code -> execute

So Java is a half-compiled, half-interpreted language. JIT is fast in the sense that it executes machine code more efficiently than it interprets bytecode, not in the sense that compilation is faster than interpretation. Therefore, JIT compilation is still slower than interpretation.

Again, compiling to machine code will encounter the above space problem. Therefore, only code that is frequently executed is compiled in the JIT, which generally includes the following two types:

  1. A method that is called multiple times.
  2. The body of a loop that is executed multiple times.

This part is cached when the hot code is compiled. The next time you run it, you don’t need to compile it again, and it’s much more efficient. This is why many JVMS use the interpreter +JIT.

Little Dingo said:

JSContext

JSContext is the context in which JS is run. If we want to run JS code on a WebView, we need JSContext. Take this code as an example:

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var i = 4"];
NSNumber *number = [context[@"i"] toNumber];
Copy the code

The JSContext above calls evaluateScript to execute a piece of JS code that takes the corresponding JSValue object from the context.

JSValue

JSCore helped us to do type conversion when JS and OC exchanged data. JSCore provided 10 types of type conversion:

   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

JSExport

JSExport supports exposing Native objects to JS environments. Such as:

@protocol NativeObjectExport <JSExport>

@property (nonatomic, assign) BOOL property1;

- (void)method1:(JSValue *)arguments;

@end

@interface NativeObject : NSObject<NativeObjectExport>

@property (nonatomic, assign) BOOL property1;

- (void)method1:(JSValue *)arguments;


@end
Copy the code

As long as the above NativeObject implements JSExport, it can be directly called by JS. We need to inject an object into the Context to call Native in the JS environment.

context[@"nativeMethods"] = [NativeObject new];
Copy the code

Call from JS:

nativeMethods.method1({
    callback(data) {
    }
])
Copy the code

React Native

H5 in Hybrid is always running in WebView, WebKit is responsible for drawing. Facebook released React Native based on React because of performance bottlenecks in browser rendering.

Due to the platform-independent advantage of the Virtual DOM in React, Virtual DOM can theoretically be mapped to different platforms. This is DOM in the browser, and some of the Native components in Native.

Constrained by the performance of browser rendering, React Native learned from the experience and handed over rendering to Native, greatly improving the experience. Personally, React Native is a Hybrid technology.

In RN, JavaScriptCore is directly used to provide the JS running environment, and the Bridge is used to inform Native to draw the interface, which is ultimately rendered by Native.

Therefore, the performance is better than Hybrid, but it is still inferior to Native due to the performance consumption of JS and Native communication.

JS and Native communication principles

When JS communicates with Native, it usually goes through Bridge, which is asynchronous.

At App startup, Native creates a Module configuration table, which contains information about all modules and Module methods.

{
    "remoteModuleConfig": {
        "XXXManager": {
            "methods": {
                "xxx": {
                    "type": "remote"."methodID": 0}},"moduleID": 4},... }},Copy the code

Since every module class provided to JS calls in OC implements the RCTBridgeModule interface, get all the classes in the project by objc_getClassList or objc_copyClassList. Then determine whether each class implements RCTBridgeModule to determine whether it needs to be added to the configuration table.

Then Native will inject this configuration table information into JS, so that JS can get the module information in OC.

In fact, if you have written JS Bridge, you will find that this process is somewhat similar to WebViewJavaScriptBridge. The steps are as follows:

  1. .js calls a method of an OC module
  2. Convert this call to ModuleName, MethodName, and arguments and hand it over to MessageQueue.
  3. Put the JS Callback function into the MessageQueue and match with the CallbackID. ModuleName and MethodName are mapped to ModuleID and MethodID according to the configuration table.
  4. Then pass all the above ids to OC
  5. OC gets corresponding modules and methods from the configuration table according to these ids
  6. RCTModuleMethod processes parameters sent from JS, mainly converting the JS data type to the OC data type
  7. Call the OC module method
  8. The corresponding Callback method is executed and passed by CallbackID

Hot update

One advantage RN has over Native is thermal updating. We end up packing RN projects into a Bundle for the client to load. The Bundle is loaded at App startup time and executed by JavaScriptCore.

If there was a new version, how would it have been updated? This is actually quite simple. Repackage a Bundle and use THE BS Diff algorithm to make binary differences between different versions of the file.

The client will compare the local version with the remote version. If the local version is behind, it will download the differential file and also use THE BS Diff algorithm patch into the Bundle to achieve hot updates.

This method also applies to offline package update of H5, which can largely solve the problem of slow loading of H5.

RN the new architecture

In the old RN architecture, the performance bottleneck is mainly reflected in JS and Native communication, namely Bridge.

Our RN code will be serialized by the JS Thread, and then deserialized by the Bridge to the shadow Thread to get the native layout information.

After that, the Bridge is passed to the UI Thread, which is deserialized and drawn based on the layout information. There are three threads communicating through the Bridge.

There is a performance penalty due to multiple serialization/deserialization and Bridge communication.

In particular, it is easy to cause a blank screen when you swipe through a list quickly. However, there is no blank screen when you swipe through a list quickly in the browser. Why is this?

Mainly in the browser, JS can hold references to C++ objects, so this is actually a synchronous call.

Due to the impact of Flutter, the RN community proposed a new architecture to solve these problems. To address Bridge communication issues, the RN team abstracted a layer of JSI (JavaScript Interface) on top of JavaScriptCore, allowing the underlying layer to change to a different JavaScript engine.

In addition, JS can also get C++ references, so that it can communicate directly with Native, without repeatedly serializing objects, and saves the cost of Bridge communication.

Here’s why you can communicate with Native if you get a C++ reference. Since OC itself is an extension of the C language, C/C++ methods can be called directly. Java cannot be extended by C, but it can be called through JNI.

JNI is the Java Native Interface. It is a set of programming frameworks provided by the JVM that enable Java code running on the JVM to call C++ programs and be called by C++ programs.

It is believed that the arrival of the new architecture will solve some of RN’s existing pain points and bring a leap in performance.

Flutter

There are two traditional cross-ends. One is Hybrid, which implements JS running on top of a WebView. This performance bottleneck depends on browser rendering.

The other is to map JS components to Native components, such as React Native and Weex. The disadvantage is that JS Bridge is still required for communication (old architecture).

Instead of making Native mappings, Flutter uses the Skia rendering engine, Chrome’s underlying 2D graphics library, implemented in C/C++. Call the CPU or GPU to complete the drawing. So Flutter is like a game engine.

Flutter is syntactically influenced by React, uses setState to update the interface, and uses ideas like Redux to manage state. From the early days of WPF, to React, to SwiftUI, the idea of declarative UI was used.

The diagram of the Flutter architecture is as follows:

The Framework is an SDK implemented with Dart that encapsulates basic libraries such as animation and gestures. We’ve also implemented a library of UI components in the Material and Cupertino styles. Material for Android, Cupertino for iOS.

Engine is the C/C++ implementation SDK, including the Skia Engine, Dart runtime, text rendering, etc.

Embedder is an Embedder layer that allows you to embed a Flutter into various platforms.

Flutter uses Dart and supports AOT compilation to ARM Code for higher performance. JIT is also supported in Debug mode.

In A Flutter, Widgets are the basic building blocks of the interface, similar to the React Component. The StatelessWidget is similar to the React Functional Component.

class Echo extends StatelessWidget { const Echo({ Key key, @required this.text, this.backgroundColor:Colors.grey, }):super(key:key); final String text; final Color backgroundColor; @override Widget build(BuildContext context) { return Center( child: Container( color: backgroundColor, child: Text(text), ), ); }}Copy the code

There are three trees in the Flutter rendering process, namely, Widgets tree, Element tree and RenderObject tree.

If you’ve ever written React, it’s really like React.

When initialized, Widgets use the build method to generate the Element, similar to how React. CreateElement generates the Virtual DOM.

Element re-creation is expensive, so every time it is re-rendered it will not be rebuilt. There is also a Diff environment between the Element Tree and the RenderObject Tree, calculating the minimum area to be redrawn.

This is also similar to the React rendering process. The virtual DOM is Diff compared to the real DOM, and the difference is rendered to the browser.

Browser rendering

Earlier we talked about the browser rendering process. Typically, HTML is parsed into a DOM tree, CSS is parsed into a CSSOM tree, and both are combined into a RenderObject tree.

A RenderObject holds all the information needed to draw a DOM node, and it knows how to draw itself. The different RenderObject objects form a tree, called a RenderObject tree. It is a tree created based on a DOM tree.

WebKit then creates new RenderLayer objects for those RenderObject objects, and finally a RenderLayer tree. In general, RenderLayer and RenderObject have a one-to-many relationship. Each RenderLayer contains subtrees of the RenderObect tree.

When is a RenderLayer object created? Such as the Video node, the Document node, the transparent RenderObject node, the specified location node, and so on, all create a RenderLayer object.

A RenderLayer can be seen as a layer in Photoshop, and each layer forms an image.

Flutter rendering

Flutter rendering is similar to browser rendering. The Widget generates The Element with createElement, while Element creates the RenderObject with createRenderObject, which ultimately generates the Layer.

In general, RenderObject stores layout information, so layout and drawing is done in RenderObject. Flutter works by deeply traversing the RenderObject tree, determining the position and size of each object, and drawing it onto different layers. After drawing, Skia completes the composition and rendering.

So the update process of Flutter is as follows:

communication

Flutter cannot fulfill all of Native’s functions, such as calling the camera, so we need to develop plug-ins, which are based on the communication between Flutter and Native.

The communication between Flutter and Native is completed by Channel. Generally, there are the following communication scenarios:

  1. Native sends data to Dart
  2. Dart sends data to Native
  3. Dart sends data to Native, and Native sends data back to Dart

Flutter can communicate in three ways:

  1. EventChannel: A one-way communication method by which a Native sends data to a Flutter. The Flutter cannot return any data to the Native. Generally used by Native to send cell phone power, Internet connection, etc to Flutter.
  2. MethodChannel: Two-way communication between Native and Flutter. Calls Native and corresponding methods in a Flutter via MethodChannel that return values.
  3. BasicMessageChannel: Bidirectional communication between a Native and a Flutter. Support the most data types, the widest range of use. This method has a return value.

BinaryMessenger is a tool for Flutter and Channel communication. It’s an interface in Android that uses binary format data to communicate.

In FlutterView, it can communicate with the bottom layer of the system through JNI. Therefore, it is basically the same as a native call, unlike a Bridge call in RN that requires data transformation.

Therefore, if you want to develop plug-ins, you still need to realize the functions of Android and iOS, as well as the API that encapsulates plugin. In general, you still cannot operate without Native.

Contrast the React Native

  1. Flutter officially does not support hot updates. RN has a mature Code Push solution
  2. Flutter abandons the Web ecosystem. RN has a web-mature ecosystem and a more powerful tool chain.
  3. Flutter compiles the Dart code AOT to native code and communicates near native. Not only does RN require multiple serialization, but different threads also need to communicate through Bridges, which is inefficient.

For more details, please refer to zhihu: Do you recommend React Native or Flutter for cross-platform App development?

PS: Welcome to pay attention to my public account [front-end small pavilion], we come together to discuss technology.

reference

  1. Understanding WebViews
  2. What is WebKit and how is it related to CSS?
  3. Gpu-accelerated Composition in Chrome (Part 1)
  4. JavaScriptCore
  5. Why JIT improves performance so much?
  6. Simple JIT compiler
  7. How Flutter renders Widgets
  8. Flutter theory and experience sharing
  9. Inside WebKit technology
  10. React Native’s New Architecture