задний план
Как важная отрасль технологии мобильных клиентов, динамизация всегда была направлением, активно исследуемым отраслью. В настоящее время все популярные в отрасли динамические решения, такие как React Native от Facebook и Weex от Alibaba, используют интерфейсные DSL-решения, и они могут без проблем работать в системах iOS без какого-либо героя: JavaScriptCore (для краткости ниже JSCore), который устанавливает мост между двумя языками Objective-C (далее OC) и JavaScript (далее JS). Будь то эти популярные динамические решения, или решение WebView Hybrid, или ранее широко популярный JSPatch, JSCore сыграла в этом ключевую роль. Для инженера-разработчика iOS понимание Javare постепенно стало одним из обязательных навыков.
из браузера
После iOS 7 компания Apple предоставила JCore разработчикам в качестве фреймворка системного уровня. JCore — важная часть браузерного движка Apple WebKit, который существует уже много лет. Если вы хотите вернуться к истокам и исследовать тайны JCore, вам следует начать с рождения языка JS и его самого важного хоста — браузера Safari.
Введение в историю JavaScript
JavaScript родился в 1995 году, его разработчиком является Брендан Эйх из Netscape, и в настоящее время Netscape является повелителем рынка браузеров.
Более 20 лет назад у людей был очень плохой опыт при просмотре веб-страниц, потому что браузеры в то время почти только имели возможность отображать страницы, но не имели возможности взаимодействовать с пользователями и логически их обрабатывать. Поэтому, даже если требуемое поле ввода пусто, оно должно быть проверено сервером, и ответ не будет дан, пока не будет возвращен результат, кроме того, скорость сети в это время очень низкая, может быть, полминуты прошло, и возвращенный результат должен сообщить вам, что определенные обязательные поля не заполнены. Итак, БренданНаписание JavaScript заняло десять дней, который интерпретируется и выполняется браузером. С тех пор браузер также имеет некоторые базовые возможности интерактивной обработки и возможности проверки данных формы.
И Брендан, вероятно, не ожидал этого более двух десятилетий спустя. JS, динамический язык сценариев, который интерпретирует и выполняет, не только стал «ортодоксальным» в области внешнего интерфейса, но и вторгся в область внутренней разработки, заняв первое место в рейтинге языков программирования, уступив только на Python и Java. Как интерпретировать и выполнять JS — это основная технология каждого движка. В настоящее время наиболее распространенными JS-движками на рынке являются Google V8 (используется в операционной системе Android и Google Chrome), а наш главный герой сегодня — JSore (используется в операционной системе iOS и Safari).
WebKit
Мы прикасаемся к браузерам каждый день, используя их для работы и развлечений. Основной частью обеспечения нормальной работы браузера является ядро браузера.Каждый браузер имеет свое собственное ядро.Ядром Safari является WebKit. WebKit родился в 1998 году, а исходный код был открыт Apple в 2005 году. Google Blink также был разработан на основе WebKit.
WebKit состоит из нескольких важных модулей.С помощью следующего рисунка мы можем получить общее представление о WebKit:
Проще говоря, WebKit — это механизм рендеринга страниц и логической обработки. Разработчики внешнего интерфейса принимают в качестве входных данных «тройку» из HTML, JavaScript и CSS. работать. Как видно из рисунка выше, WebKit состоит из четырех частей, обрамленных на рисунке. Наиболее важными из которых являются WebCore и JSore (или другие JS-движки), мы разделим эти две части на две небольшие главы, чтобы описать их подробно. Кроме того, WebKit Embedding API является частью, отвечающей за взаимодействие между пользовательским интерфейсом браузера и WebKit, а порты WebKit упрощают перенос Webkit на различные операционные системы и платформы и предоставляют некоторые интерфейсы для вызова Native Library, например, в На уровне рендеринга в системе iOS Safari передается CoreGraphics, а в системе Android Webkit передается Skia.
WebCore
На приведенной выше диаграмме состава WebKit мы видим, что только WebCore выделен красным цветом. Это связано с тем, что до сих пор у WebKit было много ответвлений, и крупные производители провели большую оптимизацию и преобразование, но часть WebCore является общей для всех WebKit. WebCore — это наиболее кодируемая часть WebKit и основной механизм рендеринга во всем WebKit. Итак, сначала давайте взглянем на процесс рендеринга всего WebKit:
Во-первых, браузер находит кучу файлов ресурсов, состоящих из HTML, CSS и JS, через URL-адрес и отправляет файлы ресурсов в WebCore через загрузчик (реализация этого загрузчика также очень сложна, поэтому я не буду вдаваться в подробности). подробности здесь). Затем синтаксический анализатор HTML преобразует HTML в дерево DOM, а синтаксический анализатор CSS преобразует CSS в дерево CSSOM. Наконец, два дерева объединяются для создания окончательного требуемого дерева рендеринга, а затем с помощью макета и интерфейса рендеринга конкретных портов WebKit дерево рендеринга визуализируется и выводится на экран, который становится окончательной веб-страницей, представляемой пользователю. Пользователь.
JSCore
Обзор
Напоследок поговорим о главном герое этого выпуска — JCore. JS-движок JS, встроенный в WebKit по умолчанию, называется JScore. Причина, по которой говорят, что он встроен по умолчанию, заключается в том, что многие браузерные движки, разработанные на основе веток WebKit, разработали свои собственные движки JS, наиболее известным из которых является Chrome V8. Все эти JS-движки выполняют одну и ту же задачу — интерпретировать и выполнять JS-скрипты. Из приведенной выше блок-схемы рендеринга мы видим, что существует взаимосвязь между JS и деревом DOM, потому что основная функция сценария JS в браузере — манипулировать деревом DOM и взаимодействовать с ним. Аналогично, мы также смотрим на его рабочий процесс через картинку:
Видно, что по сравнению со статически скомпилированными языками после генерации синтаксического дерева требуются такие операции, как связывание, загрузка и генерация исполняемых файлов, а процесс интерпретируемых языков значительно упрощается. Часть кадра в правой части этой блок-схемы — это часть JSCore: Lexer, Parser, LLInt и часть JIT (часть JIT отмечена оранжевым цветом, потому что не у всех JIT есть часть JIT). Далее мы познакомим каждую часть со всем рабочим процессом, который в основном разделен на следующие три части: лексический анализ, синтаксический анализ, интерпретация и выполнение.
PS: Строго говоря, сам язык не имеет компилируемого или интерпретируемого типа, потому что язык представляет собой лишь некоторые абстрактные определения и ограничения и не требует конкретных методов реализации и выполнения. Здесь JS — это «интерпретируемый язык», но JS обычно интерпретируется и выполняется динамически движком JS, а не атрибутом самого языка.
Лексический анализ -- Лексер
Хорошо изучен лексический анализ, который представляет собой процесс разложения написанного нами фрагмента исходного кода на последовательности токенов, который также называется сегментацией слов. В JCore лексический анализ выполняется с помощью Lexer (некоторые компиляторы или интерпретаторы называют сегментацию слов Scanner).
Это очень простое выражение языка C:
sum = 3 + 2;
После его токенизации можно получить следующую таблицу:
элемент | Тип отметки |
---|---|
sum | идентификатор |
= | оператор присваивания |
3 | количество |
+ | оператор сложения |
2 | количество |
; | конец заявления |
Это результат после лексического анализа, но лексический анализ не обращает внимания на отношения между каждым Токеном, совпадают они или нет, просто различает их и ждет, пока грамматический анализ «нанизает» эти Токены. Функция лексического анализа обычно вызывается синтаксическим анализатором. В JCore код лексера Lexer в основном сосредоточен в файлах parser/Lexer.h и Lexer.cpp.
Синтаксический анализ -- Парсер
Подобно человеческому языку, когда мы говорим, мы на самом деле следуем условностям, и наши привычки общения говорят одно слово за другим в соответствии с определенной грамматикой. Аналогия с компьютерным языком: компьютер должен понимать компьютерный язык, а также понимать синтаксис предложения. Например, следующий оператор JS:
var sum = 2 + 3;
var a = sum + 5;
Parser проанализирует последовательность токенов, сгенерированную после анализа Lexer, и создаст соответствующее абстрактное синтаксическое дерево (AST). Как выглядит это дерево? Порекомендуйте сайт здесь:esprima Parser, входной оператор JS может сразу сгенерировать нужный нам AST. Например, приведенный выше оператор сгенерирует такое дерево:
После этого ByteCodeGenerator сгенерирует байт-код JCore в соответствии с AST и завершит весь этап разбора грамматики.
Интерпретируемое выполнение -- LLInt и JIT
Исходный код JS прошел два этапа лексического анализа и синтаксического анализа и был преобразован в байт-код, что является необходимым шагом для любого языка программирования - компиляцией. Но в отличие от того, когда мы компилируем и запускаем код OC, после компиляции JS не будет создан объектный код или исполняемый файл, хранящийся в памяти или на жестком диске. Сгенерированный байт-код инструкции будет немедленно интерпретирован и выполнен построчно виртуальной машиной JCore.
Байт-код рабочей инструкции (ByteCode) является очень важной частью движка JS, и оптимизация каждого движка JS в основном сосредоточена на нем. Интерпретация и выполнение JSByteCode — это очень сложная система, особенно после добавления OSR и многоуровневой технологии JIT вся интерпретация и выполнение становятся все более и более эффективными, а выполнение всего байт-кода находится между низкой задержкой и высокой пропускной способностью. является хорошим балансом: LLInt с малой задержкой интерпретирует и выполняет ByteCode.При обнаружении нескольких повторных вызовов или рекурсии циклы и другие условия будут переключаться на JIT через OSR для интерпретации и выполнения (в соответствии с конкретными условиями срабатывания он будет вводить разные JIT для динамической интерпретации), чтобы ускорить процесс. Поскольку эта часть содержания относительно сложна и не находится в центре внимания данной статьи, это лишь краткое введение, а не подробное обсуждение.
Примечательные особенности JCore
В дополнение к вышеперечисленным частям, JCore имеет несколько примечательных особенностей.
Структура набора инструкций на основе регистров
В JCore используется структура набора инструкций на основе регистров.По сравнению со структурой набора инструкций на основе стека (например, в некоторых реализациях JVM), поскольку результаты операций не нужно часто выталкивать и извлекать из стека, эффективность выполнения набора инструкций эта архитектура более эффективна. Однако из-за такой архитектуры это также вызывает проблему больших накладных расходов на память.Кроме того, существует также проблема слабой переносимости, поскольку виртуальные регистры в виртуальной машине должны совпадать с регистрами ЦП в реальной машине. , и могут быть настоящие процессоры.Недостаточно регистров.
Структура набора инструкций на основе регистров обычно представляет собой набор инструкций с тремя или двумя адресами, например:
i = a + b;
//转成三地址指令:
add i,a,b; //把a寄存器中的值和b寄存器中的值相加,存入i寄存器
Процесс операции в трехадресном наборе инструкций заключается в перемещении a и b в два регистра соответственно, а затем сложении значений двух регистров и сохранении их в третьем регистре. Это рабочий процесс трехадресной инструкции.
Стек на основе обычно представляет собой набор инструкций с нулевым адресом, потому что его работа не зависит от конкретных регистров, а использует стек операндов и определенные операторы для завершения всей операции.
однопоточный механизм
Стоит отметить, что весь код JS реализован в потоке, это не похоже на OC, Java и другие языки, которые мы используем, вы можете подать заявку на несколько потоков для решения некоторых трудоемких задач в вашей собственной среде выполнения. чтобы предотвратить блокировку основного потока. Сам JS-код не имеет возможности многопоточной обработки задач. Но почему JS также имеет многопоточность и асинхронность? Мощный механизм управления событиями является ключом к тому, чтобы JS также мог выполнять многопоточную обработку.
механизм управления событиями
Как упоминалось ранее, JS был создан для того, чтобы позволить браузерам иметь некоторые возможности взаимодействия и логической обработки. Взаимодействие между JS и браузером достигается посредством событий.Например, когда браузер обнаруживает, что произошел щелчок пользователя, он передает событие щелчка, чтобы уведомить поток JS о необходимости обработки события. С помощью этой функции мы также можем позволить JS выполнять асинхронное программирование.Проще говоря, при столкновении с трудоемкими задачами JS может передать эту задачу рабочему потоку (WebWorker), предоставленному хостом JS для обработки. После того, как рабочий поток завершит обработку, он отправит сообщение, чтобы сообщить потоку JS, что задача была выполнена, и выполнит соответствующий обработчик событий в потоке JS. (Но следует отметить, что, поскольку рабочий поток и поток JS не находятся в одной и той же среде выполнения, они не имеют общей области, поэтому рабочий поток не может работать с окном и DOM.)
Механизм связи между потоками JS и рабочими потоками, а также событиями браузера называется EventLoop, который похож на цикл выполнения iOS. Он имеет две концепции: одна — стек вызовов, а другая — очередь задач. Когда рабочий поток завершает асинхронную задачу, он помещает сообщение в очередь задач, и это сообщение является функцией обратного вызова во время регистрации. Когда стек вызовов пуст, основной поток берет сообщение из очереди задач и помещает его в стек вызовов для выполнения.Основной поток JS будет повторять это действие до тех пор, пока очередь сообщений не опустеет.
Рисунок выше примерно описывает управляемый событиями механизм JCore, и вся JS-программа на самом деле работает так. На самом деле это немного похоже на цикл выполнения iOS в состоянии простоя.Когда событие источника на основе порта пробуждает цикл выполнения, оно обрабатывает все исходные события в текущей очереди. Подход JS, основанный на событиях, на самом деле «аналогичен по назначению» очередям сообщений. Именно благодаря наличию рабочих потоков и механизмов, управляемых событиями, JS обладает многопоточными асинхронными возможностями.
JCore в iOS
После iOS7 Apple инкапсулировала Javare в WebKit с помощью Objective-C и предоставила его всем разработчикам iOS. Фреймворк JCore предоставляет приложениям, написанным на языках Swift, Objective-C и C, возможность вызывать JS-программы. В то же время мы также можем использовать JCore для вставки некоторых пользовательских объектов в среду JS.
Есть много мест, где можно использовать AOore в iOS, например, AOore, инкапсулированный в UIWebView, AOore, инкапсулированный в WKWebView, и AOore, предоставляемый системой. На самом деле, даже если они оба являются JCore, между ними есть много различий. Потому что с развитием языка JS появляется все больше и больше хостов JS, появляются различные браузеры и даже Node.js (работающий на основе V8), который распространен на стороне сервера. В зависимости от различных сценариев использования и постоянной оптимизации команды WebKit, JCore постепенно дифференцируется в разные версии. В дополнение к старой версии JCore, есть также Nitro (SquirrelFish), работающий в Safari, WKWebView и т. Д., Анонсированные в 2008 году. В этой статье мы в основном познакомимся с JCore Framework, который поставляется с системой iOS.
Официальная документация iOSВнедрение JCore очень простое, фактически оно в основном предоставляет приложению возможность вызывать JS-скрипты. Сначала мы «видим леопарда» через 15 открытых заголовочных файлов JCore Framework, как показано на следующем рисунке:
На первый взгляд понятий много. Однако, за исключением некоторых общих заголовочных файлов и некоторых очень подробных понятий, не так много общеупотребительных понятий.Я думаю, что есть только четыре понятия, которые необходимо понять: JSVM, JSContext, JSValue и JSExport. Ввиду того, что было опубликовано много статей, описывающих эти концепции, в этой статье делается попытка объяснить эти концепции с разных точек зрения (таких как принцип, сравнение расширений и т. д.).
JSVirtualMachine
Экземпляр JSVirtualMachine (далее JSVM) представляет собой автономную среду выполнения JS или ряд ресурсов, необходимых для запуска JS. У этого класса есть два основных применения: одно — поддержка одновременных вызовов JS, а другое — управление памятью объекта-моста между JS и Native.
JSVM — это первая концепция, которую мы собираемся изучить. Официальное введение JSVM предоставляет базовые ресурсы для выполнения JavaScript, и, судя по буквальному переводу имени класса, JSVM представляет собой виртуальную машину JS.Мы также упоминали концепцию виртуальной машины выше, поэтому давайте обсудим, что такое виртуальная машина. машина первая. Сначала мы можем взглянуть на (наверное) самую известную виртуальную машину — JVM (Java Virtual Machine). JVM в основном делает две вещи:
- Первое, что ему нужно сделать, это сгенерировать ByteCode, созданный компилятором JavaC (ByteCode на самом деле является инструкцией виртуальной машины JVM), чтобы сгенерировать машинные инструкции, необходимые для каждой машины, чтобы программа Java могла быть выполнена (как показано на рисунке ниже).
- На втором этапе JVM отвечает за управление пространством памяти, GC и интерфейс между Java-программой и Native (т. е. C, C++), необходимые для запуска всей Java-программы.
С функциональной точки зрения виртуальная машина языка высокого уровня в основном разделена на две части: одна часть представляет собой часть интерпретатора, которая используется для запуска байт-кода, сгенерированного компиляцией языка высокого уровня, а другая часть представляет собой среду выполнения. время выполнения, которое используется для открытия пространства памяти во время выполнения, управления и т. д. На самом деле, JCore часто считают оптимизированной виртуальной машиной для языка JS.Он делает что-то похожее на JVM, но по сравнению со статически скомпилированной Java, он также берет на себя работу по компиляции исходного кода JS в байт-код.
Поскольку JSore считается виртуальной машиной, что такое JSVM? По сути, JSVM — это абстрактная виртуальная машина JS, которая позволяет разработчикам работать напрямую. В приложении мы можем запускать несколько JSVM для выполнения разных задач. И каждый JSContext (описанный в следующем разделе) принадлежит JSVM. Но следует отметить, что каждая JSVM имеет свое собственное независимое пространство кучи, и GC может обрабатывать только объекты внутри JSVM (механизм JS GC будет кратко объяснен в следующем разделе). Поэтому нельзя передавать значения между разными JSVM.
Также стоит отметить, что в предыдущей главе мы упомянули однопоточный механизм JS. Это означает, что в JSVM только один поток может выполнять код JS, поэтому мы не можем использовать JSVM для многопоточных задач JS. Если нам нужна многопоточная обработка задач JS, нам нужно одновременно сгенерировать несколько JSVM для достижения цели многопоточной обработки.
GC механизм JS
JS также не требует от нас ручного управления памятью. Управление памятью JS использует механизм GC (Tracing Garbage Collection). В отличие от счетчика ссылок OC, Tracing Garbage Collection представляет собой цепочку ссылок, поддерживаемую GCRoot (Context).Если цепочка ссылок не может достичь узла объекта, объект будет переработан. Как показано ниже:
JSContext
JSContext представляет собой среду выполнения JS. Мы можем вызывать сценарии JS, создавая JContext, получать доступ к некоторым значениям и функциям, определенным JS, а также предоставлять интерфейс для доступа JS к Native объектам и методам.
JContext — это одна из концепций, которые мы часто используем, когда фактически используем JCore. Понятие «Контекст» мы все более или менее видели в других сценариях разработки, чаще всего его переводят как «контекст». Так что же такое контекст? Например, в статье мы видим предложение: «Он быстро выбежал.» Но если мы не смотрим в контекст, мы не знаем, что означает это предложение: Кто выбежал? Кто он? Почему он бежит?
Написание языка программирования, понятного компьютеру, похоже на написание статьи: нам нужен такой «контекст», чтобы выполнить любое выражение. Например, введение внешних переменных, глобальных переменных, определений функций, выделенных ресурсов и так далее. С помощью этой информации мы можем точно выполнить каждое предложение кода.
Аналогичным образом, JS-контекст является средой выполнения языка JS. Весь код JS должен выполняться в JS-контексте. То же самое верно и для WebView. В это время мы можем получить JS-контекст WebView через KVC. Запустить фрагмент кода JS через JSContext очень просто, как в следующем примере:
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var a = 1;var b = 2;"];
NSInteger sum = [[context evaluateScript:@"a + b"] toInt32];//sum=3
С API AssessmentScript мы можем выполнять код JS в ОС с помощью JContext. Его возвращаемое значение — это последнее значение, сгенерированное в JS, упакованное и возвращенное с JSValue (описанным в следующем разделе), которое принадлежит текущему JSContext.
Мы также можем вставить множество глобальных объектов или глобальных функций в JSContext через KVC:
JSContext *context = [[JSContext alloc] init];
context[@"globalFunc"] = ^() {
NSArray *args = [JSContext currentArguments];
for (id obj in args) {
NSLog(@"拿到了参数:%@", obj);
}
};
context[@"globalProp"] = @"全局变量字符串";
[context evaluateScript:@"globalFunc(globalProp)"];//console输出:“拿到了参数:全局变量字符串”
Это очень полезная и важная функция, и есть много известных фреймворков, использующих JCore, таких как JSPatch, чтобы использовать эту функцию для достижения очень умных вещей. Здесь мы говорим не слишком много о том, что вы можете с этим делать, а о том, как это на самом деле работает. В API JSContext есть одно примечательное свойство только для чтения — globalObject типа JSValue. Он возвращает глобальный объект текущего исполняемого объекта JContext.Например, в WebKit, JSContext вернет текущий объект Window. И этот глобальный объект на самом деле является ядром JSContext.Когда мы заходим и присваиваем значения через KVC и JSContext,Фактически он взаимодействует с этим глобальным объектом., почти все находится в глобальном объекте, можно сказать, что JSContext — это просто оболочка globalObject. В приведенных выше двух примерах эта статья берет globalObject контекста и преобразует его в объект OC, как показано ниже:
Видно, что этот globalObject сохраняет все переменные и функции, что еще раз подтверждает вышеприведенное утверждение (о том, почему globalObject, соответствующий объекту OC, является типом NSDictionary, мы опишем в следующем разделе). Так что можно сделать еще один вывод, так называемые глобальные переменные и глобальные функции в JS — это всего лишь свойства и функции глобального объекта.
Также стоит отметить, что каждый JSContext принадлежит JSVM. Мы можем получить JSVM, связанную с текущим JSContext, через доступное только для чтения свойство JSContext — virtualMachine. Между JSVM и JSVM существует отношение «многие к одному»: JSVM может быть привязан только к одной JSVM, но JSVM может одновременно содержать несколько JSVM. Как мы упоминали выше, у каждой JSVM есть только один поток для одновременного выполнения JS-кода, поэтому в целом процесс простого запуска JS-кода через JSore и получения возвращаемого значения на нативном уровне примерно выглядит следующим образом:
JSValue
Экземпляр JSValue — это указатель на значение JS. Мы можем использовать класс JSValue для преобразования между основными типами данных Objective-C и JS. В то же время мы также можем использовать этот класс для создания объектов JS, которые обертывают пользовательские классы Native, или тех объектов JS, которые реализуют методы JS, предоставляемые собственными методами или блоками.
В разделе JSContext мы коснулись множества переменных типа JSValue. В разделе JSContext мы узнали, что мы можем легко манипулировать глобальным объектом JS через KVC, а также можем напрямую получать возвращаемое значение результата выполнения кода JS (при этом каждое значение в JS существует в среде выполнения , а также Другими словами, каждое JSValue существует в JSContext, который является областью действия JSValue), и все потому, что JSCore помогает нам использовать JSValue для автоматического преобразования типов между OC и JS на нижнем уровне.
AOore предоставляет в общей сложности 10 типов обмена следующим образом:
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
При этом также предоставляется соответствующий API обмена (выдержка):
+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context;
+ (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context;
- (NSArray *)toArray;
- (NSDictionary *)toDictionary;
Прежде чем говорить о преобразовании типов, давайте сначала разберемся с типами переменных языка JS. Согласно определению ECMAScript (которое можно понимать как стандарт JS): В JS существует два типа данных значений, один из которых является значением базового типа, который относится к простому сегменту данных. Второе — это значение ссылочного типа, которое ссылается на объекты, которые могут состоять из нескольких значений. К примитивным значениям относятся «undefined», «nul», «Boolean», «Number», «String» (да, String тоже примитивный тип), а все остальное — ссылочный тип. Не следует много говорить о замене первых пяти основных типов. Далее сосредоточимся на обмене ссылочными типами:
NSDictionary <--> Object
В предыдущем разделе мы преобразовали globalObject из JSContext в объект OC и обнаружили, что он имеет тип NSDictionary. Чтобы понять это преобразование, сначала у нас есть краткое представление об объектно-ориентированных функциях языка JS. В JS объект — это экземпляр ссылочного типа. В отличие от привычных OC и Java, объект не является экземпляром класса, поскольку в JS не существует понятия класса. ECMA определяет объект как неупорядоченный набор свойств, свойства которого могут содержать примитивные значения, объекты или функции. Из этого определения мы можем сделать вывод, что объекты в JS представляют собой неупорядоченные пары ключ-значение, которые аналогичны NSDictionary в OC и HashMap в Java.
var person = { name: "Nicholas",age: 17};//JS中的person对象
NSDictionary *person = @{@"name":@"Nicholas",@"age":@17};//OC中的person dictionary
В приведенном выше примере кода автор использует аналогичный метод для создания объектов в JS (называемый «объектным литералом» в JS) и NSDictionary в OC, что, как мне кажется, может быть более полезным для понимания этих двух преобразований.
NSBlock <--> Function Object
В примере из предыдущего раздела автор назначил блок «globalFunc» в JS-контексте, который можно вызывать непосредственно как функцию в коде JS. Я также могу использовать ключевое слово typeof для определения типа globalFunc в JS:
NSString *type = [[context evaluateScript:@"typeof globalFunc"] toString];//type的值为"function"
В этом примере мы также можем обнаружить, что входящий объект Block был преобразован в тип «функция» в JS. Концепция «Объект-функция» может быть непонятна нам, разработчикам, которые привыкли писать на традиционных объектно-ориентированных языках. По сути, язык JS, помимо базовых типов, является ссылочным типом. Функция на самом деле является объектом типа «Функция», и каждое имя функции на самом деле является ссылкой на объект функции. Например, мы можем определить функцию в JS следующим образом:
var sum = function(num1,num2){
return num1 + num2;
}
В то же время мы также можем определить такую функцию (не рекомендуется):
var sum = new Function("num1","num2","return num1 + num2");
По второму способу написания мы можем интуитивно понять, что функция — это тоже объект, ее конструктор — это функция, а имя функции — просто указатель на этот объект. NSBlock — это класс, который оборачивает указатели на функции.AOore преобразует Function Object в объект NSBlock, который можно назвать очень подходящим.
JSExport
Реализация протокола JSExport может предоставить классы OC и их методы экземпляра, методы класса и свойства для вызовов JS.
В дополнение к специальным типам преобразования, упомянутым в предыдущем разделе, у нас остается тип NSDate, и необходимо разобраться с преобразованием с типами id и class. Тип NSDate не нуждается в подробностях, поэтому в этом разделе мы сосредоточимся на разъяснении преобразования двух последних.
Обычно, если мы хотим использовать классы и объекты в OC в среде JS, они нужны нам для реализации протокола JSExport для определения свойств и методов, доступных для среды JS. Например, нам нужно предоставить класс Person и метод для получения имени в среде JS:
@protocol PersonProtocol <JSExport>
- (NSString *)fullName;//fullName用来拼接firstName和lastName,并返回全名
@end
@interface JSExportPerson : NSObject <PersonProtocol>
- (NSString *)sayFullName;//sayFullName方法
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end
Затем мы можем передать экземпляр JSExportPerson в JSExportPerson и напрямую выполнить метод fullName:
JSExportPerson *person = [[JSExportPerson alloc] init];
context[@"person"] = person;
person.firstName = @"Di";
person.lastName =@"Tang";
[context evaluateScript:@"log(person.fullName())"];//调Native方法,打印出person实例的全名
[context evaluateScript:@"person.sayFullName())"];//提示TypeError,'person.sayFullName' is undefined
Это очень простой пример использования JSExport, но обратите внимание, что мы можем вызывать только тот метод, который открыт в объекте в JSExport. Если он не открыт, например, метод «sayFullName» в приведенном выше примере, если он вызывается напрямую, он сообщит об ошибке TypeError, поскольку метод не определен в среде JS.
После разговора о конкретном использовании JSExport давайте рассмотрим наш первоначальный вопрос. Когда объект OC передается в среду JS, он преобразуется в JSWrapperObject. Вопрос в том, что такое JSWrapperObject? В исходном коде JCore мы можем найти некоторые подсказки. Во-первых, в JSValue от JCore мы можем найти такой метод:
@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)value inContext:(JSContext *)context;
Этот API может передавать объект OC любого типа, а затем возвращать значение JSValue, содержащее объект OC. Этот процесс определенно включает в себя обмен объектами OC на объекты JS, поэтому нам просто нужно проанализировать исходный код этого метода (на основеэта веткаанализ). Поскольку реализация исходного кода слишком длинная, нам нужно сосредоточиться только на коде ядра.В JSContext есть метод "wrapperForObjCObject", но на самом деле он вызывает метод "jsWrapperForObject" JSWrapperMap. Этот метод может ответить на все сомнения:
//接受一个入参object,并返回一个JSValue
- (JSValue *)jsWrapperForObject:(id)object
{
//对于每个对象,有专门的jsWrapper
JSC::JSObject* jsWrapper = m_cachedJSWrappers.get(object);
if (jsWrapper)
return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context];
JSValue *wrapper;
//如果该对象是个类对象,则会直接拿到classInfo的constructor为实际的Value
if (class_isMetaClass(object_getClass(object)))
wrapper = [[self classInfoForClass:(Class)object] constructor];
else {
//对于普通的实例对象,由对应的classInfo负责生成相应JSWrappper同时retain对应的OC对象,并设置相应的Prototype
JSObjCClassInfo* classInfo = [self classInfoForClass:[object class]];
wrapper = [classInfo wrapperForObject:object];
}
JSC::ExecState* exec = toJS([m_context JSGlobalContextRef]);
//将wrapper的值写入JS环境
jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec);
//缓存object的wrapper对象
m_cachedJSWrappers.set(object, jsWrapper);
return wrapper;
}
В процессе создания объекта «JSWrapperObject» мы создадим соответствующий JSObjCClassInfo для каждого входящего объекта через JSWrapperMap. Это очень важный класс, у него есть прототип (Prototype) и конструктор (Constructor) этого класса, соответствующий объекту JS. Затем JSObjCClassInfo генерирует объект JSWrapper конкретного объекта OC.Этот объект JSWrapper содержит всю информацию, требуемую объектом JS (т.е. прототип и конструктор), а также указатель на соответствующий объект OC. После этого запишите объект jsWrapper в среду JS, после чего объект можно будет использовать в среде JS. Это истинное лицо «JSWrapperObject». Как мы упоминали выше, если передается класс, в среде JS будет сгенерирован объект-конструктор, так что это также легко увидеть из исходного кода, когда обнаруживается, что класс передается (сам класс также объект), он будет напрямую возвращать свойство конструктора, которое является истинным лицом «объекта конструктора», который на самом деле является конструктором.
Тогда есть еще два вопроса.Первый вопрос заключается в том, что объекты OC имеют свои собственные отношения наследования, так как же описать эти отношения наследования в среде JS? Второй вопрос: как вызываются методы и свойства JSExport в среде JS?
Давайте сначала рассмотрим первый вопрос, как решить отношения наследования? В JS наследование достигается через цепочку прототипов, так что же такое прототип? Объект-прототип — это обычный объект и экземпляр функции-конструктора. Все объекты, сгенерированные этим конструктором, совместно используют этот объект.При поиске значения атрибута объекта, а результат не существует, он переходит к объекту-прототипу объекта, чтобы продолжить поиск, чтобы увидеть, существует ли атрибут, таким образом достигая цель инкапсуляции. Мы быстро разбираемся через объект-прототип Person:
//原型对象是一个普通对象,而且就是Person构造函数的一个实例。所有Person构造函数的实例都共享这一个原型对象。
Person.prototype = {
name: 'tony stark',
age: 48,
job: 'Iron Man',
sayName: function() {
alert(this.name);
}
}
Цепочка прототипов — это ключ к реализации наследования в JS, ее суть в том, чтобы переписать объект-прототип конструктора и связать объект-прототип другого конструктора. Таким образом, чтобы найти свойства объекта, он будет продолжать поиск по цепочке прототипов, чтобы достичь цели наследования. Давайте быстро рассмотрим пример:
function mammal (){}
mammal.prototype.commonness = function(){
alert('哺乳动物都用肺呼吸');
};
function Person() {}
Person.prototype = new mammal();//原型链的生成,Person的实例也可以访问commonness属性了
Person.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(); // 弹出'哺乳动物都用肺呼吸'
person1.sayName(); // 'tony stark'
И когда мы генерируем информацию о классе объекта (конкретный код см. в разделе «allocateConstructorAndPrototypeWithSuperClassInfo»), мы также генерируем информацию о классе родительского класса. Для каждого класса OC, реализующего JSExport, в JSContext будет предоставлен прототип. Например, класс NSObject имеет соответствующий прототип объекта в JS. Для других классов OC будет создан соответствующий прототип, а внутренний атрибут прототипа [Prototype] будет указывать на прототип, созданный для родительского класса этого класса OC. Эта цепочка прототипов JS может отражать отношения наследования соответствующего класса OC.В приведенном выше примере Person.prototype назначается как объект экземпляра млекопитающего, то есть процесс связывания прототипа.
Поговорив о первом вопросе, давайте рассмотрим второй вопрос. Как JSExport предоставляет методы OC среде JS? Ответ на этот вопрос также появляется, когда мы генерируем 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);
});
Для каждого свойства и метода, объявленных в JSExport, classInfo сохранит соответствующее свойство и метод в прототипе и конструкторе. После этого мы можем получить фактический SEL с помощью методов установки и получения, сгенерированных конкретными методами methodName и PropertyName. Наконец, к методам и свойствам в JSExport можно получить правильный доступ. Проще говоря, JSExport отвечает за маркировку этих методов, используя methodName в качестве ключа, SEL в качестве значения и сохраняя его в карте (прототип и конструктор по сути являются картой), а затем вы можете получить соответствующий метод через имя_метода.SEL для вызова. Это также объясняет, что в приведенном выше примере, когда мы вызываем метод, который не открыт в JSExport, он будет отображать undefined, потому что в сгенерированном объекте нет такого ключа.
Суммировать
JCore предоставляет iOS-приложению среду выполнения и ресурсы, которые JS может интерпретировать и выполнять. Для нашей актуальной разработки наиболее важными являются два класса: JSContext и JSValue. JSContext предоставляет интерфейс для вызова друг друга, а JSValue обеспечивает промежуточное преобразование типов данных для этого взаимного вызова. Пусть JS выполняет методы Native, а Native вызывает обратно JS, и наоборот.
С JCore мы можем делать много творческих вещей. Вся гибридная разработка на основе JCore в основном опирается на принцип, показанный на рисунке выше, для реализации взаимных вызовов, разница в том, что конкретные методы реализации и использования не совпадают. Путь прост, если правильно понять этот базовый процесс, все остальные решения — лишь некоторые модификации, которые можно быстро освоить.
какое-то расширенное чтение
Объекты и методы JSPatch не реализуют протокол JSExport Как JS корректирует метод OC?
JS в OC не через JSExport. На пути реализации через JSExport много проблем, нужно сначала написать нативный класс и реализовать протокол JSExport, который сам по себе не может удовлетворить потребности "Patch".
Таким образом, JSPatch использует другой подход и использует для этого механизм пересылки сообщений среды выполнения OC, как показано в следующем простом коде вызова JSPatch:
require('UIView')
var view = UIView.alloc().init()
-
require создает переменные UIView в глобальной области, чтобы указать, что этот объект является OCClass.
-
При регулярном изменении .alloc() на ._c('alloc') метод закрывается, и, наконец, будет вызываться _methodFunc() для передачи имени класса, объекта и имени метода в среду OC через собственный метод, уже определенный в Контекст.
-
Наконец, вызывается метод CallSelector OC.Нижний уровень использует имя класса, имя метода и объект, полученные из среды JS, а затем реализует динамический вызов через NSInvocation.
Коммуникация JSPatch не проходит через протокол JSExport, но использует AOore Context и преобразование типов OCore, а также механизм пересылки сообщений OC для выполнения динамических вызовов.Идея реализации действительно умная.
Как реализация мостового метода взаимодействует с JCore?
На рынке есть два распространенных вызова метода моста:
-
Запросы Bridge JS обрабатываются с помощью метода делегата UIWebView: shouldStartLoadWithRequest. JSRequest принесет имя метода и вызовет метод через класс WebViewBridge. После выполнения WebView будет использоваться для выполнения метода обратного вызова JS.Конечно, JContext в вызываемом WebView фактически выполняется для завершения всего вызывающего процесса обратного вызова.
-
Через метод делегата UIWebView: получите JContext UIWebView через KVC в webViewDidFinishLoad webViewDidFinishLoad, а затем установите подготовленный метод моста через этот JS-контекст для вызова среды JS.
использованная литература
- «Продвинутое программирование на JavaScript»
- Webkit Architecture
- Обсуждение виртуальной машины 1: Интерпретатор...
- Дай Мин: Углубленный анализ WebKit
- JSCore-Wiki
- [Знание Tw93] JCore в iOS
об авторе
Тан Ди — старший инженер Meituan Dianping. Он присоединился к первоначальному Meituan в 2017 году и в настоящее время является основным разработчиком команды iOS на вынос.Он в основном отвечает за построение мобильной инфраструктуры, динамическое и другое связанное с этим продвижение, а также стремится повысить эффективность и качество мобильных исследований и разработок.
Прием на работу
Meituan Waimai уже давно набирает старших/старших инженеров и технических экспертов по Android, iOS, FE в Пекине, Шанхае и Чэнду Заинтересованные студенты могут отправить свои резюме на chenhang03#.meituan.com.