NativeScriptでは、JavaScriptコードをネイティブから呼び出すことができ、その逆も可能です。 これは、「他の世界」(ネイティブまたはJavaScript)に公開する必要のある各インスタンスに対応するブリッジを作成することによって行われます。 これらにより、開発者は(ネイティブインターフェイスを実装するか、JavaScriptのネイティブクラスから派生することにより)ネイティブインスタンスを作成/アクセスし、JavaScriptからメソッドを呼び出すことにより、JavaScriptからネイティブAPIにアクセスして利用できます。
この記事では、JavaScriptとネイティブインスタンスのライフサイクルについて説明し、 2つのガベージコレクションランタイム(Android)またはガベージコレクションランタイムと参照カウンタ(iOS)の複雑さから生じる可能性のある厄介な状態を示します。
免責事項:これらの用語は必ずしも文献で十分に確立されているわけではありませんが、次のセクションで便宜上それらを紹介しています。
ネイティブインスタンス - Objective-Cクラスインスタンス(iOS)またはJavaクラスインスタンス(Android)。
参照カウンタ - iOSのObjective-Cランタイムは、ライフタイム管理に参照カウンタを使用します。 インスタンスは、インクリメントおよびデクリメントできる内部カウンターを保持しています。 インスタンスを指すように強参照が設定されるたびに、インスタンスの参照カウンタが増分されます。 強参照が変更されるたびに、それが指し示した以前のインスタンスの参照カウンタが減らされます。 カウントが0に達すると、インスタンスの割り当てが解除されます。
GC - 一般的なガベージコレクションです。 GCが実行されると、最初にスレッドをブロックして、スタックからすべての強力なインスタンスを見つけます。 次に、GCがすべての到達可能なオブジェクトを別のスレッドでマークするまで実行を再開します。 次に、スレッドを再度ブロックして、マーキングを完了します。 そして最終的に、検出された到達不能インスタンスをファイナライズおよび割り当て解除します。 実際のGC実装ははるかに洗練されている場合がありますが、UIに使用される仮想マシンのすべての実装は、メインスレッドがブロックされる時間を最小限に抑えることを目的としています。 Android Java VM、AndroidのV8、iOSのJavaScriptCoreは、NativeScriptで使用されるガベージコレクターを備えた3つの最先端の仮想マシンです。
弱/強参照 - インスタンスは相互に参照できます。 ルートインスタンス(スタック上のローカル変数、静的フィールドなどによって保持されているインスタンス)から別のインスタンスへの強い参照のグラフにパスがある場合、2番目のインスタンスはガベージコレクションできません。 一方、弱い参照は、その指示対象のコレクションを妨げません。
スプライス - 新しいNativeScript用語を紹介しましょう。 ネイティブインスタンスとJavaScriptインスタンスによるJavaScriptの表現との間に作成された結合をスプライスと呼びます。 場合によっては、スプライスがネイティブで最初にインスタンス化される可能性があります(たとえば、iOS AppDelegateクラス、AndroidのApplication、Activity、およびFragmentクラス)。
スプライスには、JavaScriptインスタンスとネイティブインスタンスへの参照があります。
スプライスの動作:
スプライスが作成される条件:
Objective-CランタイムにはGCがなく、代わりに参照カウンタに依存しています。 各Objective-Cインスタンスの保持(retain)および解放(release)呼び出しは、iOSランタイムによって横取りされます。 Objective-Cには、ネイティブオブジェクトに動的にキーと値のペアを割り当てることができる関連付けAPIがあります。 JavaScriptCoreには、強力/弱体化(つまり、ガベージコレクションの許可または拒否)に使用できるJavaScriptインスタンスを保護するAPIがあります。 ここでの「スプライス」は、クラスのObjective-CインスタンスとJavaScriptインスタンスのリンクを指します。
スプライスが作成されると、Objective-Cインスタンスの参照カウンタが1増加し、参照カウンタが1より大きい場合、スプライスはJavaScriptインスタンスを強力にします。その時点から:
JavaScript GCはネイティブオブジェクトを通過せず、サイクルの検出に失敗するため、Objective-CでネイティブおよびJavaScriptインスタンスをリークする参照サイクルを作成できます。 ネイティブツール(Xcode、Instruments)を使用して、リークしているインスタンスを検出し、特定することができます。
setTarget:selector:...のようなメソッドを含む弱いプロパティまたはAPIを使用する場合、インスタンスを早期にコレクションさせることができます。 それらは、Objective-Cインスタンスをネイティブターゲットとして追加しますが、Objective-Cインスタンスの参照カウンタをインクリメントしない弱いObjective-C参照を使用します。 ターゲットの参照カウンタが1のままで、JavaScript GCがスプライスのJavaScriptインスタンスをコレクションすると、Objective-Cインスタンスの割り当ても解除されます。 面倒なことに、ほとんどの場合はコードは適切に機能しますが、GCの非決定的な完了により、上記の割り当て解除が発生し、プログラムが例外をスローしたりクラッシュしたりすることがあります。
スプライスの対応するJavaScriptが収集されると、ネイティブのObjective-Cインスタンスの割り当て解除がスケジュールされます。 ネイティブインスタンスがメッセージを投稿できる非常に短い時間枠があります(例えば、通常は弱い参照のプロパティに保持されているデリゲートでのメソッド呼び出し)。 これにより、既にコレクションされたJavaScriptインスタンスが呼び出されます。
全体的に、実装は本当にObjective-Cに対応しており、予測可能です。 ネイティブAPIを使用する場合は、メモリ管理に関する追加の注意が必要ですが、iOSの一般的な知識にすぎません。 UIに非常にフレンドリーで、メインUIスレッドに一時停止を導入しません。
Obj-CからJavaScriptに公開されたリンクリストを収集するために必要なGCサイクルの数は、リスト内のノードの数に基づいて線形です。
次のシナリオ(実際にはtns-core-modulesで解決された実際の問題)を取り上げます。
Page -> StackPanel -> Button
|.ios |.ios |.ios
UIViewController UIView UIButton
「Visible」の場合、UIViewController
にはUIViewを指すルートビュープロパティがあり、UIView
にはUIButton
への参照を保持するコレクションがあります。
それぞれにJavaScriptラッパーがあります。
ビジュアルツリーが表示されている間、Objective-CUIViewController
、UIView
、およびUIButton
の参照カウンタは2であり、JavaScript参照は「保護」されています(つまり、JavaScript GCはこれらのオブジェクトをルートと見なし、それらを収集しません)。
ページから「移動」すると、親UINavigationController
はUIViewController
を削除し、その参照カウンタを1に減らすため、JavaScriptラッパーを「保護しない」状態となり、ガベージコレクションの対象になります。
次に、次のGCがPage
を収集しますが、UIView
の参照カウンタは2のままであり、そのJavaScriptラッパーは保護されます。
そのツリー全体を収集するために必要なものは次のとおりです。
UIVIEWCONTROLLER | UIVIEW | UIBUTTON | |
---|---|---|---|
Visible | RC: 2, Protected | RC: 2, Protected | RC: 2, Protected |
Navigated Away | RC: 1, Unprotected | RC: 2, Protected | RC: 2, Protected |
GC Pass 1 | Collected | RC: 1, Unprotected | RC: 2, Protected |
GC Pass 2 | Collected | Collected | RC: 1, Unprotected |
GC Pass 3 | Collected | Collected | Collected |
すべてのオブジェクトを解放することによる複数のGCの要件を回避するために、ネイティブビューを分離するいくつかの追加のロジックが実装されています。
ビジュアルツリーからページを削除すると、UIView
からUIButton
が削除され、UIViewController
からUIView
が削除されます。
UIVIEWCONTROLLER | UIVIEW | UIBUTTON | |
---|---|---|---|
Visible | RC: 2, Protected | RC: 2, Protected | RC: 2, Protected |
Navigated Away | RC: 1, Unprotected | RC: 1, Unprotected | RC: 1, Unprotected |
GC Pass 1 | Collected | Collected | Collected |
Androidでは、Java VMとJavaScript VMの両方がGCベースです。 Android Java VMにはGCイベントをサブスクライブするための限定されたパブリックAPIがありますが、V8にはGCプロローグとエピローグをサブスクライブするためのより豊富なAPIがあり、JavaScriptインスタンスがコレクション用にマークされたときに通知をサブスクライブすることもできます。 外部からまだ参照されていることが判明した場合は、オプションで復活させます。
Androidスプライスには2つのフレーバーがあります。
スプライスが作成されるとき
V8 GC収集フェーズ:
その後、すべてのスプライスは次のケースに従って処理されます。
iOSとは異なり、Androidランタイム内ではJavaとJavaScriptの両方が管理されます。 ネイティブフレームワークでは、弱参照を使用することはめったにないため、早期のコレクションはほとんど観察されません。 GC for Androidの最も一般的な問題は、half-deadスプライスです。
メモリリークはまれです。 JavaまたはJavaScriptから到達不能なスプライスのプールがある場合、ある時点でV8 GCがJavaScriptインスタンスにそれらがコレクション用にマークされていることを通知し、対応するJavaインスタンスへの参照が弱くなります。 次に、次のAndroid VM GCがJavaインスタンスを収集し、その後のV8 GCがJavaScriptインスタンスを収集します(対応するJavaインスタンスが停止しているため)。
コレクションはガベージコレクターによって駆動されるため、スプライスのJavaScriptインスタンスへの弱い参照を保持することが可能です。 V8 GC後、スプライスはJavaインスタンスへの参照を弱くして、Android VM GCがそれを収集できるようにします。 次に、次のV8 GCの前にJavaScriptインスタンスが弱い参照から取得され、そのメソッドが呼び出されると、(対応するJavaインスタンスはすでに停止しているので)half-deadスプライスにアクセスすることになります。 ランタイムによって報告されたエラーは、指定されたIDのオブジェクトを見つけることができなかったことを指摘しています。 これらの問題はランダムに発生し、再現が非常に困難です。
1つのJavaオブジェクトに対して複数のスプライスとJavaScriptインスタンスを作成できますが、プロパティが失われる可能性があります。
実装オブジェクトのないスプライスは、JavaScriptインスタンスを簡単に収集できます。次の実行シーケンスを検討してください。
その結果、新しいインスタンスは対応するネイティブオブジェクトのJavaプロパティのみを取得できるため、最初のJavaScriptオブジェクトに割り当てられたプロパティは失われます。
短命のビッグオブジェクトを操作すると、メモリ不足のクラッシュを簡単に引き起こす可能性があります。 Androidスプライスのライフサイクルにより、大きなネイティブインスタンス(ビットマップなど)を破棄するには、V8 GCと後続のAndroid VM GCが必要です。
全体的に、実装は本当にJavaフレンドリーです。ランタイムの内部動作に関する追加の知識はほとんど必要ありません。
重要な順に対処する必要がある問題の一部を次に示します。
markingMode: "none"
オプションを導入しました。
詳細については、ドキュメントの記事を参照してください。ランタイムのオブジェクトの内部メモリ管理により、大きなネイティブオブジェクトが必要以上に長く存続する場合があります。 これは、オブジェクトがGCに適格になった後、JSガベージコレクターが長時間実行されない場合に発生する可能性があります。 その結果、このオブジェクトの強力な参照はネイティブ側に残ります。
この問題を解決する1つの方法は、複数のガベージコレクションをトリガーすることです(JS/TSおよびネイティブ側(Androidで実行している場合))。 ただし、これは平易な操作ではありません。 手動でガベージコレクションをトリガーすると、時間がかかるだけでなく、通常のガベージ管理が混乱する可能性があります。
この問題を解決するもう1つの方法releaseNativeCounterpart
は、ネイティブクラスのインスタンスを引数として受け取り、ランタイムでその強力な参照を削除する関数を使用することです。
これを行うことにより、Androidのネイティブガベージコレクターは、停止していると判断した場合、次回の実行時に重い可能性のあるネイティブオブジェクトを削除できます。
iOSでは、この関数を使用するガベージコレクターがないため、ネイティブオブジェクトの参照カウンタが1つ減り、このオブジェクトの他の使用法がない場合は削除されます。
releaseNativeCounterpart関数を使用した後、JS/TSでネイティブオブジェクトを使用しようとした場合、動作は未定義なので、オブジェクトが再び使用されないことが確実な場合はこの関数を使用します。
JS/TS でのreleaseNativeCounterpart
関数の使用例:
const heavyNativeObject = new com.native.HeavyObject();
releaseNativeCounterpart(heavyNativeObject); // all usages of heavyNativeObject after this line would have undefi