background

What is dynamic

In recent years, more and more dynamic schemes based on the front-end technology stack have been introduced into client development. Have you ever thought about the difference between the client technology stack and the front-end technology stack when developing and writing code?

To put it simply, no matter Android or iOS apps are released, they must go through the process of source code compilation, packaging and compilation, release of the app store, and user upgrade and installation. First of all, compilation speed will increase in proportion to the size of the application. For large-scale applications, sometimes we just change the color of a control, but have to wait a few minutes to verify the result, the development efficiency is very low. Secondly, the release of the app store, users upgrade installation, will greatly prolong the application release cycle, delay the verification of the product effect; Finally, the development of the same function requires at least one engineer on both sides to write two copies of code, and the labor cost is ++.

Let’s take a look at the front-end development process: because it is using JavaScript scripting language development, there is no need to compile in advance, in the browser can directly preview the effect; After the release of the new version, can also directly contact the browser, users do not need additional operations; Most importantly, the same functionality can be developed once and run in browsers on almost any operating system.

After understanding the two development modes, we naturally think, why can’t we introduce the development process of the front-end technology stack into the client side, so that the client side application can also have the front-end dynamic and cross-end?

In fact, there are already a variety of solutions in the industry. You should have heard or come into contact with React Native, Weex, wechat mini program, etc. Even in some specific scenarios, client functions have been completely developed using front-end technology, such as operational activities that need to be delivered dynamically, product functions that need to be quickly trial-and-error, and in-application ecological construction of small programs.

Dynamic engine is the most core module in the dynamic scheme. Only with dynamic engine can the JS application written by the developer run in the client and realize the dynamic UI and logic.

Hello Hybrid World

In fact, dynamic is not difficult to imagine, as long as you understand the principle, every student can build a dynamic engine from 0 to 1. Furthermore, we can even use our own dynamic engine to write a JS application for it as follows:

From an application developer’s point of view, to implement such an interface, you need to create vertical layouts, text components, image components, and button components in YOUR JS code that can set click events.

These are some of the capabilities that we need to support in a dynamic engine, but there are many more underlying capabilities that are not intuitive from a developer’s perspective, as discussed in the next section.

Build your own dynamic engine Step by Step

The author is an Android engineer, so I will use Android and Java technology stack to achieve, iOS or other end principle is actually similar.

Step 1. Target disassembly

Question: What modules are needed to realize a dynamic engine based on the front-end technology stack?

The following figure shows the modules and components required for a basic dynamic engine:

From top to bottom:

The module role
Business Code JS application interface and logic code
JS Framework A layer of JS runtime encapsulation beneath the business code provides basic capabilities such as life cycle callbacks, application entry functions, VDom, Diff algorithms, and so on, communicating directly with the Native side
JS Engine JS virtual machine, which runs core modules of JS code, such as V8 and JavaScriptCore
JS Bridge Two-way communication channel between JS and Native
ModuleManager It is usually a collection of all Native Bridges and provides registration, acquisition and other methods of Bridges
RenderManager Manage the application rendering process, such as parsing VDom data sent by JS Framework, rendering instructions, building Dom tree and View tree of Native side, etc
Debugging Debugging capability Interworking with the Chrome DevTools Protocol (CDP), you can perform debugging operations on Chrome DevTools
Native Modules The bridge implemented by Native side is basically a secondary encapsulation of Native API for JS side to call
Native Components Controls implemented by Native side are basically secondary encapsulation of Native View for JS side to call

Once we are familiar with the important modules of the dynamic engine, we can start implementing it step by step.

Step 2. JS engine

JS engine is a virtual machine that processes JavaScript scripts. It is the premise and foundation of dynamic development. Only with it can developers run JS code in client applications.

Common JS engines include V8 and JavaScriptCore.

The V8 engine is implemented in C++, and since we are developing in Android, we need to use J2V8. J2V8 is the Java encapsulation of the V8 engine, providing a variety of easy-to-use interfaces.

J2V8:github.com/eclipsesour…

Rely on

dependencies {
    implementation 'com. Eclipsesource. J2v8: j2v8:6.2.1 @ aar'
}
Copy the code

Creating a V8 Engine

V8 runtime = V8.createV8Runtime();
Copy the code

Native executes the JS script

Execute a section of JS logic:

V8 runtime = V8.createV8Runtime();
int result = runtime.executeIntegerScript("var i = 0; i++; i");
System.out.println("result: " + result);
// result: 1
Copy the code

Native executes JS methods

Define a JS method and execute:

V8 runtime = V8.createV8Runtime();
runtime.executeVoidScript("function add(a, b) { return a + b }");
V8Array args = new V8Array(runtime).push(1).push(2);
int result = runtime.executeIntegerFunction("add", args);
System.out.println("result: " + result);
// result: 3
Copy the code

Packaged JS engine

We can abstract the engine part into two modules — JsBundle and JsContext.

JsBundle

JsBundle is a package file for a JS application. It contains all the source code and resources of the application, such as local image resources and application information list. However, we are just implementing a simple dynamic framework that includes only JS source files for the time being, or we can use a.js file as a bundle.

public class JsBundle {

    private String mAppJavaScript;

    public String getAppJavaScript(a) {
        return mAppJavaScript;
    }

    public void setAppJavaScript(String appJavaScript) {
        this.mAppJavaScript = appJavaScript; }}Copy the code

MAppJavaScript is the APPLICATION’s JS code.

JsContext

JsContext is a secondary encapsulation of the V8 engine that describes how a JS engine initializes and executes application JS code:

public class JsContext {

    private V8 mEngine;

    public JsContext(a) {
        init();
    }

    private void init(a) {
        mEngine = V8.createV8Runtime();
    }

    public V8 getEngine(a) {
        return mEngine;
    }

    public void runApplication(JsBundle jsBundle) { mEngine.executeStringScript(jsBundle.getAppJavaScript()); }}Copy the code

Theoretically, when we run the following code, a JS engine starts up and can execute any non-native JS code:

JsBundle jsBundle = new JsBundle();
jsBundle.setAppJavaScript("var a = 1");

JsContext jsContext = new JsContext();
jsContext.runApplication(jsBundle);
Copy the code

Tips: If you want to use Native’s capabilities, you also need to inject the so-called bridge before the engine initialization, which is used to complete THE JS-to-Native communication

Step 3. Two-way communication — JS Bridge

In the previous section we saw how to create a V8 engine and execute JS scripts. However, in order to achieve the ability of JS to call the native system, the native system notifies JS of the occurrence of events, we need a communication mechanism, that is, the bridge we often say — JS Brdige.

As a two-way communication mechanism, JS Bridge ensures that JS code can use the native system capabilities (such as taking photos, accessing the network, obtaining device information, etc.). At the same time, when the native system has messages or events, it can also notify JS side (such as gyroscope monitoring, push message touch, user click events, etc.).

JS executes Native methods

The V8 engine provides the ability to inject Native methods into JS, such as the console.info function, which is most common in the front end, as follows:

V8 runtime = V8.createV8Runtime();
V8Object console = new V8Object(runtime);
console.registerJavaMethod((v8Object, params) -> {
  String msg = params.getString(0);
  Log.i(TAG, msg);
  return null;
}, "info");
runtime.add("console", console);
Copy the code
console.info("print some messages!")
Copy the code

Then in adb logcat we’ll see a log like this typed out.

Native executes JS functions

ExecuteScript directly, calling a defined JS function:

function sayHello() {
    return "Hello Hybrid World!"
}
Copy the code
V8 runtime = V8.createV8Runtime();
String result = runtime.executeStringScript("sayHello()");
// Hello Hybrid World!
Copy the code

JS can pass a V8Function to Native, such as implementing a callback that listens for latitude and longitude changes:

V8 runtime = V8.createV8Runtime();
V8Object device = new V8Object(runtime);
console.registerJavaMethod((v8Object, params) -> {
  V8Function listener = (V8Function) params.getObject(0);
  V8Array locations = new V8Array(runtime).push(116.1234567).push(46.1234567);
  listener.call(v8Object, locations);
  return null;
}, "onLocationChanged");
runtime.add("$device", device);
Copy the code
$device.onLocationChanged(listener: function (x, y) {
  console.info(x);
})
/ / 116.1234567
Copy the code

Build JS Bridge,

Now that we have learned how to use the V8 engine’s capabilities to implement JS-native two-way communication, we have abstracted these behaviors and information to make it easier to manage and register the bridge.

You can use JsModule to represent the capabilities of a Native bridge:

public abstract class JsModule {

    public abstract String getName(a);

    public abstract List<String> getFunctionNames(a);

    public abstract Object execute(String functionName, V8Array params);
}

// The abstraction of the console.info method
public class ConsoleModule extends JsModule {

    @Override
    public String getName(a) {
        return "console";
    }

    @Override
    public List<String> getFunctionNames(a) {
        List<String> functions = new ArrayList<>();
        functions.add("info");
        return functions;
    }

    @Override
    public Object execute(String functionName, V8Array params) {
        switch (functionName) {
            case "info":
                Log.i("Javascript Console", params.getString(0));
                break;
        }
        return null; }}Copy the code

Use the ModuleManager to manage and register all JsModule:

public class ModuleManager {

    private ModuleManager(a) {}private static class Holder {
        private static final ModuleManager INSTANCE = new ModuleManager();
    }

    public static ModuleManager getInstance(a) {
        return Holder.INSTANCE;
    }

    private final List<JsModule> mModuleList = new ArrayList<>();
    private JsContext mJsContext;

    public void init(JsContext jsContext) {
        mJsContext = jsContext;
        mModuleList.add(new UiModule());
        mModuleList.add(new ConsoleModule());
        registerModules();
    }

    private void registerModules(a) {
        for (JsModule module : mModuleList) {
            V8Object moduleObj = new V8Object(mJsContext.getEngine());
            for (String functionName : module.getFunctionNames()) {
                moduleObj.registerJavaMethod((v8Object, params) -> {
                    return module.execute(functionName, params);
                }, functionName);
            }
            mJsContext.getEngine().add(module.getName(), moduleObj); }}}Copy the code

At this point, our dynamic framework already supports JS calling Native capabilities. You can inherit JsModule and write any bridge you need to implement a variety of capabilities.

In contrast to the previous section, JS applications can now include Native methods in their code, no longer limited to the original JS environment.

Step 4. Render engine

At this point, we are ready to implement logic dynamics, theoretically all logical behavior that does not require user interaction can be executed in JS.

But in addition to the backend logic, a more important part of a modern application is the user-facing UI interface, which means the user’s first impression of the application, so in this section we’ll look at how to make the UI dynamic.

UI Framework

Android developers are familiar with XML, where we define static page structures such as:


      
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:gravity="center"
        android:text="Hello Hybrid World!"
        android:textSize="24sp" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="24dp"
        android:src="@drawable/ic_launcher_background" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="32dp"
        android:text="BUTTON" />
</LinearLayout>
Copy the code

Question: Why can XML be converted to UI components on screen?

To put it simply: The Android UI framework reads and parses the XML file, then builds it into a View and ViewGroup, forming a View tree of a page, which is rendered from top to bottom by the system and displayed on the screen. So XML is a DSL of the Android UI framework, and View system is a rendering engine of the Android UI framework.

We learned two things at the heart of the UI framework: 1. DSL for developers; 2. Operating system-oriented rendering engine.

React Native, Weex and other dynamic rendering frameworks actually change the DSL layer, but the way developers write UI has changed, but the interface is still built into View trees for rendering.

However, UI frameworks like Flutter and Jetpack Compose not only change the DSL, but also use a completely different rendering engine (based on SKIA) for UI rendering on Android devices.

The UI dynamic

To make the UI dynamic, the core principle is to have the DSL that builds the page support dynamic delivery, and the rendering engine support dynamic parsing and creation of view components.

The simplest DSL that can represent interface elements and layouts using JSON structures. For example, the interface we want to implement at the beginning of this article could be described as follows:

const hello = "Hello ";
const title = hello + "Hybrid World!"
$view.render({
    rootView: {
        type: "verticalLayout".children: [{"type": "text"."text": title,
                "textSize": 24."marginTop": 16
            },
            {
                "type": "image"."width": 72."height": 72."marginTop": 80."url": ""
            },
            {
                "type": "button"."text": "Click Print Log"."marginTop": 80."marginLeft": 40."marginRight": 40."onClick": function () {
                    console.info("success!"}}]}})Copy the code

We define a $view.render method as the interface rendering entry function, when JS executes to this method, will start rendering; Until then, you can write any interface independent logic.

React and Vue are both front-end UI frameworks with intuitive DSL syntax, powerful VDom mechanisms, and syntactic sugar that make it easy for developers to write UI interfaces. This is one of the goals of UI DSLS. I use JSON as a UI DSL because its data structure is the most common and easy to understand and can be implemented without additional syntax parsers. The real industry UI DSL is much more complex than this 😊

$view.render is a Native bridge, so implement it using the JsModule defined in the previous section:

public class UiModule extends JsModule {
    @Override
    public String getName(a) {
        return "$view";
    }

    @Override
    public List<String> getFunctionNames(a) {
        List<String> functionNames = new ArrayList<>();
        functionNames.add("render");
        return functionNames;
    }

    @Override
    public Object execute(String functionName, V8Array params) {
        switch (functionName) {
            case "render":
                V8Object param1 = params.getObject(0);
                V8Object rootViewObj = param1.getObject("rootView");
                RenderManager.getInstance().render(rootViewObj);
                break;
        }
        return null; }}Copy the code

The $view.render method passes in an object with the rootView field indicating the root layout of the interface; Generally speaking, an interface can have only one root node, and there are many children below the root node, eventually forming a tree structure.

There is a type field under the rootView node, indicating that it is a verticalLayout type, that is, verticalLayout; And the Children field, which indicates which of the children nodes are present.

The first child in the children array is a text component of type TEXT, which also has a number of properties, such as text size, spacing, and so on. Similarly, the remaining child nodes are the Image component and the button button component, which also have their own properties.

DomElement

The object passed by JS will be carried in the form of V8Object, which is not convenient for direct operation. We can abstract the V8Object passed by JS into DomElement, representing the attribute information of a node element, which is convenient for Native View to use these attributes later.

DomElement is a data class that directly corresponds to view node information passed from the JS side.

// View elements can have common attributes
public class DomElement {

    public String type;
    public int marginTop;
    public int marginBottom;
    public int marginLeft;
    public int marginRight;
    public V8Function onClick;

    public void parse(V8Object v8Object) {
        for (String key : v8Object.getKeys()) {
            switch (key) {
                case "type":
                    this.type = v8Object.getString("type");
                    break;
                case "marginTop":
                    this.marginTop = v8Object.getInteger("marginTop");
                    break;
                case "marginBottom":
                    this.marginBottom = v8Object.getInteger("marginBottom");
                    break;
                case "marginLeft":
                    this.marginLeft = v8Object.getInteger("marginLeft");
                    break;
                case "marginRight":
                    this.marginRight = v8Object.getInteger("marginRight");
                    break;
                case "onClick":
                    this.onClick = (V8Function) v8Object.get("onClick");
                    break;
                default:
                    break; }}}}// Each specific view element can also have its own unique attributes
public class DomText extends DomElement {
    public String text;
    public int textSize;
    public String textColor;

    @Override
    public void parse(V8Object v8Object) {
        super.parse(v8Object);
        for (String key : v8Object.getKeys()) {
            switch (key) {
                case "text":
                    this.text = v8Object.getString("text");
                    break;
                case "textSize":
                     int textSize = v8Object.getInteger("textSize");
                    if (textSize == 0) {
                        textSize = 16;
                    }
                    this.textSize = textSize;
                    break;
                case "textColor":
                    String textColor = v8Object.getString("textColor");
                    if (TextUtils.isEmpty(textColor)) {
                        textColor = "# 000000";
                    }
                    this.textColor = textColor;
                    break; }}}}Copy the code

Question: You can try writing the remaining DomElements you need. Such as: DomButton, DomVerticalLayout and so on

We also need a DomFactory, which uses the factory pattern to create different types of DomElements:

public class DomFactory {

    public static DomElement create(V8Object rootV8Obj) {
        String type = rootV8Obj.getString("type");
        switch (type) {
            case "text":
                DomText domText = new DomText();
                domText.parse(rootV8Obj);
                return domText;
            case "image":
                DomImage domImage = new DomImage();
                domImage.parse(rootV8Obj);
                return domImage;
            case "button":
                DomButton domButton = new DomButton();
                domButton.parse(rootV8Obj);
                return domButton;
            case "verticalLayout":
                DomVerticalLayout domVerticalLayout = new DomVerticalLayout();
                domVerticalLayout.parse(rootV8Obj);
                return domVerticalLayout;
        }
        return null; }}Copy the code

Of course, factory mode is only one way to implement this. You can create DomElement objects using annotations to record the type information. The advantage is that the creation of objects is completely automated, and there is no need to manually instantiate them when there are dozens of UI controls.

Then you can easily create a DomElement tree with a JS side root layout:

V8Object rootViewObj = ... ; DomElement rootViewElement = DomFactory.create(rootViewObj);Copy the code

JsView

We can already access node element data freely in Native for use by Native Views that are about to be rendered, because Native Views need to know how they should display, what text to display, what click events to respond to, etc.

However, instantiating Native View, setting properties in DomElement, and building Native View tree directly after $view.render makes UiModule classes too bloated. Therefore, we also need an intermediate layer to abstract the virtual View corresponding to Native View — JsView.

JsView’s purpose is to make the element nodes more cohesive, just focus on creating itself. Like DomElement, JsView builds a tree to represent the interface structure. Each JsView has a createView method that returns its Native View instance:

public abstract class JsView<V extends View.D extends DomElement> {

    protected D mDomElement;
    protected V mNativeView;

    public void setDomElement(DomElement domElement) {
        mDomElement = (D) domElement;
    }

    public abstract String getType(a);

    public abstract V createViewInternal(Context context);

    public V createView(Context context) {
        V view = createViewInternal(context);
        mNativeView = view;
        returnview; }}Copy the code

For example, the text component needs to inherit from JsView:

public class TextJsView extends JsView<TextView.DomText> {

    @Override
    public String getType(a) {
        return "text";
    }

    @Override
    public TextView createViewInternal(Context context) {
        TextView textView = new TextView(context);
        textView.setGravity(Gravity.CENTER);
        textView.setText(mDomElement.text);
        textView.setTextSize(mDomElement.textSize);
        textView.setTextColor(Color.parseColor(mDomElement.textColor));
        returntextView; }}Copy the code

Question: You can try writing the rest of the JsView. Such as ButtonJsView, VerticalLayoutJsView and so on. Again, we still need a JsViewFactory to create different types of JsView instances, like DomElement, which we won’t go into here.

Finally, we can use RenderManager to manage DSL parsing, DomElement tree creation, JsView tree creation, and Native View rendering. RenderManager also needs a Native View container to hold the JS rendered root layout:

public class RenderManager {

    private RenderManager(a) {}private static class Holder {
        private static final RenderManager INSTANCE = new RenderManager();
    }

    public static RenderManager getInstance(a) {
        return Holder.INSTANCE;
    }

    private Context mContext;
    private ViewGroup mContainerView;

    public void init(Context context, ViewGroup containerView) {
        mContext = context;
        mContainerView = containerView;
    }

   public void render(V8Object rootViewObj) {
     	DomElement rootDomElement = DomFactory.create(rootViewObj);
        JsView rootJsView = JsViewFactory.create(rootDomElement);
        if(rootJsView ! =null) { View rootView = rootJsView.createView(mContext); mContainerView.addView(rootView); }}}Copy the code

Step 5. Integrate dynamic engines

So far, we have almost all the modules we need for a dynamic engine, and now we just have to put it together.

It is expected that Native can be used conveniently when creating dynamic engine, so the whole dynamic container can be abstracted into a JsApplication externally:

public class JsApplication {
    private JsContext mJsContext;

    public static JsApplication init(Context context, ViewGroup containerView) {
        JsApplication jsApplication = new JsApplication();
        JsContext jsContext = new JsContext();
        jsApplication.mJsContext = jsContext;
        RenderManager.getInstance().init(context, containerView);
        ModuleManager.getInstance().init(jsContext);
        return jsApplication;
    }

    public void run(JsBundle jsBundle) { mJsContext.runApplication(jsBundle); }}Copy the code

In MainActivity, just initialize JsApplication and execute JsBundle:

FrameLayout containerView = findViewById(R.id.js_container_view);

JsBundle jsBundle = new JsBundle();
jsBundle.setAppJavaScript(JS_CODE);

JsApplication jsApplication = JsApplication.init(this, containerView);
jsApplication.run(jsBundle);
Copy the code

A basic dynamic engine has been completed, which is surprisingly easy to implement. Once we understand the core principles and necessary modules of the dynamic engine, the final implementation methods are diverse, and you can adapt the various modules of the engine in a way that you are familiar with and good at.

For example, modify the Factory mode creation JsView to annotate automatic instantiation; Or transform the JSON DSL into a VUe-like declarative syntax; Or simply use Lua instead of JavaScript, instead of the application development language.

Here is a JS application written with VS Code and running on a mobile phone:

Third, summary

Dynamic engine: github.com/kwai-ec/Hyb… I’ve posted the dynamic engine on Github, and you can clone it and modify it as you like.

This paper mainly introduces the core modules of dynamic engine, and expands the implementation method of each module step by step, hoping that you can understand the principle of dynamic engine from the process of manual implementation, but also understand the difference between the front-end technology stack and the client.

If you are interested, you can continue to improve the dynamic engine, add the ability you want to write more interesting JS applications ~

Hi, I am Xie from Kuaishou E-commerce wireless technology team is recruiting talents 🎉🎉🎉! We are the core business line of the company, which is full of talents, opportunities and challenges. With the rapid development of the business, the team is also expanding rapidly. We welcome you to join us and create world-class e-commerce products together. Hot positions: Android/iOS Senior Developer, Android/iOS expert, Java Architect, Product Manager (e-commerce background), test development… A lot of HC waiting for you ~ internal recommendation please send resume to >>> our email: [email protected] <<<, note my roster success rate is higher oh ~ 😘