NativeScript.ru

Кросс-платформенная разработка мобильных приложений

Как работает NativeScript

Как работает NativeScript

NativeScript (далее - NS) представляет из себя рантайм, позволяющий создавать нативные iOS, Android и Windows Universal приложения, программируя на Javascript (что, опять?!). Компания-разработчик - Telerik - сделала хитрый финт ушами, и в итоге мы имеем двухсторонние биндинги JS->native и возможность использовать CSS для стилизации приложения. Погодите разбегаться, это не PhoneGAP с WebView, тут все гораздо интереснее. Одна из важнейших особенностей NS, о которой и пойдет речь в данной статье, это механизмы платформы, позволяющие получить прямой доступ к платформозависимому API из Javascript. Круто, неправда ли? Это вам не React Native :) Но не будем торопиться и отправлять на биржу текущих “нативных” разработчиков под iOS и Android, просто разберем, как такое возможно. Давайте для примера рассмотрим небольшой кусок кода для NativeScript Android приложения:

var time = new android.text.format.Time();
time.set(1, 0, 2015);
console.log(time.format("%D"));

Негодуем

Что за гребаный Экибастуз? Да, это код на языке Javascript, создающий экземпляр Java-класса android.text.format.Time(), вызывающий метод set() на нем и записывающий в лог возвращаемое методом format() значение, что в итоге будет строкой “01/01/15”. Запасаемся попкорном… Кроличья нора становится глубже. Теперь рассмотрим пример для iOS (кстати, все примеры рабочие):

var alert = new UIAlertView();
alert.message = "Hello world!";
alert.addButtonWithTitle( "OK" );
alert.show();

Здесь наш Javascript код создает инстанс Objective-C класса UIAlertView, устанавливает свойство message и вызывает методы addButtonWithTitle() и show(). При запуске такого приложения на экране будет что-то вроде этого: iOS Hello World Выдохнули?

Прежде чем мы продолжим, нужно отметить одну интересную деталь: попросту потому, что мы имеем доступ к нативным API Android и iOS, совершенно не значит, что NS приложения содержат только заджаваскрипченный (надо записать слово) Objective-C или Java код. Платформа включает некоторое количество кросс-платформенных модулей для повседневных задач, к примеру, для работы с HTTP, с нативным UI и тому подобное. Но как уже было сказано, большинству приложений необходимо (хотя бы иногда) взаимодействовать с нативным API и NS рантайм позволяет обеспечить такие хотелки, когда необходимо. Давайте посмотрим, как ему это удается. We need to go deeper

Рантайм

Все это может казаться сущей магией (к которой, к слову, все более-менее дельные Javascript-разработчики привыкли), но, поверьте, не так страшен черт, как говорится. Что логично, все начинается с виртуальной машины Javascript (будем называть ее JSVM чтобы не было путаницы с JVM). NS использует ее для интерпретации Javascript команд (правда что-ли?). Если конкретнее, NS юзает движок V8 (node.js, привет!) на платформе Android и JavaScriptCore на iOS. Что еще более логично, любой нативно-API-зависимый код, который мы пишем, должен быть валидным кодом Javascript.

Вообще говоря, NativeScript пытается заюзать последние стабильные сборки V8 и JavaScriptCore. Соответственно, подедржка ECMAScript для iOS будет такой же, как и на десктопном (маковом) Safari. То же самое справедливо для Android - что есть в десктопном Chrome, то есть и у нас. Соответственно (внимание!) у нас даже есть доступ к некоторым уже поддерживающимся плюшкам ES6.

Понимание того, что NS использует JSVM важно, но это только начало всей мозаики. Вернемся к первой строчке кода в статье:

var time = new android.text.format.Time();

В рантайме NS Android этот код будет скомпилирован (JIT скомпилирован) и запущен движком V8. Как это работает для простых выражений вроде var x = 1+2; понятно, но откуда, черт возьми, V8 знает про android.text.format.Time()? Нельзя просто взять и

Далее все примеры будут для движка V8 и Android для простоты, но похожие архитектурные принципы применимы и к iOS с ее JavaScriptCore. Если будут какие-то различия, о них будет сказано отдельно. Также не будет обсуждаться работа NS в среде Windows Universal так как, во-первых, это экспериментальная фича, ну и в-дальнейших ее работа очень похожа на работу JavaScriptCore из iOS.

Как NativeScript работает с виртуальной машиной Javascript

V8 знает, что за android такой потому, что рантайм NS инжектит его вовнутрь. Вообще говоря, V8 имеет громадный API, позволяющих производить подобные вмешательства. Можно вставлять куски кастомного C++ кода для профайлинга Javascript, управлять сборщиком мусора да и вообще делать все, что захочется. V8 internals У V8 сотни API, кто бы знал…

Посреди всего этого разнообразия существует несколько "Context" классов, позволяющих манипулировать глобальным пространством имен. Они-то и позволяют NS инжектить объект android в глобальное пространство имен. Чуете, запахло знакомым? Ну точно, node.js делает то же самое! К примеру, NS испольузет require() также, как это делает node.js для инжекта своего API для взаимодействия с нативным. В iOS используется схожие техники. Такие дела! Вернемся к нашему коду:

var time = new android.text.format.Time();

Теперь мы знаем, что движок знает что за зверь такой android из-за того, что NativeScript инжектит необходимые объекты в глобальное пространство. С этим понятно, но остаются другие вопросы, например, каким образом NS знает, какое API инжектить и вообще что делать когда мы вызываем, к примеру, Time()? Начинаем глядеть за кулисы. Android js global scope

Метаданные

NativeScript использует Reflection для построения списка API, доступного для выбранной платформы (сплошной реверс-инжиниринг какой-то!). Если вы тертый калач в Javascript, понятие Отражения (рефлексии, reflection, зовите как хотите) вам может быть незнакомо, ибо оно здесь попросту не нужно. В других языках, особенно в Java, Reflection это единственный способ интроспекции объекта в рантайме. К примеру, в Java для получения списка методов какого-нибудь объекта Object нужно проинспектировать его (объект) с Reflection. Даже в PHP с 5-ой версии такая штука есть. Понятие Reflection выходит за рамки этой статьи, так что всем читать википедию и побольше кодить на компилируемых языках.

Для NativeScript Reflection является той палкой-выручалкой, которая позволяет получить исчерпывающий список доступного платформе API, включая упомянутый выше android.text.format.Time. Генерирование подобных структур суть долгая по времени операция (безделие суть ересь!), поэтому NS делает это заранее и включает сгенерированные метаданные при сборке Android\iOS проекта. В последний раз вернемся к нашему коду с этими знаниями:

var time = new android.text.format.Time();

Теперь мы знаем, что данный код запускается на движке V8, в который NS инжектит Javascript-объект android.text.format.Time, который, в свою очередь, инжектится отдельным NativeScript-процессом с метаданными, которые и будут включены в итоговую сборку проекта. Следующий вопрос на повестке дня - как NS превращает вызов джаваскриптового Time() в нативный android.text.format.Time() объект?

Вызов нативного кода

Ответ на вопрос “Как NativeScript вызывает нативный код” лежит в плоскости API виртуальной машины Javascript. Выше мы рассмотрели способ инжекта всякого мусора в global scope, а теперь мы подсмотрим, как коллбэки позволяют NativeScript запускать кастомный C++ код в нужных местах во время выполнения Javascript-кода (если кто спросит, почему C++ - потому что гладиолус!).

К примеру, код new android.text.format.Time() вызывает функцию JS, для которой у движка V8 есть коллбэк. Вот и приехали - у V8 есть функция обратного вызова (коллбэк), позволяющая NS перехватить вызов функции, давая возможность запуститься кастомному C++ коду, и возвращающая новый результат. В случае с Android, рантаймовый NativeScript C++ код не может напрямую взаимодествовать с Java API, таким, например, как android.text.format.Time. Однако, Android JNI, то бишь Java Native Interface, предоставляет возможность создания моста между C++ и Java кодом, и (барабанная дробь) NativeScript использует эту связку для своих темных делишек. На iOS такой мост не нужен, ибо код C++ может напрямую вызывать API Objective-C. Джеки негодуэ

Если ваш мозг переварил описанное выше безумие, предлагаю продолжить нашу вакханалию.

Вспомним про (да, опять…)

var time = new android.text.format.Time();

Итак, мы знаем, что данный код запускается на движке V8; движок знает про android.text.format.Time, а все потому, что NativeScript инжектит его в global scope, а инжектит он его потому, что есть отдельный, генерирующий метаданные, процесс для получения такого API. Также известно, что при вызове Time() происходит следующее:

  1. V8 вызывает функцию обратного вызова - коллбэк.
  2. Рантайм NativeScript использует свои метаданные чтобы понять, что вызов Time() означает инстанцирование объекта android.text.format.Time.
  3. Рантайм NativeScript использует JNI для инстанцирования нативного объекта android.text.format.Time и сохраняет ссылку на него.
  4. Рантайм NativeScript возвращает объект Javascript, который проксирует (подменяет) Java-объект Time.
  5. Контроль возвращается к Javascript, где проксированный объект сохраняется в локальной переменной time.

Проксированный объект - это и есть, в сущности, как NativeScript поддерживает маппинг объектов Javascript в их нативные версии.

Что у нас там дальше было?

var time = new android.text.format.Time();
time.set(1, 0, 2015);

Из сгенерированных метаданных NS знает, какие методы включить в проксированный объект, и в данном случае код выше вызывает метод set() объекта Time. При запуске данного метода движок V8 снова вызывает свой коллбэк; NS определяет, что это вызов метода и использует JNI для осуществления соответствующего вызова на Java-объекте Time. Всё. я все понял Собственно, вот где и кроется основной оверхед над полностью нативным приложениями - нужно какое-то время для преобразования всех этих JS->Native и обратно вызовов. Разработчики говорят про 10% падение производительности, но в текущей бете NS оно достигает 40-50% с фризами при запуске и ошалелым потреблением памяти. Такие вот дела. Правда, не будем забывать, что это не релиз. окей, ясно

Собственно, это практически все что нужно знать о том, как работает NativeScript изнутри, если вы обычный Javascript-разработчик. Опущены действительно сложные вещи, ибо портирование Objective-C и Java объектов не такое простое, как кажется, учитывая различные модели наследования каждого из языков.

Так что не будем рыть глубже того, что у нас уже есть, ибо вряд ли это поможет вам при написании типичных приложений NativeScript. Но есть еще одна штука, позволяющая не окунаться в говны нативного кода с частотой тактового генератора - это модули TNS.

Модули TNS

TNS модули (Telerik NativeScript) что-то вроде модулей Node.js, но только требующие для своей работы рантайм NativeScript. Модули являются CommonJS-совместимыми, как и стандартные модули Node.js, так что если вы уже знаете как работает require() и exports, вы знаете и как работают модули TNS. С помощью данных модулей можно абстрагироваться от платформозависимого кода в пользу платформо-независимого API. Компания Telerik к выходу беты NativeScript уже подготовила кучку таких модулей. Ну, к примеру, нужно нам создать файл в системе Anroid/iOS. Для Android сферический код в вакууме будет примерно таким:

new java.io.File( path );

а для iOS - таким:

NSFileManager.defaultManager();
fileManager.createFileAtPathContentsAttributes( path );

А для NativeScript - таким:

var fs = require( "file-system" ),
    file = new fs.File( path );

ну давай, расскажи мне за нативскрипт

Собственно, внутри модулей никакой особой магии нет - в них используются все те же техники, описанные выше в данной статье. Т.е. мы можем открыть любой модуль, подглядеть как он написан, какие дергает нативные API, ну и запилить свой велосипед, с педалями и сидушкой. Пример кода в модуле TNS для двух различных платформ, который выводит версию операционной системы устройства:

// device.ios.js
module.exports = {
    version: UIDevice.currentDevice().systemVersion
}

// device.android.js
module.exports = {
    version: android.os.Build.VERSION.RELEASE
}

Ясен паровоз, что, в зависимости от платформы, будет дергаться одна версия из этих двух возможных (будет же еще Windows потом, не забыли?). Ну и как уже стало понятно, подключение модулей TNS ничем не отличается от других node.js (npm) модулей:

var device = require( "./device" );
console.log( device.version );

В общем, модули TNS штука хорошая и приятная, так что с течением некоторого времени, если платформа NativeScript наберет обороты, можно ждать появления камазов говнокода, ибо подключить модуль как в node.js и быстро пробежаться по его API не то же самое, чем изучать литературу по нативным вызовам, их граблям, аргументам и т.п.

Ну и напоследок несколько волнующих общественность ответов на вопросы:

  • Можно ли использовать модули Node.JS совместно с NativeScript? - “Можно, только осторожно”. Что-то можно, что-то нельзя. Если модуль дергает какие-то специфичные вещи (например, любые модули для работы с базами данных), то практически всегда ответ - нет (пока нет). Если это просто библиотека, которая может работать и без Node.js (например, в браузере), то да, но есть детали. Так что на вопрос: “Можно ли подключить модули Node.js для работы с БД?” ответ - нет. По крайней мере пока.
  • А что насчет существующих JavaScript библиотек навроде Angular, jQuery, Backbone? - Если либа не зависит от объектной модели документа (DOM) браузера, то можно. Остальное - в разработке.

Так что начинаем пробовать NativeScript уже сейчас.

Ах да, чуть не забыл - все, что здесь написано, является вольным переводом статьи из блога разработчиков. Желающим прочесть оригинал - ссылка.

<< Сюда