8.7 版本发布—WinterCG 兼容性第一部分
了解更多

NativeScript for iOS 处理 JavaScript 和 Objective-C 数据类型之间的转换。以下是 NativeScript 在 JavaScript 中公开 Objective-C API 时遵循的规则和例外情况的详尽但非详尽列表。

转换 Objective-C 类和对象

类可以具有实例方法、静态方法和属性。NativeScript 将 Objective-C 类及其成员公开为 JavaScript 构造函数,并根据 原型继承模型 关联一个原型。这意味着 Objective-C 类上的每个静态方法都成为其 JavaScript 构造函数上的一个函数,每个实例方法都成为 JavaScript 原型上的一个函数,每个属性都成为同一原型上的一个属性描述符。每个为公开 Objective-C 类而创建的 JavaScript 构造函数都排列在一个原型链中,该原型链镜像了 Objective-C 中的类层次结构:如果 NSMutableArray 扩展 NSArray,而 NSArray 又扩展了 Objective-C 中的 NSObject,那么在 JavaScript 中,NSObject 构造函数的原型是 NSArray 的原型,而 NSArray 的原型又是 NSMutableArray 的原型。

为了说明

objc
@interface NSArray : NSObject

+ (instancetype)arrayWithArray:(NSArray *)anArray;

- (id)objectAtIndex:(NSUInteger)index;

@property (readonly) NSUInteger count;

@end
js
var NSArray = {
  __proto__: NSObject,

  arrayWithArray: function () {
    [native code]
  }
}

NSArray.prototype = {
  __proto__: NSObject.prototype,

  constructor: NSArray,

  objectAtIndex: function () {
    [native code]
  },

  get count() {
    [native code]
  }
}

Objective-C 类的实例在 JavaScript 中存在为特殊的“包装器”奇异对象 - 它们跟踪和引用原生对象,以及管理它们的内存。当原生 API 返回一个 Objective-C 对象时,NativeScript 为它构建一个包装器,以防它不存在。包装器具有与普通 JavaScript 对象相同的原型。此原型与公开本机对象所属类的 JavaScript 构造函数的原型相同。实质上

js
const tableViewController = new UITableViewController() // returns a wrapper around a UITableViewController instance
Object.getPrototypeOf(tableViewController) === UITableViewController.prototype // returns true

围绕 Objective-C 对象只有一个 JavaScript 包装器,始终如此。这意味着 Objective-C 包装器维护 JavaScript 标识相等

js
tableViewController.tableView === tableViewController.tableView

要调用期望 Objective-C 类或对象的原生 API,只需传递类的 JavaScript 构造函数或对象的包装器即可。

如果 API 在 Objective-C 中声明为接受一个 Class,那么 JavaScript 中的参数就是构造函数

objc
NSString *className = NSStringFromClass([NSArray class]);
js
const className = NSStringFromClass(NSArray)

相反,如果 API 声明为接受特定类的实例,例如 NSDate,那么参数就是继承自该类的对象的包装器。

objc
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
NSDate *date = [NSDate date];
NSString *formattedDate = [formatter stringFromDate:date];
js
const formatter = new NSDateFormatter()
const date = NSDate.date()
const formattedDate = formatter.stringFromDate(date)

期望 Objective-C 中的 id 数据类型的 API 意味着它将在 JavaScript 中接受任何 Objective-C 类或对象。

objc
NSMutableArray *array = [[NSMutableArray alloc] init];
Class buttonClass = [UIButton class];
UIButton *button = [[buttonClass alloc] init];
[array setObject:buttonClass atIndex:0];
[array setObject:button atIndex:1];
js
const array = new NSMutableArray()
const buttonClass = UIButton
const button = new buttonClass()
array.setObjectAtIndex(buttonClass, 0)
array.setObjectAtIndex(button, 1)

使用多个参数调用 Objective-C/Swift 方法

考虑以下 NSMutableArray 选择器:replaceObjectsInRange:withObjectsFromArray:range:

在 JavaScript 中,它表示为:replaceObjectsInRangeWithObjectsFromArrayRange(objectsToRange, sourceArray, sourceRange)(参数名称是任意的)。

在 Objective-C 中,当为方法生成函数名称时,它遵循一个约定,即附加 Objective-C 选择器定义的参数的名称。函数名称以第一个参数的小写字母开头,并将后续参数附加以大写字母。

此命名约定有助于根据方法的参数创建唯一且描述性的函数名称。通过将参数名称包含在函数名称中,它在使用 Objective-C API 时提供清晰度和可读性。

重要的是要注意,此约定特定于 Objective-C,可能与其他编程语言中的命名约定不同。

有关如何扩展 Objective-C/Swift 类的示例,请查看 在 NativeScript 中扩展 iOS 类

将 JavaScript 数组转换为 CGFloat 数组

以下代码展示了如何将 JavaScript 数组转换为 CGFloat 数组,以便将其传递给期望 CGFloat 作为参数的 Objective-C 方法

js
const CGFloatArray =
  interop.sizeof(interop.types.id) == 4 ? Float32Array : Float64Array
const jsArray = [4.5, 0, 1e-5, -1242e10, -4.5, 34, -34, -1e-6]

FloatArraySample.dumpFloats(CGFloatArray.from(jsArray), jsArray.length)
objc
@interface FloatArraySample
+ (void)dumpFloats:(CGFloat*) arr withCount:(int)cnt;
@end

@implementation TNSBaseInterface

+ (void)dumpFloats:(CGFloat*) arr withCount:(int)cnt {
    for(int i = 0; i < cnt; i++) {
      NSLog(@"arr[%d] = %f", i, arr[i]);
    }
}
@end

注意

请记住,CGFloat 是与体系结构相关的。在 32 位设备上,我们需要使用 Float32ArrayFloat64Array -- 在 64 位设备上。验证设备/模拟器体系结构的一种简单方法是通过 interop.sizeof(interop.types.id) 检查指针大小。指针大小的返回值对于 32 位体系结构将为 4 字节,对于 64 位体系结构将为 8 字节。有关更多信息,请查看 CGFloat 文档

原始异常

NativeScript 认为 NSNullNSNumberNSStringNSDate 的实例是“原始类型”。这意味着这些类的实例不会通过包装器奇异对象在 JavaScript 中公开,而是会被转换为等效的 JavaScript 数据类型:NSNull 变成 nullNSNumber 变成 numberbooleanNSString 变成 string,而 NSDate 变成 Date。对此的例外是这些类上声明为返回 instancetype 的方法 - 初始化方法和工厂方法。这意味着对 NSString.stringWithString 的调用(其在 Objective-C 中的返回类型是 instancetype)将返回一个围绕 NSString 实例的包装器,而不是 JavaScript 字符串。这适用于所有在 NSNullNSNumberNSStringNSDate 上返回 instancetype 的方法。

另一方面,任何在 Objective-C 中期望 NSNullNSNumberNSStringNSDate 实例的 API 都可以在 JavaScript 中使用包装器对象或 JavaScript 值调用 - nullnumberbooleanstringDate。NativeScript 会自动处理转换。

转换数字类型

ts
console.log(`pow(2.5, 3) = ${Math.pow(2.5, 3)}`)

iOS 运行时将 JavaScript 数字字面量转换为原生双精度数,并使用原生 pow(double x, double y) 函数。生成的原生整数会自动转换回 JavaScript 数字,然后作为参数传递给 console.log() 以进行输出。

转换字符串

ts
let button = UIButton.new()
button.setTitleForState('Button title', UIControlState.Normal)
console.log(button.titleLabel.text)

按钮标题 被转换为 NSString,返回的 NSString 被转换为 JavaScript string

转换布尔值

ts
let str = NSString.stringWithString('YES')
let isTrue = str.boolValue

Objective-C 协议

Objective-C 中的协议与其他编程语言中的接口具有相似的作用。它们定义了一个蓝图或契约,指定类应该实现的成员(方法、属性等)。协议在 JavaScript 中被公开为空对象。协议通常只在 子类化 Objective-C 类或检查对象或类是否符合协议时被引用。

objc
BOOL isCopying = [NSArray conformsToProtocol:@protocol(NSCopying)];
js
const isCopying = NSArray.conformsToProtocol(NSCopying)

要在 NativeScript 中实现 Objective-C/Swift 协议,请查看 在 NativeScript 中符合 Objective-C/Swift 协议

Objective-C 选择器

在 Objective-C 中,SEL 是一种数据类型,它表示 Objective-C 类中的方法名称。NativeScript 以 JavaScript 字符串的形式公开这种数据类型。在使用 Objective-C 中的 API 时,如果 API 期望选择器值,那么 NativeScript 中的相应 JavaScript 投影将期望一个表示方法名称的字符串。

objc
NSMutableString *aString = [[NSMutableString alloc] init];
BOOL hasAppend = [aString respondsToSelector:@selector(appendString:)];
js
const aString = NSMutableString.alloc().init()
const hasAppend = aString.respondsToSelector('appendString:')

Objective-C 块

Objective-C 块 是 Objective-C 中的匿名函数。它们可以是闭包,就像 JavaScript 函数一样,并且经常用作回调。NativeScript 隐式地将 Objective-C 块公开为 JavaScript 函数。任何在 Objective-C 中接受块的 API 都在 JavaScript 中调用时接受 JavaScript 函数

objc
NSURL *url = [NSURL URLWithString:@"http://example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:nil completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    NSLog(@"request complete");
}];
js
const url = NSURL.URLWithString('http://example.com')
const request = NSURLRequest.requestWithURL(url)
NSURLConnection.sendAsynchronousRequestQueueCompletionHandler(
  request,
  null,
  (response, data, connectionError) => {
    console.log('request complete')
  }
)

Objective-C 中的块,尤其是那些充当闭包的块,需要正确的保留和释放以避免内存泄漏。但是,在 NativeScript 中,此内存管理是自动处理的。当块公开为 JavaScript 函数时,它会在函数被垃圾回收时被释放。相反,隐式转换为块的 JavaScript 函数在块被保留期间不会被垃圾回收。

CoreFoundation 对象

iOS 包含 Objective-C 标准库(Foundation 框架)和纯 C 标准库(Core Foundation)。Core Foundation 在很大程度上模仿 Foundation,并实现了一个有限的对象模型。CFDictionaryRef 和 CFBundleRef 等数据类型是 Core Foundation 对象。Core Foundation 对象与 Objective-C 对象一样被保留和释放,使用 CFRetain 和 CFRelease 函数。NativeScript 为注释为返回保留的 Core Foundation 对象的函数实现自动内存管理。对于那些没有注释的函数,NativeScript 返回一个 Unmanaged 类型,它包装 Core Foundation 实例。这使您部分负责保持实例的生存。您可以:

  • 调用 takeRetainedValue(),这将返回对包装实例的托管引用,并在执行此操作时递减引用计数
  • 调用 takeUnretainedValue() 方法,该方法将返回对包装实例的托管引用,不会递减引用计数。

无缝桥接

Core Foundation 有 无缝桥接类型 的概念 - 可以与它们的 Objective-C 对应类型互换使用的数据类型。在处理无缝桥接类型时,NativeScript 始终将其视为其 Objective-C 对应类型。Core Foundation 对象在 无缝桥接类型列表 上被公开,就好像它们是等效 Objective-C 类的实例一样。这意味着 JavaScript 中的 CFDictionaryRef 值在其原型上具有与 NSDictionary 对象相同的 方法。与常规的 Core Foundation 对象不同,无缝桥接类型由 NativeScript 自动进行内存管理,因此无需使用 CFRetainCFRelease 来保留或释放它们。

空值

Objective-C 有三个空值 - NULLNilnilNULL 表示指向零的常规 C 指针,Nil 是指向 Objective-C 类的 NULL 指针,而 nil 是指向 Objective-C 对象的 NULL 指针。它们在 JavaScript 中隐式转换为 null。当使用 null 参数调用本机 API 时,NativeScript 会将 JavaScript null 值转换为指向零的 C 指针。某些 API 要求其参数不是指向零的指针 - 使用 JavaScript 中的 null 调用它们可能会导致应用程序崩溃,而没有恢复的机会。

数字类型

Objective-C 中的整数和浮点数数据类型被转换为 JavaScript 数字。这包括 charintlongfloatdoubleNSInteger 以及它们的无符号变体等类型。但是,大于 ±253 的整数值会丢失精度,因为 JavaScript 数字类型的尺寸限制为 53 位整数。

结构类型

NativeScript 将 Objective-C 结构公开为 JavaScript 对象。此类对象上的属性与它公开的结构上的字段相同。在 Objective-C 中需要结构类型的 API 可以使用与结构形状相同的 JavaScript 对象进行调用。

objc
CGRect rect = {
  .origin = {
    .x = 0,
    .y = 0
  },
  .size = {
    .width = 100,
    .height = 100
  }
};
UIView *view = [[UIView alloc] initWithFrame:rect];
js
const rect = {
  origin: {
    x: 0,
    y: 0,
  },
  size: {
    width: 100,
    height: 100,
  },
}
const view = UIView.alloc().initWithFrame(rect)

有关 NativeScript 如何处理结构的更多信息,请参见 此处

NSError ** 序列化

原生到 JavaScript

objc
@interface NSFileManager : NSObject
+ (NSFileManager *)defaultManager;
- (NSArray *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
@end

我们可以用以下方式从 JavaScript 中使用此方法

js
const fileManager = NSFileManager.defaultManager
const bundlePath = NSBundle.mainBundle.bundlePath

console.log(fileManager.contentsOfDirectoryAtPathError(bundlePath, null))

如果我们要使用输出参数检查错误

js
const errorRef = new interop.Reference()
fileManager.contentsOfDirectoryAtPathError('/not-existing-path', errorRef)
console.log(errorRef.value) // NSError: "The folder '/not-existing-path' doesn't exist."

或者我们可以跳过传递最后一个 NSError ** 输出参数,如果从原生设置了 NSError **,则会抛出 JavaScript 错误

js
try {
  fileManager.contentsOfDirectoryAtPathError('/not-existing-path')
} catch (e) {
  console.log(e) // NSError: "The folder '/not-existing-path' doesn't exist."
}

JavaScript 到原生

在覆盖方法时,最后有 NSError ** 输出参数,任何抛出的 JavaScript 错误都会被包装并设置为 NSError ** 参数(如果提供)。

指针类型

C 家族中的语言,包括 iOS SDK,利用指针数据类型的概念。指针是表示另一个值的内存位置的值。但是,与基于 C 的语言不同,JavaScript 不原生支持指针。为了弥合这一差距,NativeScript 引入了 Reference 对象。引用是专门设计用于使 JavaScript 能够处理和与指针值交互的特殊对象。它们为 JavaScript 提供了一种推理和访问内存位置的机制。为了说明这一点,请考虑以下示例

objc
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory;
BOOL exists = [fileManager fileExistsAtPath:@"/var/log" isDirectory:&isDirectory];
if (isDirectory) {
    NSLog(@"The path is actually a directory");
}

此代码片段调用 NSFileManager 类的 Objective C fileExistsAtPath:isDirectory: 选择器方法。该方法将 NSString 作为其第一个参数,将指向布尔值的指针作为其第二个参数。当执行时,该方法使用提供的指针直接更新布尔值,从而允许修改 isDirectory 变量。可以通过以下方式使用 NativeScript 表示此代码

js
const fileManager = NSFileManager.defaultManager
const isDirectory = new interop.Reference()
const exists = fileManager.fileExistsAtPathIsDirectory('/var/log', isDirectory)
if (isDirectory.value) {
  console.log('The path is actually a directory')
}
上一个
序列化