使用 NativeScript、React Native、Flutter 或 Ionic 等跨平台框架,可以帮你避免编写 Java for Android 和 Objective-C for iOS 等原生代码,从而省去很多麻烦。这些框架抽象了许多原生元素,并为你提供了更清晰的接口和函数来使用。
不过,有时框架的能力还是会不够用。你的应用可能需要处理一些非常特殊的需求,出于性能原因需要在原生层面处理,或者单纯是因为只靠框架无法实现。
在本文中,我们将探讨 NativeScript 如何让你能够轻松扩展任何原生 Java 类,主要介绍以下两种方法。
方法一:NativeScript 方式,使用 TS/JS 绑定
NativeScript 最独特的功能之一是原生类型编组(marshalling),这意味着大多数原生类和类型可以直接从你的 TypeScript/JavaScript 代码库中访问。NativeScript 自动处理 JavaScript 环境和原生环境之间的数据类型转换。
这一特性在扩展原生类时起着至关重要的作用,无需处理复杂的原生 Java 代码。
你可以在 NativeScript 官方文档中找到关于编组和扩展 Android 的更多信息,其中包含有关 JS 和原生之间如何映射原生数据的非常有用的信息:
教我怎么做
通过 .extend() 函数
如果你喜欢函数式风格,可以调用任何原生类的 extend() 方法来从中创建一个子类。在 Java 中,类是按照被称为包名的目录组织的。在 NativeScript 中,一个原生 Java 类可以通过它的完整包名和类名来引用。
以下是一个扩展代码示例:
typescript
(<any>androidx.appcompat.app.AppCompatActivity).extend('com.newbiescripter.MainActivity', {
onCreate(savedInstanceState: android.os.Bundle): void {
this.super.onCreate(savedInstanceState)
},
onNewIntent(intent: android.content.Intent): void {
// ..
},
onSaveInstanceState(outState: android.os.Bundle): void {
// ..
},
onStart(): void {
// ..
},
onStop(): void {
// ..
},
// 如果需要,重写任何其他方法
// ...
});
如果你不确定包名,可以随时在 Android 开发者参考文档中查看。例如,这是 Activity 类的文档,查看它的类层次结构就能看到完整包名。

通过 @NativeClass 和 @JavaProxy 装饰器
如果你喜欢更干净、更自然的类语法,你可能会想看看 NativeScript 提供的装饰器:@NativeClass 和 @JavaProxy。
下面是执行与前面示例完全相同操作的示例代码:
typescript
@NativeClass
@JavaProxy('com.newbiescripter.MainActivity')
class MainActivity extends (<any>androidx.appcompat.app.AppCompatActivity) {
onCreate(savedInstanceState: android.os.Bundle): void {
this.super.onCreate(savedInstanceState)
}
onNewIntent(intent: android.content.Intent): void {
}
onSaveInstanceState(outState: android.os.Bundle): void {
}
onStart(): void {
}
onStop(): void {
}
// 如果需要,重写任何其他方法
// ...
};
@JavaProxy 让你有机会在 Java 世界中为你的类命名,以便你可以在代码库的其他部分引用它。虽然你可以完全省略 @JavaProxy,这样你的类就会变成匿名的,但我更喜欢给自己的类命名。
这两种方法的结果是相同的;它们的区别只在于编写风格,因此你可以根据自己的喜好进行选择。最终,代码将在构建时被编译成 Java 并存储在 platforms/android 文件夹中。
NativeScript 扩展类被转换为 Java 代码。
一个有趣的地方是,在生成的 Java 类中你看不到你的 TypeScript/JavaScript 代码的实现。它实际上只包含一小段处理传入参数的 Java 代码,然后将它们发送回 JavaScript 世界,最终调用你的 JavaScript 方法。这很酷吧!
有什么需要注意的吗?
如果你做错了什么,幸运的话,你会在构建时看到编译器抛出错误。但有时它会一直静默无闻,直到运行时出现意外错误。以下是一些在调试时常让我抓耳挠腮的常见问题:
- 必须是 Android 的原生类 :例如,
androidx.appcompat.app.AppCompatActivity可以使用。或者是通过app.gradle安装的包里的类。确保这个类存在。 - 避免扩展已经被扩展过的类 :尝试扩展一个已经使用上述方法扩展过的类将不起作用。尝试扩展编译时类,如 NativeScript 生成的 Activity 类:
com.tns.NativeScriptActivity将不起作用。 - 必须被包含才能生成 :包含扩展原生类代码的
.ts/.js文件,必须直接包含在 webpack 的appComponents参数中,或者通过被包含在appComponents中的文件间接导入这个.ts/.js文件。(请注意,默认情况下app.ts/js已被包含)。如果该文件未被任何其他.ts文件导入,则会被视为未使用,在构建时不会被生成。 - 必须执行一次
ns build android:这是生成原生.java类文件及其绑定所必需的。否则,不会生成.java文件,并且在运行时会抛出ClassNotFoundException错误。注意:单独执行ns run android并不总能触发此操作。 - 90% 的问题在于原生
.java类文件未生成 :如果你不确定,可以导航到platforms/android/app/src/main/java文件夹并查找你的自定义类。在 Java 中,类是按其包名组织的,因此com.newbiescripter.CustomClass将位于app/src/main/java/com/newbiescripter/CustomClass.java... 这也是我建议给你的类命名并避免使用匿名类的原因,这样在platforms文件夹中查找它会更容易。
方法二:原生对原生,原生 Java 方式
如果你是一位硬核 Java 开发者,更喜欢在较低级别进行操作,这对你来说可能更直接。从技术上讲,放在你项目 App_Resources/Android/src 里面的任何东西,在后面都会被复制到 platforms/android/app/src 并编译进最终的应用程序包中。
所以,我们可以利用这一点来实际编写 Java 代码,并在其中扩展其他类。这是一个快速的 Java 代码示例:
java
package com.newbiescripter;
import android.content.Context;
import android.widget.Button;
public class CustomButton extends Button {
public CustomButton(Context context) {
super(context);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
}
}
我们将此文件放置在 App_Resources/Android/src/main/java/com/newbiescripter/CustomButton.java(虽然路径有点深......)。在构建时,你会看到它被复制到 platforms/android 文件夹,如下所示:
这种方法的优点是,你的类是 100% 原生的,没有编组(marshalling)的过程,如果代码计算密集,这可能会带来更好的性能。
小结
总之,你可以在这两种方法中选择一种,找到更适合你和你的团队的方法。就我个人而言,我更喜欢方法二,因为它让我能舒适地待在 JavaScript 这边,这也充分发挥了 NativeScript 的真正威力。