我们都知道,在内存管理方面,NativeScript 一直在其 Android 运行时使用一个特殊的例程:MarkReachableObjects
。它的主要目的是确保只要 JavaScript 表示(在 V8 中)需要 Java 对象,这些对象就不会被垃圾回收器收集,反之亦然。然而,虽然这种机制保证了稳定性,但它也有代价 - 在某些情况下,V8 GC 通道的执行时间可能长达一秒,这会导致阻塞 UI 线程并影响应用程序的整体性能。
从 NativeScript 3.2 开始,一种新的且当时实验性的垃圾回收模式被添加到 Android 运行时 - markingMode: none
标志。它的作用是关闭前面提到的例程,并在 Android 运行时使用另一种内存管理机制。结果是应用程序的性能有了很大的提升。缺点?是的,虽然可以避免,但在运行时可能会出现一些不可预测的错误/崩溃,这是由于过早地收集了 Java/Javascript 对象。
为了确保使用此模式正确执行代码,需要以一种方式编写代码,即永远不会在 JavaScript 对应对象仍然存在的情况下释放 Java 对象,反之亦然。实际上,tns-core-modules
从一开始就考虑到了 markingMode: none
,并且不应该成为应用程序因内存问题而崩溃的原因(当然,可能存在 bug,因此请在 NativeScript Android 运行时 或可疑插件的仓库中记录问题)。
后来,随着 NativeScript 5.1 的发布,{N} 团队宣布所有核心插件(由 NativeScript 团队提供的插件)也支持 markingMode:none
。
最近,我们对 NativeScript 市场 上一些最流行的插件进行了测试,使用了它们的演示应用程序(并进行了一些调整以提高失败的可能性)。以下是一些插件的列表:
……并且发现使用 markingMode:none
运行它们时没有问题。如果您发现任何问题,我们会很乐意协助您。
markingMode: none
已经存在了一段时间,并且已经达到了稳定状态,我们宣布markingMode: none
现在正式得到团队支持! 您可以随时报告使用此模式时出现的任何问题。NativeScript 团队将尽最大努力解决这些问题。
关于未来的计划,我们将:
markingMode: none
的应用程序。markingMode: none
选项将成为默认模式。为什么我们今天在使用 markingMode: none
时应该谨慎?让我们深入了解一个例子。
考虑一个具有以下布局的 NativeScript 页面:
<StackLayout id="root">
<Label class="t-20" text="{{ fileName }}"></Label>
<Button text="add button with click listener" tap="{{ onAddClickListener }}"></Button>
</StackLayout>
如您所见,此页面绑定到其 bindingContext
的多个成员。让我们关注 onAddClickListener
事件处理程序。
public onAddClickListener() {
let root = <StackLayout>currentPage.getViewById('root');
let btn = new android.widget.Button(root._context);
btn.setText("ta-daa, now click!");
root.android.addView(btn);
let file = new java.io.File('real file'); // create Android native instance of a File
// create native click listener implementation
btn.setOnClickListener(new android.view.View.OnClickListener(
{
onClick: () => {
// call some method on the Android native instance
this.fileName = `${file.getName()} exists at ${new Date().toTimeString()}`;
}
}
));
}
此处理程序的作用是:
android.widget.Button
并将其添加到页面。java.io.File
。OnClickListener
接口实现设置到按钮,并在内部调用 java.io.File
实例上的 file.getName()
。就 TypeScript 语法和逻辑而言,这一切看起来都很好,并且在没有启用 markingMode: none
的情况下,它确实表现如预期的那样。但是,让我们设置标志(在 app/package.json
中):
"android": {
"markingMode": "none",
}
……并运行应用程序。然后:
ADD BUTTON WITH CLICK LISTENER
按钮 - onAddClickListener
被调用,并且会按预期添加一个额外的按钮。错误:com.tns.NativeScriptException: Attempt to use cleared object reference id=<some-object-id-number>
JavaScript 实例不再具有可用的 Java 实例对应部分。
.因此,如果我们回顾 onAddClickListener
方法,java.io.File
实例被包含在本机按钮的 onClick
回调实现中,但是在启用 markingMode: none
的情况下,框架不再负责查找这种连接。当 GC 在 V8(JavaScript)或 Android(Java)中发生时,java.io.File
实例(或其本机表示)会被 GC 收集。这会导致 Java 或 JavaScript 实例丢失。因此,在调用 onClick
并试图使用已经被收集的对象时,应用程序会崩溃并出现任何一个错误。
从逻辑上讲,我们应该确保 java.io.File
实例在应用程序执行期间始终存在(是 JavaScript 或 Java 表示被收集并导致崩溃,对吧?)。在我们的例子中,我们需要它只要页面仍然存在,因为我们不希望在页面消失后处理点击事件 😀。因此,在我们的例子中,将实例存储在页面绑定的 ViewModel
的属性中就足够了:
export class ViewModel extends Observable {
...
private myFile: java.io.File;
...
……并在回调实现中使用它,如下所示:
btn.setOnClickListener(new android.view.View.OnClickListener( { onClick: () => { this.fileName =${this.myFile.getName()} exists at ${new Date().toTimeString()}
; } } ));
这将确保除非包含它的对象(ViewModel
实例)被收集,否则 GC 不会收集 java.io.File
实例。
注意:因为在实际应用程序中,这种错误可能以不可预测的方式出现,所以测试应用程序是否有问题的一个方便方法是使用 adb 的“monkey”来模拟随机点击和手势。有关详细信息,请阅读
markingMode: none
文档。
markingMode: none
和 {N} 中的 Android 内存管理 的文档