这是关于创建使用原生 iOS 和 Android 视图的 NativeScript 插件系列的第 2 部分。它建立在第 1 部分中学到的知识的基础上
我们将探索 NativeScript 的更多有趣方面,涵盖直接使用原生平台 API 的“原样”(Swift、Objective C、Kotlin 和 Java)以及在 TypeScript/JavaScript 中直接编写原生平台 API 带来的便利性 - 这是真正的集所有优点于一身的方法。我们甚至会提供一些性能指标的细分,以了解一种方法与另一种方法是否真的重要。
特别是,我们将创建一个闪烁插件,它可用于广泛改进用户体验,并使用各种幽灵占位符布局进行显示,而内容正在加载。
我们在第 1 部分(标题为:使用第三方或直接编码?)中讨论过,包含第三方代码有时可能是我们作为开发人员做出的最难的决定。对于这个插件,我们将避免依赖任何第三方并自行编写代码,因此我们唯一依赖的是平台本身(而不是其他第三方开源作者)。
这就是 NativeScript 经常成为开发者工具箱中最通用的工具的地方。你可以在 Swift、Objective C、Java、Kotlin 中编写闪烁效果,作为平台视图扩展,作为 CocoaPod 或 gradle 包含提供,或者最令人惊讶的是,在 TypeScript 或普通的旧 JavaScript 中。
为了好玩,让我们使用这些不同的方法为 iOS 和 Android 编写效果,以演示 NativeScript 在实践中的极大通用性。这样一来,我们甚至有机会运行一些性能指标,看看一种方法与另一种方法是否重要,还是仅仅是个人喜好。
在 第 1 部分中,我们创建了一个新的插件工作区,nstudio/nativescript-ui-kit,便于通过 官方插件种子 管理和维护插件,该种子还包含用于轻松添加更多包的工具。
npm run add
? What should the new package be named?
› nativescript-shimmer
? Should it use the npm scope of the workspace?
› true
"@nstudio/nativescript-shimmer" created and added to all demo apps.
Ready to develop!
这创建了一个包含所有所需自定义 NativeScript 插件基本内容的 packages/nativescript-shimmer
文件夹。它还注释了工作区中的所有演示应用程序,以便使用它,这样尝试起来既快速又高效。
我们继续前进 🚀
鉴于大多数公开可用的闪烁效果代码示例,比如这个,可能已经用 Objective C 或 Swift 编写,这可能是最快/最简单的方法来启动和运行。你可以直接在插件中包含 .swift 文件并立即使用它。例如,在 platforms/ios/src
中创建一个 .swift 文件
platforms/ios/src/Shimmer.swift
extension UIView {
@objc func startShimmering(
speed: Float = 1.4,
repeatCount: Float = MAXFLOAT
) {
// see: https://swiftdevcenter.com/uiview-shimmer-effect-swift-5/
}
@objc func stopShimmering() {
// see: https://swiftdevcenter.com/uiview-shimmer-effect-swift-5/
}
}
@objc
修饰符是 iOS 平台 API 功能,允许 NativeScript 元数据获取 Swift API。
这特别方便,因为包含像这样的UIView 扩展将为所有 iOS 视图(比如 UILabel、UIButton 等 - 任何从 UIView 派生的东西)提供这些新的原生平台 API。这意味着 NativeScript 的 Label
、Button
、StackLayout
、GridLayout
等现在立即具备这种新功能,例如
<Label text="Shimmer Me" loaded="{{ loaded }}"/>
export function loaded(args) {
const label = <Label>args.object;
// start shimmer effect!
label.ios.startShimmeringWithSpeedRepeatCount(1.4, Number.MAX_VALUE);
// then to stop it:
label.ios.stopShimmering();
}
这个 Swift 代码是如何...
func startShimmering(speed: Float = 1.4, repeatCount: Float = MAXFLOAT) {
变成下面在 TypeScript 中的代码的?
function startShimmeringWithSpeedRepeatCount(speed: number, repeatCount: number) {
NativeScript 始终会将原生平台 API 命名约定折叠为简单的可调用 JavaScript 结构,并遵循易于理解的规则,例如
// Swift:
func startShimmering(speed: Float = 1.4, repeatCount: Float = MAXFLOAT)
// NativeScript:
--- method name + 'With' + Capitalized Arguments ~
function startShimmering SpeedRepeatCount
// resulting in:
function startShimmeringWithSpeedRepeatCount(speed: number, repeatCount: number)
请记住,我们编写并集成了所有这些内容,而无需离开 VS Code - 真是太方便了。
但是,如果还不够酷,你还是 Swift 大师,你甚至可以在 Xcode 中打开项目并获得你想要的所有 Swift 智能感知 - 你甚至会在 Xcode 中看到插件,其中 Shimmer.swift
文件完全可编辑并保存回插件源,在那里进行管理
我们喜欢 TypeScript,所以让我们用 TypeScript 完全编写相同的内容。实际上,除了 NativeScript 的平台 API 智能感知外,你还可以使用我们上面学到的所有规则将这段超级酷的 Swift 代码转换为 NativeScript
import { View } from '@nativescript/core'
export class Shimmer {
static start(view: View, speed?: number, repeat?: number) {
startShimmering(view, speed, repeatCount)
}
static stop(view: View) {
stopShimmering(view)
}
}
// same platform API effect written in TypeScript
function startShimmering(view: View, speed = 1.4, repeatCount = Number.MAX_VALUE) {
// create color
const lightColor = UIColor.colorWithDisplayP3RedGreenBlueAlpha(1.0, 1.0, 1.0, 0.1).CGColor
const blackColor = UIColor.blackColor.CGColor
// create gradient
const gradientLayer = CAGradientLayer.layer()
gradientLayer.colors = NSArray.arrayWithArray([blackColor, lightColor, blackColor])
const viewSize = view.ios.bounds.size;
gradientLayer.frame = CGRectMake(-viewSize.width, -viewSize.height, 3 * viewSize.width, 3 * viewSize.height)
gradientLayer.startPoint = CGPointMake(0, 0.5)
gradientLayer.endPoint = CGPointMake(1, 0.5)
gradientLayer.locations = NSArray.arrayWithArray([0.35, 0.5, 0.65])
view.ios.layer.mask = gradientLayer
// animate over gradient
CATransaction.begin()
const animation = CABasicAnimation.animationWithKeyPath('locations')
animation.fromValue = [0.0, 0.1, 0.2]
animation.toValue = [0.8, 0.9, 1.0]
animation.duration = speed
animation.repeatCount = repeatCount
CATransaction.setCompletionBlock(() => {
view.ios.layer.mask = null
})
gradientLayer.addAnimationForKey(animation, 'shimmerAnimation')
CATransaction.commit()
}
function stopShimmering(view: View) {
view.ios.layer.mask = null
}
现在,这也适用于完全在 TypeScript 中实现
<Label text="Shimmer Me" loaded="{{ loaded }}"/>
export function loaded(args) {
const label = <Label>args.object;
// start shimmer effect!
Shimmer.start(label);
// then to stop it:
Shimmer.stop(label);
}
Objective C 是 iOS 上所有内容的基础,因此我们还可以使用另一个很棒的开源作者提供的这个出色的类别来用可靠的 Objective C 编写它 这里,所有功劳归于 Vikram Kriplaney。
platforms/ios/src/UIView+Shimmer.h
#import <UIKit/UIKit.h>
@interface UIView (Shimmer)
- (void)startShimmering;
- (void)stopShimmering;
@end
platforms/ios/src/UIView+Shimmer.m
#import "UIView+Shimmer.h"
@implementation UIView (Shimmer)
- (void)startShimmering
{
// see: https://github.com/markiv/UIView-Shimmer/blob/master/Classes/UIView%2BShimmer.m#L13-L30
}
- (void)stopShimmering
{
// see: https://github.com/markiv/UIView-Shimmer/blob/master/Classes/UIView%2BShimmer.m#L35
}
@end
为了使其正常工作,你还需要添加一个 module.modulemap
文件,该文件是 这里讨论的另一个 iOS 说明 以及 另一个很好的参考资料。
platforms/ios/src/module.modulemap
module UIViewShimmer {
header "UIView+Shimmer.h"
export *
}
在这种情况下,模块名称可以是任何内容,因为它只是一个 iOS 类别添加,我们还指定了确切的头文件,并确保所有内容都已导出。
现在,这也可以在纯粹的 Objective C 中实现,但通过 JavaScript 控制
<Label text="Shimmer Me" loaded="{{ loaded }}"/>
export function loaded(args) {
const label = <Label>args.object;
// start shimmer effect!
label.ios.startShimmering();
// then to stop it:
label.ios.stopShimmering();
}
几年前,TSC 的 Teodor Dermendzhiev 发表了一篇关于这个主题的精彩博客文章,其中也提到了其他细节。
有人可以掐我一下吗?作为工程师,我们一直在寻求通用的工具来解决现实世界的问题,而 NativeScript 不断地为我们提供无限的有效选择。
注意:根据您的各种项目依赖项,您可能存在或不存在隐式依赖项,该依赖项将引入 QuartzCore.framework
。当包含使用默认情况下未添加的各种 iOS 框架中部分 API 的 Objective C 源代码时,可能会导致类似于此的构建错误
Undefined symbols for architecture arm64:
"_OBJC_CLASS_$_CABasicAnimation", referenced from:
objc-class-ref in UIView+Shimmer.o
"_OBJC_CLASS_$_CAGradientLayer", referenced from:
objc-class-ref in UIView+Shimmer.o
ld: symbol(s) not found for architecture arm64
这纯粹是一个 iOS 构建错误,它与 2 个类有关,CABasicAnimation
和 CAGradientLayer
,这些类在该 Objective C 类别中使用,但是这些类是 QuartzCore.framework 的一部分,默认情况下不会包含。如果您遇到类似情况,请仔细检查 Apple 文档以查看是否提到的符号是 iOS 框架的一部分,并且您可以将 platforms/ios/build.xcconfig
文件添加到您的插件中,以使用以下内容包含它们
OTHER_LDFLAGS = $(inherited) -framework QuartzCore
这将确保 QuartzCore
框架包含在您的项目中,并解决构建问题。您可以对项目可能需要的任何其他框架执行此操作。您可以了解更多关于 xcconfig 文件的通用性。
对于 Android,我们可以用 Kotlin 从头开始编写我们自己的闪烁实现,但与 iOS 不同的是,Facebook 积极维护他们的 Android 闪烁实现,该实现是用 Java 编写的。与其仅仅使用他们的 gradle 插件(为了避免使用第三方包含,因为我们在本文中正在探索直接在原生平台 API 中使用),我们可以将他们的实现作为编写我们自己的实现的指南。我们甚至可以使用 Android Studio 将 Java 转换为 Kotlin,以便有一个很好的直接参考点。
Converting Java to Kotlin if you ever need to:
1. Open Android Studio to a folder containing .java source code
2. Select .java file, choose from the top menu: Code > Convert Java File to Kotlin File
3. You will now have `.kt` files to work with
使用 Android Studio 将 facebook 闪烁 .java 文件转换为 Kotlin,我们有了一个不错的入门基础。我们将进一步缩减源代码,使其只包含我们需要的基本闪烁效果功能,并重点介绍如何将 Kotlin 源代码与 NativeScript 结合使用。
你可以直接将 .kt
文件放到插件中并立即使用它们。所有 Kotlin 源代码都可以放在 platforms/android/java
文件夹中。Kotlin 源代码始终进一步嵌套在与它声明的打包路径匹配的文件夹结构中,例如
platforms/android/java/io/nstudio/ui/Shimmer.kt
package io.nstudio.ui
import android.animation.ValueAnimator
import android.graphics.Color
...
class Shimmer: FrameLayout {
fun start(
speed: Long,
direction: Int,
repeatCount: Int,
@ColorInt lightColor: Int,
@ColorInt blackColor: Int
) {
// see: https://gist.github.com/NathanWalker/5309671b8d80a10ea88b5da9730e3476
}
}
现在,Kotlin 源代码将被构建到我们添加了插件的任何应用程序中。实际上,如果我们现在使用演示应用程序构建它,NativeScript CLI 会自动为我们在 platforms/android
文件夹中为我们生成一个构建的 .aar
文件,如下所示
这非常方便,因为我们可以使用此 .aar
生成 TypeScript 声明。
在 第 1 部分(标题为:生成与之一起工作的类型)中,我们展示了如何从 iOS 平台 API 生成 TypeScript 声明,所以这次让我们对 Android 做同样的事情。我们可以通过将 .aar
文件拖放到像 The Unarchiver 这样的工具上来解压缩它,以解压缩并显示 classes.jar
文件,如下所示
现在,我们可以将该 jar 传递给 NativeScript CLI 的类型命令,该命令从任何 NativeScript 应用程序(如工作区的 apps/demo
)的根目录执行
apps/demo$ ns typings android --jar ../../packages/nativescript-shimmer/aar-unarchived/classes.jar
现在,我们的 TypeScript 声明已经准备好了,可以用来表示从 Kotlin 源代码创建的原生平台 API!
就像我们在第 1 部分中所做的那样,我们可以将这些类型移到我们的插件中以帮助我们开发。
创建一个 packages/nativescript-shimmer/typings
文件夹并将 android.d.ts
放入其中,更准确地说,我们将将其重命名为 android-shimmer.d.ts
,并删除顶部的 /// <reference ...
标签,因为它不再需要了
现在,我们可以将其添加到包 references.d.ts
中,以确保我们的 TypeScript 编辑器允许我们直接针对这些类型进行编码。
注意:现在可以删除 apps/demo/typings
文件夹,因为我们不再需要它了。
这使我们能够使用 Kotlin 源代码直接在 TypeScript 代码库中提供的原生平台 API 进行编码。
鉴于 Android 实现使用 Drawables 并且需要覆盖各种draw 方法以实现效果,我们可以使用此实现作为 NativeScript 视图组件,以在任何布局上启用闪烁效果。
import { ContentView, View } from '@nativescript/core';
export class Shimmer extends ContentView {
// @ts-ignore
get android(): io.nstudio.ui.Shimmer {
return this.nativeView;
}
createNativeView() {
// all {N} views can access their Android context via `this._context`
// construct shimmer effect using native platfom API Kotlin implementation
return new io.nstudio.ui.Shimmer(this._context);
}
initNativeView() {
// autostart shimmer effect
// could expose a property like 'autoStart' to allow user control
this.start();
}
start() {
// could expose optional method arguments for user to control shimmer settings
this.android.start(1000, 0, -1, new Color('rgba(255,255,255,1)').android, new Color('rgba(0,0,0,.8)').android);
}
stop() {
this.android.hideShimmer();
}
}
现在,我们可以用它来包装任何我们想闪烁的 NativeScript 布局
<Page xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:ui="@nstudio/nativescript-shimmer">
<StackLayout class="c-bg-black">
<ui:Shimmer>
<GridLayout class="p-20 text-left" rows="auto,5,auto,5,auto,5,auto">
<ContentView class="c-bg-gray" row="0" width="65%" height="20"/>
<ContentView class="c-bg-gray" row="2" height="60" width="100%"/>
<ContentView class="c-bg-gray" row="4" height="10" width="50%"/>
<ContentView class="c-bg-gray" row="6" height="10" width="30%"/>
</GridLayout>
</ui:Shimmer>
<StackLayout>
</Page>
要在 Angular 项目中使用它,你只需注册组件
import { registerElement } from '@nativescript/angular';
import { Shimmer } from '@nstudio/nativescript-shimmer';
registerElement('Shimmer', () => Shimmer);
// can now be used with Angular components...
<Shimmer>
<GridLayout ...>
<!-- etc. and so on -->
https://gist.github.com/triniwiz/b3f87c8d3d07d0c57f5f2c13ae14d71d
同样,我们喜欢 TypeScript,所以让我们用 TypeScript 完全编写相同的内容
// shimmer-android-typescript-implementation.ts
@NativeClass()
class ShimmerView extends android.widget.FrameLayout {
private mContentPaint = new android.graphics.Paint();
private mShimmerDrawable = new ShimmerDrawable();
private mShowShimmer = true;
private mStoppedShimmerBecauseVisibility = false;
// use constructor overloads just like Kotlin or Java would:
constructor(param0: android.content.Context);
constructor(param0: android.content.Context, param1?: android.util.AttributeSet) {
super(param0, param1);
this.init(param0, param1);
return global.__native(this);
}
// more here: https://gist.github.com/triniwiz/b3f87c8d3d07d0c57f5f2c13ae14d71d
}
export { ShimmerView };
就像我们上面使用 Kotlin 实现一样,我们可以用纯粹的 TypeScript 构造完全相同的内容
import { ContentView } from '@nativescript/core';
import { ShimmerView } from './shimmer-android-typescript-implementation';
export class Shimmer extends ContentView {
// @ts-ignore
get android(): ShimmerView {
return this.nativeView;
}
createNativeView() {
// construct shimmer effect using purely TypeScript implementation
return new ShimmerView(this._context);
}
...
现在,这与选项 A 的效果完全相同
<Page xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:ui="@nstudio/nativescript-shimmer">
<StackLayout class="c-bg-black">
<ui:Shimmer>
<GridLayout class="p-20 text-left" rows="auto,5,auto,5,auto,5,auto">
...
Java 是 Android 的基础,也是大学中最常教授的语言之一,因此有很多 Java 源代码公开可用。正如我们在上面看到的,Facebook shimmer 插件已经用 Java 编写。就像我们上面对 Kotlin 所做的那样,让我们使用它们的实现作为指南,并使用 Java 实现效果的一部分。
您可以将 Java 源代码包含在与 Kotlin 绝对相同的方式中,在一个名为 platforms/android/java
的文件夹中,该文件夹包含与它在其中声明的打包路径匹配的嵌套文件夹结构。
platforms/android/java/io/nstudio/ui/Shimmer.java
package io.nstudio.ui;
import android.animation.ValueAnimator;
import android.graphics.Color;
...
public class Shimmer extends FrameLayout {
public void startShimmer() {
mShimmerDrawable.startShimmer();
}
// see: https://github.com/nstudio/nativescript-ui-kit/blob/main/packages/nativescript-shimmer/platforms/android/java/io/nstudio/ui/Shimmer.java
}
由于此实现与我们对 Kotlin 所做的相同,因此为了简洁起见,我们将省略详细信息。在上面的 选项 A:用 Kotlin 编写它 中涵盖的所有内容,从 .aar
生成、类型生成到使用细节,无论您使用 Kotlin 还是 Java 都是完全相同的,这使得在 A 和 C 之间进行选择纯粹是个人喜好问题。
我们是在梦里吗?我梦想着能够做这种事情好几年了,也许我们是在梦里。
这甚至不局限于插件。您可以在您的应用程序中直接执行所有上述操作。只需将相同的原生平台 API 代码包含在 App_Resources/iOS/src/
或 App_Resources/Android/src/main/java/
中,所有内容都会完全相同 🤯
例如,将 App_Resources/iOS/src/Shimmer.swift
或 App_Resources/Android/src/main/java/io/nstudio/ui/Shimmer.kt
添加到您的应用程序中,将完全无需插件即可实现所有这些功能。😎
您选择哪种方式重要吗?
为了运行每种方法的性能指标,我们创建了一个包含 20 个 GridLayout 的 ScrollView,每个 GridLayout 包含许多视图,以呈现一个不错的幽灵占位符外观,看起来像这样
<Button text="Toggle Shimmer" tap="{{ toggleShimmer }}" class="c-white font-weight-bold text-center" fontSize="30"/>
<!-- 20 of these in a row -->
<GridLayout class="p-20 text-left" rows="auto,5,auto,5,auto,5,auto" loaded="{{ loadedView }}">
<ContentView class="h-left" row="0" width="65%" height="20" backgroundColor="#333"/>
<ContentView class="h-left" row="2" height="60" width="100%" backgroundColor="#333"/>
<ContentView class="h-left" row="4" height="10" width="50%" backgroundColor="#333"/>
<ContentView class="h-left" row="6" height="10" width="30%" backgroundColor="#333"/>
</GridLayout>
当每个 GridLayout 上的 loaded
事件触发时,它会跟踪对该视图的引用
loadedViews: Array<View> = [];
loadedView(args) {
this.loadedViews.push(args.object);
}
当启动 toggleShimmer
时
toggleShimmer() {
this.shimmer = !this.shimmer;
if (this.shimmer) {
this.startFPSMeter();
console.time('Starting Shimmer effect')
for (const view of this.loadedViews) {
Shimmer.start(view);
}
console.timeEnd('Starting Shimmer effect')
} else {
this.stopFPSMeter();
console.time('Stopping Shimmer effect')
for (const view of this.loadedViews) {
Shimmer.stop(view);
}
console.timeEnd('Stopping Shimmer effect')
}
}
这将使我们了解使用 Swift、TypeScript、Objective C、Kotlin 和 Java 实现,每种实现将闪烁效果应用于所有视图需要多长时间,这样我们就可以进行比较。
fps-meter 是 @nativescript/core 中的一个很好的实用程序,可用于测量每秒帧数,例如
import * as FPSMeter from '@nativescript/core/fps-meter';
startFPSMeter() {
this.callbackId = FPSMeter.addCallback((fps: number) => {
console.log(`Frames per second: ${fps.toFixed(2)}`);
});
FPSMeter.start();
}
stopFPSMeter() {
FPSMeter.removeCallback(this.callbackId);
FPSMeter.stop();
}
测试环境:运行 iPhone 13 Pro 模拟器(iOS 15)的 Mac M1(macOS Monterey 12.0.1)
运行 5 次不同的随机测量,在 20 个不同的视图中同时启动闪烁效果,滚动到视窗底部,然后滚动回顶部,最后关闭闪烁效果。
每行代表一个独立的测量过程,如下所示
{start}ms/{stop}ms, {滚动时的帧率}fps
Swift | TypeScript | Objective C | |
---|---|---|---|
1 | 0.537ms/0.252ms, 60fps | 0.987ms/0.469ms, 60fps | 1.241ms/0.314ms, 60fps |
2 | 1.863ms/0.648ms, 60fps | 1.311ms/0.145ms, 60fps | 0.963ms/0.380ms, 60fps |
3 | 2.070ms/0.557ms, 60fps | 0.741ms/0.780ms, 60fps | 1.652ms/1.019ms, 60fps |
4 | 0.626ms/0.189ms, 60fps | 2.081ms/0.247ms, 60fps | 0.702ms/0.436ms, 60fps |
5 | 0.589ms/0.211ms, 60fps | 0.645ms/0.488ms, 60fps | 0.874ms/0.343ms, 60fps |
平均值 | 1.137ms/0.371ms | 1.153ms/0.425ms | 1.086ms/0.498ms |
测试环境:运行 Pixel 4 模拟器(Android 12)的 Mac M1(macOS Monterey 12.0.1)
Kotlin | TypeScript | Java | |
---|---|---|---|
1 | 0.937ms/0.584ms, 60fps | 2.885ms/0.967ms, 60fps | 1.112ms/0.458ms, 60fps |
2 | 0.863ms/0.329ms, 60fps | 1.967ms/1.177ms, 60fps | 0.932ms/0.663ms, 60fps |
3 | 1.865ms/0.685ms, 60fps | 2.451ms/1.021ms, 60fps | 0.877ms/0.391ms, 60fps |
4 | 1.021ms/0.499ms, 60fps | 1.623ms/2.011ms, 60fps | 1.455ms/0.644ms, 60fps |
5 | 0.762ms/0.387ms, 60fps | 2.081ms/0.899ms, 60fps | 0.765ms/0.529ms, 60fps |
平均值 | 1.089ms/0.496ms | 2.201ms/1.215ms | 1.028ms/0.537ms |
我们正在测量毫秒级的小变化,如您所见,无论您选择使用 NativeScript 的哪种方式,性能都非常出色。在 Android 的情况下,当使用 Drawable 或自定义绘制进行大量的自定义视图覆盖时,您可以通过将自定义 Drawable 实现放入 Kotlin 或 Java 中来获得一些性能提升。这使得这是一个很好的测试,可以了解不同的情况以及何时以及为什么不同的方法很重要,帮助您在项目中做出最佳决策。
归根结底,运行应用程序,自己感受一下,如果需要,也可以进行测量,以实现任何特定目标。最重要的是,解决现实世界的问题,同时享受乐趣——这才是 NativeScript 的宗旨。
我为这个插件选择了哪种方式?
我决定坚持使用 iOS 上的 Shimmer.swift
和 Android 上的 Shimmer.java
,因为它易于维护,并且与其他方式一样有效。
npm run publish-packages
? Which packages 📦 would you like to publish?
› nativescript-shimmer
? What 🆕 version would you like to publish?
› 1.0.0
? Sanity check 🧠 Are you sure you want to publish? (y/N)
› y
然后您的包将发布到 npm。前提是您必须登录到一个有效的 npm 帐户,并且该帐户具有发布权限。