NativeScript APP 核心原理
了解 NativeScript
NativeScript 是一个 JavaScript 运行时,它允许你完全使用 TypeScript 或 JavaScript 编写 iOS 或 Android 应用,并且不会牺牲任何原生能力。也就是说,它提供了与直接使用:iOS:Objective-C / Swift 以及 Android:Java / Kotlin 来开发应用时同等级别的原生访问能力。它不仅仅支持可以 JSON 序列化的数据类型,例如 NSString,而是支持所有原生数据类型。
为了说明 NativeScript 的能力,下面是一个使用 NativeScript 获取手机电池电量的例子:
import { isIOS, isAndroid, Application } from "@nativescript/core";
/**
* 返回电池电量
* (至少在 iOS 上),范围是 0.0 到 1.0
*
* @platform iOS 或 Android API level 21+
*/
function getBatteryLevel(){
if(isIOS){
UIDevice.currentDevice.batteryMonitoringEnabled = true;
const batteryLevel = UIDevice.currentDevice.batteryLevel;
UIDevice.currentDevice.batteryMonitoringEnabled = false;
return batteryLevel;
} else if(isAndroid){
const bm = Application.android.context.getSystemService(android.content.Context.BATTERY_SERVICE);
return bm.getIntProperty(android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY);
}
throw "Platform not yet supported!";
}
是不是感觉很神奇?🧙♂️ 你会发现:这些 JavaScript 调用实际上是一一对应到原生 API 的。并且它们是同步执行的,就像原生 API 本身一样。例如:
UIDevice.currentDevice.batteryLevel
对应 Objective-C:
[[UIDevice currentDevice] batteryLevel]
虽然上面的例子只是简单 API 调用,但 NativeScript 实际上可以完成 Objective-C 和 Java 运行时能做到的几乎所有事情,包括:
- 继承原生类
- 实现 delegate
- 传递函数
- 操作原生对象
- 多线程
- UI 管理 API
- 应用生命周期管理
- 更多原生能力
人人都可以访问 Native API!
能够直接在 JavaScript 环境中访问原生 API,对于开发者来说非常强大。这意味着你只需要掌握:一个 IDE、一个构建系统,然后通过热更新(Hot Reload)快速开发。显然 TypeScript / JavaScript 的开发体验更好,因为工具链更优秀。例如 TypeScript Language Service 类型检查速度快相比: Objective-C / Swift 的 SourceKit通常响应更快。
同时 NativeScript 的体验应该成为所有 App 开发框架努力的方向。事实上,NativeScript 技术委员会一直在尝试与其他框架结合。例如 Capacitor + NativeScript、React Native。项目 react-native-native-runtime尝试过 React Native 是否可以直接访问 Native API,作为概念验证让 React Native 调用 NativeScript。
NativeScript 的架构
首先,我们回顾一下 NativeScript 的整体架构。NativeScript 本质上由三个部分组成:
1、Metadata Generator(元数据生成器)
工具负责生成:描述所有可绑定 Native API 的元数据(metadata),包括:
- iOS:
ios-metadata-generator - Android:
android-metadata-generator
这些工具会扫描原生 SDK,例如:UIKit、Foundation、Android SDK……然后生成描述信息。
2、iOS / Android Runtime
这是 NativeScript 的核心。它们是: V8/JavaScriptCore 修改而来的 JavaScript 引擎,其作用是根据 metadata:动态生成JavaScript ↔ Native之间的绑定。也就是说 metadata 告诉 Runtime:原生世界有哪些类、方法、属性。Runtime 根据这些信息在 JS 中创建对应对象。
3、NativeScript Core
这是一个高级 API 库,建立在上述绑定之上,提供跨平台 API。例如 Alert:
alert("hello")
创建文本框:
<TextField />
它的特点同一套代码可以运行在 iOS 或 Android 上。因此:如果你只是使用 NativeScript 作为完整跨平台框架,Core 就足够。但如果你只是想把 NativeScript 当作原生访问层那么 Core 可以不用。
小结
本文主要研究前两个部分:1. Metadata Generator;2. NativeScript Runtime,也就是:
Native API
↓
Metadata Generator
↓
metadata.bin
↓
NativeScript Runtime
↓
JavaScript Binding
JavaScript 到原生绑定
单独的 JavaScript 语言本身其实提供的能力并不多,因此,JavaScript 运行时通常会在 ECMAScript API 之外,额外提供一些来自平台的 API(即原生 API)。例如:
浏览器
浏览器为 JavaScript 增加了各种 Web API,例如 DOM、网络请求、文件访问等,其中 DOM:
document.querySelector();
实际上就是 JavaScript 调用了浏览器底层的原生能力。
Node.js
Node.js 增加了大量后端 API,例如:
process
文件系统:
fs.readFile();
很多时候,这些 API 都是在访问操作系统提供的能力。例如:
- 浏览器可以访问文件系统
- Node.js 可以访问文件系统
因此,在某个实现层面 JavaScript 引擎最终必须调用原生 API。而要让 JavaScript 环境调用原生 API 必须建立一种机制:
JavaScript
|
|
Binding
|
|
Native API
这个机制就是:JS → Native Binding(JavaScript 到原生绑定)。不同 JavaScript Runtime / Engine 实现绑定的方式不同。
不同 JavaScript Runtime 如何实现 JS → Native Binding?
比较不同方案非常有价值,下面逐个分析。
Node.js
在 Node.js 中理论上用户代码可以通过:
child_process
访问任意原生能力。但是真正意义上的 Native Binding,例如让 JavaScript 可以表示原生对象:
const nativeObject = ...
通常通过: C++ Addons 实现。Node.js 提供 C++ Addons API 允许 C++ 代码注册到 JavaScript 环境。Node.js 有三种实现 C++ Addon 的方式。其中一种基于 V8 API 实现。这一点与 NativeScript 使用 V8 Runtime 有关联。
浏览器
浏览器中的情况,如果想增加新的 Native API 基本只能修改浏览器源码。例如 Chrome 想增加navigator.xxx这样的 API,必须修改 Chromium。普通用户无法动态添加。
JavaScriptCore(JSC)
JavaScriptCore 是 Apple 的 JavaScript 引擎。Safari 在使用它。它支持通过 Runtime API 从用户代码层建立绑定,当然也可以修改源码。
React Native
React Native 无论使用哪个:JavaScriptCore/Hermes/V8 都可以建立 Native Binding。传统方式使用 JSON Bridge,例如NativeModules.Camera.takePhoto()调用原生CameraModule.takePhoto()。但是这个方式有限制,只能传递 JSON 可序列化的数据。例如:
{
"name":"test",
"age":10
}
但是不能直接传递:
- Native Object
- 文件句柄
- 原生类实例
后来 React Native 推出了: JSI(JavaScript Interface),JSI 可以让 JavaScript 直接表示 Native 数据类型,但是早期文档并不完善。
NativeScript
NativeScript 的方式不同。NativeScript 会自动生成所有公开 Native API 的 metadata,包括用户自己的 Native API 与 系统 SDK API,然后在运行时根据 metadata,自动创建绑定。
流程:
Native Header
|
|
Metadata Generator
|
|
metadata
|
|
NativeScript Runtime
|
|
JavaScript Object
如果你想在 JS 中创建一个方便调用原生 API 的函数有两种方式:
方式 1:直接用 JavaScript 写
例如之前的电池例子:UIDevice.currentDevice.batteryLevel完全由 JS 调用 Native。
方式 2:使用 NativeScript Plugin API
如果你希望用 Objective-C / Java 编写 Native 部分。例如已有一个原生 SDK:MyCameraSDK.framework你可以包装成 NativeScript Plugin。这样 JS:camera.takePhoto();底层调用 Objective-C:
[Camera takePhoto]
NativeScript 的独特之处
因此 NativeScript 最大的特点是:你可以在 JavaScript 中调用任意 Native API,并且可以表示任意 Native 数据类型,而不需要自己编写 Native 代码。更关键的是:你甚至不需要重新编译 App。因为 Webpack 监听代码变化,然后 Hot Reload。例如修改:
button.text = "Hello"
保存 App 立即更新。这就是 NativeScript 与传统跨平台方案的重要区别。
NativeScript Metadata Generator 工具如何工作?
下面进入 NativeScript 最核心的部分,以 iOS 为例子。简单来说 Metadata Generator 做的事情:读取所有进入 App 的公开 Header 文件,然后生成一个二进制 metadata 文件,它会被打包进入 NativeScript App。App 启动时读取 metadata,然后根据它建立 JS → Native Binding。
简单流程:
Objective-C Header
↓
Metadata Generator
↓
metadata.bin
↓
NativeScript Runtime
↓
JavaScript Classes
Metadata Generator 的具体流程
第一步:搜索 Header
通常它会第一步:搜索 Header,使用clang HeaderSearch API查找所有 Header。然后使用`clang RecursiveASTVisitor``遍历语法树。也就是说:它不是简单文本扫描,而是使用 Clang 编译器能力理解 Objective-C。例如遇到:
@interface UIDevice : NSObject
@property(nonatomic) float batteryLevel;
@end
它能够理解这是一个 Class UIDevice,有一个属性BatteryLevel,类型是float。
第二步:创建 Meta 对象
对于每个 Clang 声明调用MetaFactory::create()判断它是什么类型,例如:
- enum
- class
- interface
- method
- property
然后创建对应`Meta class instance``,例如:
@interface Person : NSObject
@property NSString *name;
@end
生成类似:
MetaClass:
name = Person
MetaProperty:
name
type = NSString
第三步:清理数据
生成所有 Meta 对象之后进行 cleanup。例如删除重复成员、无效声明。
第四步:序列化
最后把 Meta 对象序列化成二进制,生成metadata.bin,同时它还可以生成 TypeScript 定义文件。例如:
declare class UIDevice {
batteryLevel:number;
}
Metadata 长什么样?
实际进入 App 的 metadata 是一个:二进制文件,准确来说,它被格式化为 Mach-O 目标文件中的一个 section,然后交给 linker 处理。因此它不是人类可读的。但是,如果让 Metadata Generator 输出 YAML 格式,那么它会为每个模块生成一个 YAML 文件。例如ARKit.yaml这样的文件。
Metadata 中包含了 Header 文件中的所有详细信息,例如:
- 方法签名(method signatures)
- 类的实例方法(instance methods)
-枚举值(enum values)
-Header 文件位置 - 类型信息
等等。例如假设 Objective-C 有:
@interface Person : NSObject
@property(nonatomic, strong)
NSString *name;
- (void)sayHello:(NSString *)message;
@end
Metadata 中会记录类似:
Class:
Person
Property:
name
type: NSString
Method:
sayHello:
parameter:
NSString
Runtime 后面就是利用这些信息动态生成 JavaScript 世界中的Person对象。
NativeScript Metadata Generator 在构建流程中的哪个阶段运行?
传统 NativeScript App 的构建流程大致包含四个步骤。
1. pre-build(预构建)
执行nativescript-pre-build脚本的作用是设置大量环境变量。例如:
BUILD_DIR
ARCH
CONFIGURATION
2. post-build(构建后)
执行nativescript-post-build脚本。它主要调用strip-dynamic-framework-architectures.sh,其作用是
从 App 的Frameworks目录中的动态库中删除无效架构。例如开发环境可能包含:arm64、x86_64、i386,但是最终设备只需要 arm64,于是删除多余部分。可能只是为了减少开发阶段磁盘空间占用。
3. pre-link(链接前)
这个步骤现在基本只是一个占位脚本。因为内容只有:
set -e
没有实际逻辑。
4. link(链接阶段)
这是最关键的一步。Xcode 的:
LD
LDPLUSPLUS
两个 Build Setting 被设置为$SRCROOT/internal/nsld.sh,它们不是 Xcode 官方公开文档中的标准设置。通常默认 linker:clang路径:
/Applications/Xcode.app/
Contents/Developer/
Toolchains/
XcodeDefault.xctoolchain/usr/bin/clang
NativeScript 做了一层包装:nsld.sh,它并不是替代 linker。它只是先生成 metadata,然后调用真正 linker。
流程:
Xcode Build
|
v
nsld.sh
|
|
+---- Metadata Generator
|
|
v
metadata-x86_64.bin
|
v
clang linker
metadata 如何被加入 App?
通过 Xcode 参数OTHER_LDFLAGS增加:
-sectcreate
__DATA
__TNSMetadata
metadata-x86_64.bin
意思是把metadata-x86_64.bin作为一个 section 写入最终 Mach-O 文件。最终 App 实际上包含:
Executable
|
|
+-- __DATA
|
+-- __TNSMetadata
|
+-- metadata
所以 App 启动时 NativeScript Runtime 可以直接从内存读取 metadata。不需要:
- 打开文件
- 解析 JSON
- 反序列化
这也是为什么 NativeScript 可以做到高性能。
NativeScript Runtime 如何根据 Metadata 建立 Binding?
现在假设 App 已经编译完成,Metadata 也已经链接进去。接下来看看运行时发生什么。整体流程:
App Launch
|
|
读取 metadata
|
|
NativeScript Runtime
|
|
创建 JS 类
|
|
绑定 Native 方法
|
|
JavaScript 调用 Native
第一步:获取 Metadata 地址
因为 metadata 已经作为 Mach-O section 编译进 App,所以 Runtime 需要从内存找到它。有两种方式。
方法一:Inline Assembly
例如:
extern char startOfMetadataSection
__asm("section$start$__DATA$__TNSMetadata");
void* metadataPtr =
&startOfMetadataSection;
意思告诉编译器找到:
__DATA
|
+-- __TNSMetadata
这个 section 的开始地址。
方法二:Mach-O Section 查询
例如:
#import <mach-o/getsect.h>
static const struct mach_header_64 *
mhExecHeaderPtr =
&_mh_execute_header;
extern void* runtimeMeta(){
NSString *sectname =
@"__TNSMetadata";
NSString *segname =
@"__DATA";
unsigned long size;
void* meta =
getsectiondata(
&_mh_execute_header,
[segname cStringUsingEncoding:
NSUTF8StringEncoding],
[sectname cStringUsingEncoding:
NSUTF8StringEncoding],
&size
);
return meta;
}
注意这里:
__DATA
__TNSMetadata
正是前面-sectcreate指定的 section 名称。
Metadata 是否需要反序列化?
由于 metadata 是直接作为目标文件 section 链接进去的。所以可能不需要传统意义上的:
读取文件
↓
解析二进制
↓
创建对象
过程中它可能已经以 Meta 对象列表形式存在于内存中。整个流程大致如下:
1. 设置 Metadata
AppDelegate 中创建 NativeScript:
NativeScript *script = [[NativeScript alloc] init];
同时传入 metadata 地址。NativeScript 内部把 metadata pointer 设置到RuntimeConfig,然后调用Runtime::Initialize()。内部进一步调用:
MetaFile::setInstance(
RuntimeConfig.MetadataPtr
)
这个函数设置一个单例metaFileInstance。
之后其他模块例如 ArgConverter/MetadataBuilder……都可以通过:
MetaFile::instance()
访问 metadata。
第二步:创建绑定
接下来调用Runtime::Init(),这个阶段大量类开始初始化。其中两个非常重要:
ArgConverter
负责 JavaScript 类型 ↔ Native 类型转换。例如:
123
转换成 Objective-C:
NSInteger
或者
{
name:"Tom"
}
转换成 Native Object。
ClassBuilder
负责根据 metadata 动态创建 Native 类。ClassBuilder 使用了 Objective-C Runtime,例如:
class_addMethod()
这个函数可以动态给类增加方法。例如:
@interface Person
@end
运行时添加sayHello这个 objc 的方法。
然后 ClassBuilder 再通过 V8 API 把这些 Native 方法绑定到 JavaScript。最终:
person.sayHello()
会调用 Objective-C:
-[Person sayHello]
除此之外 Runtime 中还大量使用 C 语言的`dlsym()``,它是动态链接器 API,运行时查找函数地址。
总结(Conclusion)
NativeScript 传统上一直被作为一个端到端(end-to-end)的跨平台应用开发框架使用。但是实际上:它完全可以作为一个便捷的 JavaScript → Native Runtime(JS 到原生运行时) 使用。例如可以被集成到 React Native 应用,甚至完全原生开发的 App。为了让这种使用方式变得可行,NativeScript 团队正在改进它的 可嵌入性(embeddability),也就是说让 NativeScript 更容易作为一个底层能力被其他框架调用。而要做到这一点必须深入理解 NativeScript 底层运行机制。这样才能判断:
- 哪些长期存在的设计模式可以优化;
- 哪些构建步骤可以简化;
- 哪些模板代码可以删除。
因此本文进行了这次底层机制深入研究,而且不夸张地说 NativeScript Runtime 实际上是一个非常复杂的 JavaScript ↔ Native 桥接系统。
与 React Native 的核心区别
我们不妨看看 NativeScript 和 React Native 的核心区别。
React Native 传统架构
JavaScript
|
|
JSON Bridge
|
|
Native Module
|
|
iOS / Android API
特点:
- 需要写 Native Module
- 数据通常需要 JSON 化
- 跨 JS / Native 有通信成本
NativeScript 架构
JavaScript
|
|
Native Runtime
|
|
Metadata
|
|
iOS / Android API
特点:
- 不需要为每个 API 写桥接代码
- 可以直接访问系统 API
- 支持 Native 类型
- JS 可以直接创建 Native 对象
例如在 NativeScript 中调用 Android:
const vibrator =
Application.android.context
.getSystemService(
android.content.Context.VIBRATOR_SERVICE
);
vibrator.vibrate(500);
不需要创建:
VibratorModule.java
也不需要注册:
NativeModules.Vibrator
这也是为什么 NativeScript 可以做到:
“Write once, access everything native.”
对你目前使用 NativeScript Vue 的意义
结合你之前开发:
- NativeScript Vue App
- Android/iOS 双端
- MQTT 通讯
- 原生能力调用
这篇文章解释了为什么你可以直接:
import { Application } from "@nativescript/core";
然后访问 Android:
Application.android.context
甚至:
android.bluetooth.BluetoothAdapter
而不需要写 Java Plugin。但是也要注意 NativeScript 的强大来自 Runtime。代价是:
- 首次启动需要初始化 Runtime
- 包体积通常比纯原生大
- 调试 Native 深层问题需要理解 JS Runtime + Native Runtime 两边
这也是 NativeScript 与 Flutter、React Native 最大的设计差异之一:
Flutter:
自己绘制 UI,隔离 Native。
React Native:
JS 控制 Native Component。
NativeScript:
JS 直接成为 Native Runtime 的第一等公民。
这篇文章实际上就是在解释 NativeScript 最核心的技术优势。
https://github.com/NativeScript/NativeScript/wiki/Deep-dive:-How-NativeScript’s-JS–native-bindings-work
更多推荐

所有评论(0)