在Android平臺下NativeScript原理

On NativeScript for Android

nativescript_android_header

We, at Telerik, recently announced our new solution for native cross-platform mobile development calledNativeScript. In this blog post I would like to explain some of the details for the Android platform. More specifically, I would like to explain some of the details of an important component from NativeScript, namely the JavaScript-Java bridge. For the sake of this article all use of the NativeScript is in the context of the Android platform.

A High Level Overview

Let’s start with a high-level overview and then delve into the details. The first decision that our team had to make was the choice of a JavaScript engine. Frankly, it was an easy one. We decided to use V8 because it is fast, compact and already well-proven in other projects like Node.js and Chrome. We link statically to V8 so it becomes part from *.apk package file. This prevents us from any kind of incompatibility problems and versioning issues.

nativescript1

The next important decision was to define the application workflow and lifecycle. We wanted to offer something familiar to the existing Android developers, so we decided to structure NativeScript around the Dalvik VM. As a matter of fact, every NativeScript app is just a normal Android app. This means that we use the standard things like anAndroidManifest.xml file and the android.app.Application class. It is just that we hide them in order to offer a better experience for cross-platform mobile development. The obvious benefits are the reuse of a familiar programming model and lifecycle events such as onCreate or onLowMemory.

Let’s have a look at the AndroidManifest.xml file.

<application
        android:name="com.tns.NativeScriptApplication"
        ...

As you can see we use a custom application class that helps us to initialize the application and V8 engine properly. As a matter of fact there is almost no NativeScript specific code here. The important thing we do in there is to load the NativeScript native library via the standard Java API so we can use it later via JNI.

static {
	System.loadLibrary("NativeScript");
}

The other important thing we do is to overwrite onCreate method and pass the content of a special file called bootstrap.js (think of it as of a C-stylemain() method) to the V8 engine. That’s it, we don’t have any other NativeScript specific logic in this class.

How Lifecycle Events are Exposed

If you are a careful reader, you probably recall that I wrote that NativeScript exposes the standard Android lifecycle app events so you may be curious how we do it. We implement this functionality by introducing the so-called “Binding classes”. These are automatically generated classes in Telerik namespaces and extend the original Android classes. Their sole purpose is so that the NativeScript framework can handle the two-way JavaScript-Java communication in a controlled manner.

For each non-final class in the Android API (and any other 3rd party class in the future) we generate this “binding.” In short, we overwrite every non-final public and protected method with the following pseudo-code implementation.

public [return_type] method_name (params) {
     if (is_there_JavaScript_overwrite_for_this_method) {
   	return excuteJavaScriptMethod(this, “method_name”, params);
     } else {
	return super.method_name(params);
     }
}

No magic here, it is just simple and straightforward. Let’s see the a few examples.

Suppose you want to create a new button using Java. The code you probably would write is as follows:

import android.content.Context;
import android.widget.Button;
...
Context context = …;
Button btn = new Button(context);

With NativeScript in JavaScript this code fragment would like something as:

var context = ...
var btn = new android.widget.Button(context);

Or probably you would use an alias as follows:

var Button = android.widget.Button;
var context = …;
var btn = new Button(context);

In this scenario NativeScript will create an instance ofandroid.widget.Button and will bind it to the corresponding JavaScript variable.

Inheritance

I guess, the next question you may have is about inheritance. Let’s look at an example.

import android.widget.Button;
public class MyButton extends Button {
	// overwrite method setEnabled(…) for example
}
...
MyButton btn = new MyButton(context);

We provide the following syntax in NativeScript:

var MyButton = new android.widget.Button.extends({
	setEnabled: function(enabled) {
	  	// do something
	}
});
var btn = new MyButton(context);

In this scenario NativeScript will create an instance of our binding for theButton class. It will create an instance from the following class:

package com.telerik.nativescript.android.widget;
public class Button extends android.widget.Button {
    ...
    @Override
    public void setEnabled(boolean enabled) {
        if (is_there_JavaScript_overload_for_setEnabled_method) {
            executeJavaScriptMethod(this, “setEnabled”, enabled);
        } else {
            super.setEnabled(enabled);
        }
    }
    ...
}

Interfaces

By now, you probably have a good idea what happens behind the curtains in NativeScript. We use the very same approach for Java interfaces. We generate stub classes that just forward the calls to the actual JavaScript implementation. Let’s see a concrete example.

button.setOnClickListener(new View.OnClickListener() {
   public void onClick(View v) {
       // Perform action on click
   }
});

As before, in NativeScript we try to provide as similar a syntax as possible.

button.setOnClickListener(new android.view.View.OnClickListener({
  onClick:  function() {
// Perform action on click
        }
}));

You just use the new keyword on the interface name and provide the implementation object as the sole argument.

Overloaded Methods

These examples were nice and simple. So far, we haven’t talked about overloaded methods. Yeah, nasty business right. Well, I have good news for you: NativeScript supports overloaded methods as well.

The careful reader probably noticed the different method signatures for theonClick method from previous example. The Java code uses onClick(View v) while the JavaScript code isonClick: function(). I did this on purpose. I could easily define onClick: function(v), but I want to emphasize that, in JavaScript functions, arguments can be accessed via thearguments keyword. There is no such thing as overloaded methods in JavaScript. So, we map JavaScript and Java methods by name. Hence, all overloaded Java methods would be mapped to a single JavaScript one. The developer must use thearguments collection and do the proper method dispatch. NativeScript also maps all Java constructors to theinit method in JavaScript.

Type Conversion

So far, I explained some of the decisions we made in NativeScript. While it was relatively easy to map concepts like Java inheritance to JavaScript prototype-based inheritance, there are other more challenging issues. Java and JavaScript use different type systems, which makes number conversion quite challenging.

This is a subject for another blog post but for the sake of this one I will say that NativeScript provides cast-like functions (byte, short, long, etc.) that help the process of (overloaded) method resolution.

Exception Handling

The next important decision we had to make was how NativeScript would handle Java and JavaScript exceptions. We opted for automatic exception conversion. This means that you can catch Java exceptions in JavaScript and access all members of the exception object, for example you can call the getMessage() method from JavaScript. Accordingly, when you throw a JavaScript exception in a Java callback implementation (e.g. in an overloaded method) this JavaScript exception would be rethrown as a special unchecked NativeScriptException in Java. In more complex scenarios this Java exception can be propagated back in JavaScript and NativeScript will take care of translating it back to the original JavaScript object.

Here is a quick example how you can catch Java exception in JavaScript:

try {
	var btn = new android.widget.Button(null);
} catch (ex) {
	var msg = ex.getMessage();
	// print msg
}

Multithreading

I already mentioned that Java and JavaScript differ in concepts such as inheritance and type systems. These differences can be mitigated and somehow abstracted from the developer in most cases. But there are other concepts where Java and JavaScript are really different. For example multithreading.

JavaScript does not have the concepts of threads. Java software nowadays makes extensive usage of multithreading and parallelism. So how do we map it in JavaScript?

So far, NativeScript offers limited support for this scenario. We brainstormed different ideas and we are quite optimistic that we can provide support for asynchronous and multithreading scenarios. For now, we opted to dispatch any Java-to-JavaScript call from a non-UI thread to the UI thread. This is also a very broad topic which we will cover in future blog posts.

Conclusion

NativeScript takes the task of providing the best possible experience for cross-platform mobile development using JavaScript very seriously. We try to combine the best of both the Java and JavaScript worlds. With NativeScript, developers can provide partial interface implementations and use all the dynamic features JavaScript provides. There are some trade-offs to numeric type conversions and to Java and JavaScript type systems in general. However, there is seamless support for both Java and JavaScript exceptions and there is a simple and practical initial multithreading support with many open possibilities like introducing Web Worker model and other alternatives. Stay tuned!

Why Developers Should Help With User Interface TestsNativeScript – a Technical Overview

Comments


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章