Flutterにおける滑らかなユーザーエクスペリエンスの確保: バックグラウンド処理のためのIsolatesの活用

投稿者: | 2023年10月9日

Flutterにおける滑らかなユーザーエクスペリエンスの確保: バックグラウンド処理のためのIsolatesの活用


GoogleのUIツールキットであるFlutterは、その単一のコードベースからモバイル、ウェブ、デスクトップ向けにビジュアルに魅力的なネイティブコンパイルアプリケーションを作成する能力で賞賛を受けている。Flutterのコア部分は、単一スレッドモデルで動作しており、すべてのタスクをメインのUIスレッドで実行する。このモデルは、タスク管理とステートハンドリングを単純化する一方で、ジャンクや滑らかなユーザー体験を保証するためにリソースを集中的に使用するタスクの慎重なハンドリングが求められる。


1. Flutterの単一スレッドモデル

FlutterのUIレンダリングとほとんどの操作は、メインまたはUIスレッドとして知られる単一のスレッドで実行される。この最適な状況下でジャンクフリーなUIの更新を保証しているが、JSONの大量のレスポンスを解析するなど、重たい計算タスクやI/Oバウンドワークをこのスレッドで直接実行することは、UIのジャンクまたはユーザーインターフェースで目に見えるぎこちなさとして現れるフレームの描画遅延をもたらす可能性がある。


2. FlutterへのIsolatesの導入

UIの滑らかさを損なうことなく、重いタスクを実行するメカニズムとして、FlutterはIsolatesというコンセプトを導入している。Isolateは、メインのUIスレッドから独立して動作する別の実行スレッドであり、自身のメモリヒープとイベントループを持っており、タスクを並行して実行できる。

  • Isolateの生成: Isolateを作成するために、開発者は通常、Isolate.spawn()関数を使用し、Isolateで実行される関数とデータ交換のための通信ポートを渡す。
  • コミュニケーション: Isolateはそれぞれが自身のメモリヒープを持っているため、メモリ空間を共有しない。Isolate間のコミュニケーションは、メッセージ(データとエラー)の交換を可能にするSendPortReceivePortを通じて達成される。
Isolate.spawn(myIsolateFunction, receivePort.sendPort);

3. Isolatesの実践的な実装

実際には、IsolatesはUIスレッドがレンダリングとユーザーとのインタラクションに専念できるように、重い計算タスクやI/Oバウンドの操作をUIスレッドからオフロードするメカニズムを提供する。Flutter cookbookには、UIが応答性を保つためにIsolateを使用してバックグラウンドで大規模なデータセットが解析される具体的な例が提供されている。

使用例:

Future<void> parseAndLoadData(String data) async {
  final receivePort = ReceivePort();
  await Isolate.spawn(_parseDataIsolate, receivePort.sendPort);
  final sendPort = await receivePort.first;
  final response = ReceivePort();
  sendPort.send([data, response.sendPort]);
  await for (var msg in response) {
    // パースされたデータを処理
  }
}

static void _parseDataIsolate(SendPort initialReplyTo) {
  final port = ReceivePort();
  initialReplyTo.send(port.sendPort);
  port.listen((message) {
    final String data = message[0];
    final SendPort replyTo = message[1];
    // データをパースして返す
  });
}

4. Isolatesに関する高度な洞察

  • メモリ管理: 各Isolateは自身のメモリヒープを機能させ、一つが使用するメモリが他に影響を与えないようにする。この隔離は、メモリ共有による望ましくない副作用を防ぐ。
  • イベントループ: すべてのIsolateには専用のイベントループがあり、独立してマイクロタスクとイベントを管理できる。開発者はこの専用のイベントループのおかげで、Isolatesでasync/awaitを利用できる。
  • Dart:ffi: 上級ユーザーは、Isolatesと組み合わせてDart FFI(Foreign Function Interface)を使用し、特定の計算タスクに対して相当なパフォーマンス利点を提供する可能性のある並列化されたCコードを実行できる。

結論

Flutterの単一スレッドモデルは、ステート管理とUIレンダリングの単純化を保証しているが、滑らかなユーザー体験を保証するためにリソースを集中的に使用するタスクの慎重なハンドリングが求められる。Isolatesは有力な解決策として現れ、開発者が重い計算を別の実行スレッドにオフロードし、ユーザーインターフェースの応答性と流動性を保持することができる。

補足
Flutter cookbookは縦スクロールだが、横スクロールにするためにはPhotoListウィジェットを以下のように書き換えると良い。(元のサンプルでは画像番号が表示されておらず、どこまでスクロールしたか分かりにくいのでインデックスを付けた。)

cclass PhotosList extends StatelessWidget {
  const PhotosList({Key? key, required this.photos}) : super(key: key);

  final List<Photo> photos;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: ListView.builder(
            scrollDirection: Axis.horizontal,
            itemCount: photos.length,
            itemBuilder: (context, index) {
              return SizedBox(
                width: 180, // Adjust width as per your requirement
                child: Column(
                  children: [
                    Expanded(
                      child: Image.network(photos[index].thumbnailUrl),
                    ),
                    Text('${index + 1}'),
                  ],
                ),
              );
            },
          ),
        ),
        Expanded(
          child: ListView.builder(
            scrollDirection: Axis.horizontal,
            itemCount: photos.length,
            itemBuilder: (context, index) {
              return SizedBox(
                width: 180, // Adjust width as per your requirement
                child: Column(
                  children: [
                    Expanded(
                      child: Image.network(photos[index].thumbnailUrl),
                    ),
                    Text('${index + 1}'),
                  ],
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

さらにImageをIsolatesで取得するサンプル

import 'dart:isolate';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void imageFetchIsolate(SendPort sendPort) async {
  final port = ReceivePort();
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    final String url = msg[0];
    final SendPort replyTo = msg[1];

    http.Response response = await http.get(Uri.parse(url));
    replyTo.send(response.bodyBytes);
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ImageFetcher(),
    );
  }
}

class ImageFetcher extends StatefulWidget {
  @override
  _ImageFetcherState createState() => _ImageFetcherState();
}

class _ImageFetcherState extends State<ImageFetcher> {
  late Isolate _isolate;
  late ReceivePort _receivePort;
  late SendPort _sendPort;
  late Uint8List _imageData;

  @override
  void initState() {
    super.initState();
    _receivePort = ReceivePort();
    _createIsolate();
  }

  _createIsolate() async {
    _isolate = await Isolate.spawn(imageFetchIsolate, _receivePort.sendPort);
    _sendPort = await _receivePort.first;

    // Example image URL
    String imageUrl = 'https://via.placeholder.com/150/771796';

    final port = ReceivePort();
    _sendPort.send([imageUrl, port.sendPort]);
    _imageData = await port.first;

    setState(() {}); // Update the UI
  }

  @override
  void dispose() {
    _isolate.kill(priority: Isolate.immediate);
    _receivePort.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Image Fetcher')),
      body: Center(
        child: _imageData == null
            ? CircularProgressIndicator()
            : Image.memory(_imageData),
      ),
    );
  }
}

IsolatesはCallback風の呼び出しもできる。以下はそのサンプル。

import 'dart:isolate';

void isolateEntryPoint(SendPort sendPort) {
  final receivePort = ReceivePort();
  sendPort.send(receivePort.sendPort);

  receivePort.listen((message) {
    final sendPortToMain = message[0] as SendPort;
    sendPortToMain.send("Hello from Isolate!");
  });
}

void main() {
  final mainReceivePort = ReceivePort();

  // Spawn the isolate
  Isolate.spawn(isolateEntryPoint, mainReceivePort.sendPort)
      .then((isolateInstance) {
    // Handle isolate instance, manage lifecycle if needed
  }).catchError((error) {
    // Handle error during isolate spawn
    print("Error spawning isolate: $error");
  });

  // Listen for the first message from the isolate
  mainReceivePort.first.then((isolateSendPort) {
    final isolateResponseReceivePort = ReceivePort();
    (isolateSendPort as SendPort).send(isolateResponseReceivePort.sendPort);

    // Listen for messages from the isolate
    isolateResponseReceivePort.listen((message) {
      print("Received message: $message");
    });
  });
}

Isolatesで100ずつの応答に応えるには、まずIsolatesを下記のように準備する。

void imageFetchIsolate(SendPort sendPort) {
// Setup a ReceivePort to receive messages from the main isolate.
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);

receivePort.listen((message) {
// Handle messages from the main isolate, and fetch images or do some work here…
// …
// Send a response back to the main isolate.
final mainIsolateSendPort = message[0] as SendPort;
mainIsolateSendPort.send(/your response data/);
});
}

それから

class _MyAppState extends State {
late Isolate _isolate;
final _receivePort = ReceivePort();
late SendPort _sendPort;
int _messageCount = 0;

@override
void initState() {
super.initState();
_createIsolate();
}

_createIsolate() async {
_isolate = await Isolate.spawn(imageFetchIsolate, _receivePort.sendPort);
_sendPort = await _receivePort.first;

_receivePort.listen((message) {
  _messageCount++;
  // Handle the message...
  // ...

  // If 100 messages have been received, do some job.
  if (_messageCount % 100 == 0) {
    _doSomeJob();
  }
});

}

_doSomeJob() {
// Do some job after every 100 messages…
// …
}

@override
void dispose() {
_isolate.kill();
super.dispose();
}

@override
Widget build(BuildContext context) {
// …
}
}

のようにする。

コメントを残す