返回博客首页
← 所有文章

markingMode:none 已正式发布 - 提升 Android 性能并避免内存问题

2019 年 2 月 28 日 — 作者:Teodor Bozhikov

现状

我们都知道,在内存管理方面,NativeScript 一直在其 Android 运行时使用一个特殊的例程:MarkReachableObjects。它的主要目的是确保只要 JavaScript 表示(在 V8 中)需要 Java 对象,这些对象就不会被垃圾回收器收集,反之亦然。然而,虽然这种机制保证了稳定性,但它也有代价 - 在某些情况下,V8 GC 通道的执行时间可能长达一秒,这会导致阻塞 UI 线程并影响应用程序的整体性能。

一些历史

介绍 markingMode: none

NativeScript 3.2 开始,一种新的且当时实验性的垃圾回收模式被添加到 Android 运行时 - markingMode: none 标志。它的作用是关闭前面提到的例程,并在 Android 运行时使用另一种内存管理机制。结果是应用程序的性能有了很大的提升。缺点?是的,虽然可以避免,但在运行时可能会出现一些不可预测的错误/崩溃,这是由于过早地收集了 Java/Javascript 对象。

使所有核心插件与 markingMode: none 兼容

为了确保使用此模式正确执行代码,需要以一种方式编写代码,即永远不会在 JavaScript 对应对象仍然存在的情况下释放 Java 对象,反之亦然。实际上,tns-core-modules 从一开始就考虑到了 markingMode: none,并且不应该成为应用程序因内存问题而崩溃的原因(当然,可能存在 bug,因此请在 NativeScript Android 运行时 或可疑插件的仓库中记录问题)。

后来,随着 NativeScript 5.1 的发布,{N} 团队宣布所有核心插件(由 NativeScript 团队提供的插件)也支持 markingMode:none

最近,我们对 NativeScript 市场 上一些最流行的插件进行了测试,使用了它们的演示应用程序(并进行了一些调整以提高失败的可能性)。以下是一些插件的列表:

  • nativescript-cardview
  • nativescript-pager
  • NativeScript-Drop-Down
  • NativeScript-Grid-View
  • nativescript-bottom-navigation
  • nativescript-carousel
  • nativescript-mapbox
  • nativescript-photo-editor
  • nativescript-socketio

……并且发现使用 markingMode:none 运行它们时没有问题。如果您发现任何问题,我们会很乐意协助您。

正式支持 markingMode: none 以及未来计划

markingMode: none 已经存在了一段时间,并且已经达到了稳定状态,我们宣布markingMode: none 现在正式得到团队支持! 您可以随时报告使用此模式时出现的任何问题。NativeScript 团队将尽最大努力解决这些问题。

关于未来的计划,我们将:

  • 将所有 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()}`;
            }
        }
    ));
}

此处理程序的作用是:

  1. 创建本机 Android android.widget.Button 并将其添加到页面。
  2. 实例化本机 Android java.io.File
  3. OnClickListener 接口实现设置到按钮,并在内部调用 java.io.File 实例上的 file.getName()

就 TypeScript 语法和逻辑而言,这一切看起来都很好,并且在没有启用 markingMode: none 的情况下,它确实表现如预期的那样。但是,让我们设置标志(在 app/package.json 中):

"android": {
"markingMode": "none",
}

……并运行应用程序。然后:

  1. 单击 ADD BUTTON WITH CLICK LISTENER 按钮 - onAddClickListener 被调用,并且会按预期添加一个额外的按钮。
  2. 间歇性地单击生成的按钮……在一段时间内(1-3 分钟内),应用程序会崩溃并出现以下错误之一:
    • 错误:com.tns.NativeScriptException: Attempt to use cleared object reference id=<some-object-id-number>
    • JavaScript 实例不再具有可用的 Java 实例对应部分。.

marking mode none

因此,如果我们回顾 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 文档

资源