[バックエンドチュートリアル]フラッターリストのメモリ再利用を調整し、大規模セルの問題を解決する

序文


ビッグセルの問題とは何ですか?ネイティブリストに基づくレンダリングスキームでは、大きなセルの問題が発生します。たとえば、Weexビジネスではページメモリが急増することがよくありますが、調査の結果、フロントエンドの書き込みが原因で大きなセルに画像が多すぎて、メモリが過剰になっていることがわかりました。この問題はFlutterにも存在します。本質的な理由は、Listがリサイクルする単位がCellであり、Cellの画像ではないためです。ブラウザシステムではこの問題は発生しませんが、ブラウザは追加の計算を実行し、画面上の画像を正しく復元できると考えられます。
Flutterバージョンの淘宝網の製品詳細ページを開発しているときに、大きなセルの問題も発生しました。製品の詳細は複数の画像で構成されています。これらの画像のサイズは不明であり、高度な適応性が必要です。画像は同じセルに配置されます。リストが特定の位置までスクロールし、多数の画像が読み込まれ、同時にテクスチャが生成され、メモリが突然急上昇したことがわかりました。

この問題には2つの解決策があります。

  1. ビジネスレイヤーコードを再構築し、複数のセルに画像を分散させます。ただし、高レベルの情報がないため、Cellは一度にすべて表示され、メモリの問題を引き起こします。

  2. フラッターリストのリサイクル能力を改善するセルのリサイクルに基づいて、画像単位でリサイクルできます。

オプション1は、症状が永続的ではなく、コストが高いと言えるだけです。ウィークスの経験によると、ビジネス開発の学生は、不注意による実際の大きなセルの存在により、必然的にオンラインメモリの問題を引き起こします。スキーム2は、この記事で取り上げる方法であり、画像のリサイクル能力を高め、Flutterシステムのメモリ使用量を削減します。

ソリューション探索プロセス


pictures絵を描くための座標情報

Flutterでは、画像の描画はDartレイヤーのRenderImage.paintメソッドに呼び出されます。ログで、描画時に、オフセットパラメータの値は、ページの左上隅に対する画像の距離として概算できることがわかりました。(リストが全画面ではない、TabBarが配置されているなど、ページ階層がより複雑な場合、オフセット値は不正確になる可能性があります。)

画像

`2020-02-06 Runner [45049:2962074] flutter:[AA] Render offset:Offset(0.0、74.4)` `2020-02-06 Runner [45049:2962074] flutter:[AA] Render offset:Offset(0.0 、449.4) `` 2020-02-06 Runner [45049:2962074] flutter:[AA] Render offset:Offset(0.0、824.4) `` 2020-02-06 Runner [45049:2962074] flutter:[AA] Render offset :Offset(0.0、1199.4) `` 2020-02-06 Runner [45049:2962074] flutter:[AA] Render offset:Offset(0.0、1574.4) `` .... `

the画像が画面上にあるかどうかを決定するための座標による

座標情報を使用すると、画像が画面上にあるかどうかを判断する大まかな方法​​があります。実際のコードでは、以下の方法で判断しています。このメソッドは、それが画面上にあるかどうかだけを判断できます。リストからスライドしたり、NavigationBarや他のシーンによってカバーされているかどうかは判断できません。

`void paint(PaintingContext context、Offset offset){` `// Rect(offset&size)が画面の境界と交差するかどうかを確認します。``最終的なdouble screenWidth = ui.window.physicalSize.width / ui.window.devicePixelRatio; `` final double screenHeight = ui.window.physicalSize.height / ui.window.devicePixelRatio; `` if(offset.dy> = screenHeight-1 || offset.dy <= -size.height + 1 || `` offset.dx > = screenWidth-1 || offset.dx <= -size.width + 1){`` //在屏幕外 ``} `` .... ``} `

▐フレームごとにセルを強制的に再描画します

Loggingは、セルが長い場合でも、Flutterは一度しか描画せず、大きなテクスチャを生成することがわかりました。その後、スクロールプロセス中にRenderImage.paintは呼び出されません。調査コードにより、sliver.dartファイルでは、各セルが強制的にRepaintBoundaryでラップされることがわかりました。また、addRepaintBoundariesパラメータはデフォルトでtrueです。Flutterコードのコメントによると、CellがRepaintBoundaryに追加され、スクロールのパフォーマンスが向上しています。

`//クラスSliverChildBuilderDelegate`` ///各子を[RepaintBoundary]でラップするかどうか.`` ///` `///通常、スクロールコンテナー内の子は、再描画` `///境界でラップされますリストがスクロールするときにそれらを再描画する必要はありません。」///子が簡単に再描画できる場合(たとえば、無地のブロックまたは短い「///テキストのスニペット」)は、再描画境界を追加します `` ///そして、スクロール中に子を単に再描画します `` /// `` ///デフォルトはtrueです。

ここでは、特定のCellのRepaintBoundary関数をシールドし、空の純粋な仮想クラスNoRepaintBoundaryHintを追加します。

`///セルコンテンツの再描画境界を作成しないようにスライバーに指示するウィジェットです。``抽象クラスNoRepaintBoundaryHint {` `}`

また、SliverChildBuilderDelegateクラスとSliverChildListDelegateクラスのビルドメソッドを変更します。子がNoRepaintBoundaryHintから継承する場合、RepaintBoundaryを追加しないでください。

`if(addRepaintBoundaries &&(child is!NoRepaintBoundaryHint)){` `child = RepaintBoundary(child:child);` `}`

このように、カスタムウィジェットは、ビジネスレイヤーがこのソリューションを変更する必要がある唯一の場所であるNoRepaintBoundaryHintインターフェースを実装するふりをする必要があるだけです。

`Class MyListItem extends StatefulWidget implements NoRepaintBoundaryHint {` `}`

notification画像を読み込んでリサイクルするための通知を追加する

_ImageStateクラスの場合、RawImageコンポーネントを作成し、RawImageはRenderImageを作成します。このリンクにコールバックメソッドを追加し、新しいサブクラスAutoreleaseRawImageおよびAutoreleaseRenderImageを作成します。

`///画像の描画時に、AutoreleaseRenderImageは画面イベントの内側または外側に移動する画像を所有者に通知します。``typedefSetNeedsImageCallback = void Function(bool value);`

画面が出たら、SetNeedsImageCallback(false)を呼び出し、それぞれが保持するui.Imageをnullに設定して、テクスチャを解放します。
画面に入るときに、SetNeedsImageCallback(true)を呼び出して、画像を再度要求します。コードはおおよそ次のようになります(一部は省略):

`// Class _ImageState``void didChangeDependencies(){` `_updateInvertColors();` `if(_releaseImageWhenOutsideScreen){` `return; //マークがある場合、イメージをロードしなくなり、描画命令を待つ` `}` ` ..リクエスト画像 `` super.didChangeDependencies(); ``) `` void __setNeedsImage(bool value)( `` if(value){`` if(_imageStream == null)( `` Request Image``) `` } `` else {``空の画像 ``} ``) `` void _setNeedsImage(bool value){// AutoreleaseRenderImage Callback this method`` Future((){`` __setNeedsImage(value); //ペイントプロセス中、 SetStateは許可されていないため、非同期にする必要があります ``}); ``} `

▐デモ試運転

デモでは、10個のセルごとに大きなセルを追加します。大きなセルには10枚の画像があります。コードは次のとおりです。

`Widget build(BuildContext context){` `if(widget.index%10 == 0){` `final images = [];` `for(var i = 0; i <10; i ++){` `images。 add(new Image.external_adapter( `` 'https://i.picsum.photos/id/' +(widget.index + i).toString()+ '/ 1000 / 1000.jpg'、 ``高さ:375 、 `` width:375、 ``)); ``} `` return Column( `` children:images` `);` `}` `else {` `return Container(` `width:375、` `高さ:375、 ``子:Text(widget.index.toString())、 ``); ``} ``} `

デモの効果は非常に優れています。元の画像までスクロールすると、一度に10枚の画像が読み込まれます。変更後、10枚の画像が同じセルに配置されていても、1枚ずつ読み込まれ、リサイクルされます。図に示すように、最下層にテクスチャの数を印刷し、メモリ使用量を観察します。

画像

▐実際のビジネスシナリオテスト

ただし、商品のディテールの実際のシーンでは、画像をまったく読み込むことができません。デバッグでは、デモで各画像の幅と高さを指定し、画像を通常どおりタイプセットできることがわかりました。ビジネスシナリオでは、HTMLの解析によって生成された画像コンポーネントに幅と高さの情報が不足しています。RenderImageが画像サイズ情報とタイプセットを取得するには、画像が実際に読み込まれるまで待つ必要があります。

`//クラスRenderImage``Size _sizeForConstraints(BoxConstraints制約)(` `constraints = BoxConstraints.tightFor(` `width:_width、// is null`` height:_height、// is null``).enforce(constraints) ; `` if(_image == null) `` return constraint.smallest; //画像が読み込まれていない場合、ウィジェットにはまったくサイズがありません `` return constraint.constrainSizeAndAttemptToPreserveAspectRatio(Size( `` _image.width.toDouble()/ _scale、 `` _image.height.toDouble()/ _scale、 ``)); ``) `

ここではパラドックスに陥るようです:

  • 画像は存在せず、タイプセットも表示もできません。

  • 画像を読み込むと、画面外にあるはずのすべての画像テクスチャがGPUにアップロードされます。その後、組版を完了できます。もう一度描画すると、画面外にあることがわかり、テクスチャを削除します。

このプロセスに従う場合、タイプセットする前にイメージをロードする必要があり、最適化の効果が大幅に低下します。実際、組版では画像のサイズのみが必要で、GPUテクスチャは必要ありません。ここで最適化の余地があります。

advance事前に画像サイズを取得する

AliFlutterの画像スキームでは、カスタムのExternalAdapterImageFrameCodecが実装され、それが提供するgetNextFrameインターフェイスを使用して画像が取得され、テクスチャがアップロードされた後に、使用可能なui.Imageが返されます。画像サイズを事前に取得するために、getImageInfoインターフェースを追加します。インターフェイスが画像ライブラリ(UIImageなど)から画像を取得した後は、基本情報のみを受け取り、テクスチャをアップロードしません。_ImageStateで、ウィジェットの幅と高さが指定されているかどうかを確認します。パラメータのいずれかが指定されていない場合、画像が要求されたときにパラメータが要求され、画像の基本情報のみが取得され、テクスチャはアップロードされません。

`// Class _ImageState``void didChangeDependencies(){` `if(_releaseImageWhenOutsideScreen){` `if(widget.width == null || widget.height == null){` `_resolveImage(true); //取得のみ画像サイズ、テクスチャをアップロードしない '' _listenToStream(); ``} ``} `` ....以下は ``)と省略されます '' void _handleImageInfo(int width、int height、int frameCount、int durationInMs、int iterationCount){ `` setState((){//画像サイズを取得した後、それを記録し、RenderObjectに更新します `` _imageWidth = width; `` _imageHeight = height; ``}); ``} `

その中で_resolveImage(true); ExternalAdapterImageStreamCompleterにgetNextFrameインターフェースの代わりにgetImageInfoを呼び出すように指示します。画像サイズを取得したら、それを記録し、setStateを介してAutoreleaseRenderImageに通知します。AutoreleaseRenderImageメソッドの_sizeForConstraintsメソッドを書き換えて、画像テクスチャは存在しないが画像のサイズは既知であり、スムーズなレイアウトを保証するシーンを処理します。ここでは、幅と高さを取得するために、引き続き_imageを使用することをお勧めします。_imageが空の場合、レイアウトを計算するには、上位レイヤーで指定された_imageWidthと_imageHeightを使用します。

`Size _sizeForConstraints(BoxConstraints constraint){` `constraints = BoxConstraints.tightFor(` `width:_width、` `height:_height、` `).enforce(constraints);` `//画像自体または画像のピクセル寸法に固有info.` `if(_image == null &&(_imageWidth == null || _imageHeight == null))` `return constraint.smallest;` `//ヌルでない場合に_imageを使用する` `if(_image!= null){ `` return constraint.constrainSizeAndAttemptToPreserveAspectRatio(Size( `` _image.width.toDouble()/ _scale、 `` _image.height.toDouble()/ _scale、 ``)); ``} `` //または、画像の次元を使用info.` `return constraint.constrainSizeAndAttemptToPreserveAspectRatio(Size(` `_imageWidth.toDouble()、` `_imageHeight.toDouble()、` `));` `}`

▐さらなる最適化

getImageInfoインターフェイスをExternalAdapterImageFrameCodecに追加することで、オフスクリーンテクスチャのアップロードを回避できます。ただし、画像には高レベルの情報が不足しているため、ページに入ると、画像はスタックされたままになり、画像のリクエストが大量に発生します。これらの画像は、外部画像ライブラリを介してUIImage(またはAndroidビットマップ)オブジェクトを返すように要求します。テクスチャとしてアップロードされていなくても、メモリオーバーヘッドが大きくなります。商品詳細事業の特徴は、複数の写真をつなぎ合わせることであり、写真の横幅しか指定できず、適応性の高い写真が必要です。したがって、この種のシーンでは、組版用の仮想サイズパラメータをFlutterの公式画像コンポーネントに追加しました。

image.gif

詳細なビジネス特性によると、画像ウィジェットの幅はページ幅として指定され、仮想高さは画像幅と同じです。ImageWidgetStateのビルドメソッドでは、基になるRenderObjectを作成するときに、仮想サイズが基になるRenderObjectに渡されるため、画像はおおよそのタイプセット位置を取得します。画面全体のレイアウト読み込みロジックは次のとおりです。

  1. 画像ウィジェットに特定の幅と高さがある場合、画像をロードするかどうかは画面上の描画ステージの判断に依存します。

  2. 画像ウィジェットに幅と高さの情報がない場合、組版用の仮想サイズがあると、この仮想サイズで事前組版が実行されます。組版後初めて描画するとき、画面上にあれば、実際に画像が読み込まれます。イメージがロードされた後、サイズが仮想サイズと一致しない場合は、再フォーマットされます。

▐効果

最適化後も、グラフィックの詳細は大きなセルであり、適応性の高い一連の製品画像がリストされています。私たちのソリューションは、セルが最初に表示されたときにすべての画像が一度に読み込まれ、メモリが突然上昇してOOMが発生するという事実を回避します。同時に、リストのスクロールプロセス中に、同じセル内の画像を必要に応じて復元し、メモリの水位を適切なレベルに保つことができます。

画像

まとめ


この記事で説明するソリューションは、AliFlutterが提供する外部画像ライブラリの機能の1つです。このソリューションは、このシナリオでの淘宝網の商品画像の詳細の安定性を保証します。私たちのテストでは、公式のImage.networkを使用して画像をロードし、大規模セルのシナリオを最適化しないと、より複雑な商品メモリが1GBに急増し、ほぼ100%がローエンドマシンのOOMを引き起こすことがわかりました。この場合、ビジネスは完全に利用できません。

サービスの推奨

公開元の記事0件 ・いい ね0件 訪問数330

おすすめ

転載: blog.csdn.net/weixin_47143210/article/details/105682970