高级概念
iOS 序列化
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
的原型。
为了说明
@interface NSArray : NSObject
+ (instancetype)arrayWithArray:(NSArray *)anArray;
- (id)objectAtIndex:(NSUInteger)index;
@property (readonly) NSUInteger count;
@end
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 构造函数的原型相同。实质上
const tableViewController = new UITableViewController() // returns a wrapper around a UITableViewController instance
Object.getPrototypeOf(tableViewController) === UITableViewController.prototype // returns true
围绕 Objective-C 对象只有一个 JavaScript 包装器,始终如此。这意味着 Objective-C 包装器维护 JavaScript 标识相等
tableViewController.tableView === tableViewController.tableView
要调用期望 Objective-C 类或对象的原生 API,只需传递类的 JavaScript 构造函数或对象的包装器即可。
如果 API 在 Objective-C 中声明为接受一个 Class
,那么 JavaScript 中的参数就是构造函数
NSString *className = NSStringFromClass([NSArray class]);
const className = NSStringFromClass(NSArray)
相反,如果 API 声明为接受特定类的实例,例如 NSDate
,那么参数就是继承自该类的对象的包装器。
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
NSDate *date = [NSDate date];
NSString *formattedDate = [formatter stringFromDate:date];
const formatter = new NSDateFormatter()
const date = NSDate.date()
const formattedDate = formatter.stringFromDate(date)
期望 Objective-C 中的 id
数据类型的 API 意味着它将在 JavaScript 中接受任何 Objective-C 类或对象。
NSMutableArray *array = [[NSMutableArray alloc] init];
Class buttonClass = [UIButton class];
UIButton *button = [[buttonClass alloc] init];
[array setObject:buttonClass atIndex:0];
[array setObject:button atIndex:1];
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 方法
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)
@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 位设备上,我们需要使用 Float32Array
和 Float64Array
-- 在 64 位设备上。验证设备/模拟器体系结构的一种简单方法是通过 interop.sizeof(interop.types.id)
检查指针大小。指针大小的返回值对于 32 位体系结构将为 4 字节,对于 64 位体系结构将为 8 字节。有关更多信息,请查看 CGFloat 文档。
原始异常
NativeScript 认为 NSNull
、NSNumber
、NSString
和 NSDate
的实例是“原始类型”。这意味着这些类的实例不会通过包装器奇异对象在 JavaScript 中公开,而是会被转换为等效的 JavaScript 数据类型:NSNull
变成 null
、NSNumber
变成 number
或 boolean
、NSString
变成 string
,而 NSDate
变成 Date
。对此的例外是这些类上声明为返回 instancetype
的方法 - 初始化方法和工厂方法。这意味着对 NSString.stringWithString
的调用(其在 Objective-C 中的返回类型是 instancetype
)将返回一个围绕 NSString
实例的包装器,而不是 JavaScript 字符串。这适用于所有在 NSNull
、NSNumber
、NSString
和 NSDate
上返回 instancetype
的方法。
另一方面,任何在 Objective-C 中期望 NSNull
、NSNumber
、NSString
或 NSDate
实例的 API 都可以在 JavaScript 中使用包装器对象或 JavaScript 值调用 - null
、number
或 boolean
、string
或 Date
。NativeScript 会自动处理转换。
转换数字类型
console.log(`pow(2.5, 3) = ${Math.pow(2.5, 3)}`)
iOS 运行时将 JavaScript 数字字面量转换为原生双精度数,并使用原生 pow(double x, double y) 函数。生成的原生整数会自动转换回 JavaScript 数字,然后作为参数传递给 console.log() 以进行输出。
转换字符串
let button = UIButton.new()
button.setTitleForState('Button title', UIControlState.Normal)
console.log(button.titleLabel.text)
按钮标题
被转换为 NSString
,返回的 NSString
被转换为 JavaScript string
。
转换布尔值
let str = NSString.stringWithString('YES')
let isTrue = str.boolValue
Objective-C 协议
Objective-C 中的协议与其他编程语言中的接口具有相似的作用。它们定义了一个蓝图或契约,指定类应该实现的成员(方法、属性等)。协议在 JavaScript 中被公开为空对象。协议通常只在 子类化 Objective-C 类或检查对象或类是否符合协议时被引用。
BOOL isCopying = [NSArray conformsToProtocol:@protocol(NSCopying)];
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 投影将期望一个表示方法名称的字符串。
NSMutableString *aString = [[NSMutableString alloc] init];
BOOL hasAppend = [aString respondsToSelector:@selector(appendString:)];
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 函数
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");
}];
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 自动进行内存管理,因此无需使用 CFRetain
和 CFRelease
来保留或释放它们。
空值
Objective-C 有三个空值 - NULL
、Nil
和 nil
。NULL
表示指向零的常规 C 指针,Nil
是指向 Objective-C 类的 NULL
指针,而 nil
是指向 Objective-C 对象的 NULL
指针。它们在 JavaScript 中隐式转换为 null
。当使用 null
参数调用本机 API 时,NativeScript 会将 JavaScript null 值转换为指向零的 C 指针。某些 API 要求其参数不是指向零的指针 - 使用 JavaScript 中的 null 调用它们可能会导致应用程序崩溃,而没有恢复的机会。
数字类型
Objective-C 中的整数和浮点数数据类型被转换为 JavaScript 数字。这包括 char
、int
、long
、float
、double
、NSInteger
以及它们的无符号变体等类型。但是,大于 ±253 的整数值会丢失精度,因为 JavaScript 数字类型的尺寸限制为 53 位整数。
结构类型
NativeScript 将 Objective-C 结构公开为 JavaScript 对象。此类对象上的属性与它公开的结构上的字段相同。在 Objective-C 中需要结构类型的 API 可以使用与结构形状相同的 JavaScript 对象进行调用。
CGRect rect = {
.origin = {
.x = 0,
.y = 0
},
.size = {
.width = 100,
.height = 100
}
};
UIView *view = [[UIView alloc] initWithFrame:rect];
const rect = {
origin: {
x: 0,
y: 0,
},
size: {
width: 100,
height: 100,
},
}
const view = UIView.alloc().initWithFrame(rect)
有关 NativeScript 如何处理结构的更多信息,请参见 此处。
NSError **
序列化
原生到 JavaScript
@interface NSFileManager : NSObject
+ (NSFileManager *)defaultManager;
- (NSArray *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
@end
我们可以用以下方式从 JavaScript 中使用此方法
const fileManager = NSFileManager.defaultManager
const bundlePath = NSBundle.mainBundle.bundlePath
console.log(fileManager.contentsOfDirectoryAtPathError(bundlePath, null))
如果我们要使用输出参数检查错误
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 错误
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 提供了一种推理和访问内存位置的机制。为了说明这一点,请考虑以下示例
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 表示此代码
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')
}