flutter_isolateは何をしているのか
概要
flutterの重い処理は別のisolateで行うとアプリの動作を軽くできますが、androidやios固有のコードを利用する処理(通知やセンサ情報の取得など)は別isolateではできません。
それを可能にしてくれるライブラリがflutter_isolateです。
このライブラリの中身がどうなっているのか調べてみました。
なぜ別isolateでplatform固有の処理が行えないのか
platform固有の処理を行うためにはMethodChannelを作成し、platform固有の処理を呼び出します(参考)。この時の通信(呼び出す関数の種類や引数の受け渡しなど)がmainのisolateからしか行えないような仕様になっているみたいです。(参考)
なぜflutter_isolateは別isolateでplatform固有の処理ができるのか
じゃあflutter_isolateはどうしているのかというと、別isolateを作るのではなく、dartコードの実行環境を丸ごと別で作っているらしい。
処理の概要
処理の概要は以下の通りです。
1 各platform側で新しいDart実行環境を作成
2 新しい環境と最初の環境間の通信を確立
3 最初の環境が持っている、実行するメソッドの情報を貰い、新しい環境で実行
詳細:Dart実行環境の作成
flutter_isolateのspawn()が実行されると、android、iOSそれぞれのplatformとのMethodChannelが作成され、各platform側でFlutterEngineのインスタンスが作成されます。このFlutterEngineというのがDartを実行できる環境です。
flutter_isolate.dart
static Future spawn<T>(void entryPoint(T message), T message) async {
//略
_control.invokeMethod("spawn_isolate", {
"entry_point":
PluginUtilities.getCallbackHandle(_flutterIsolateEntryPoint)
.toRawHandle(),
"isolate_id": isolateId
}); //①
return isolateResult.future;
}
//略
static final _control = MethodChannel("com.rmawatson.flutterisolate/control");
①で各platform側の対応処理が呼び出されます
FlutterIsolatePlugin.java
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("spawn_isolate")) {
IsolateHolder isolate = new IsolateHolder();
isolate.entryPoint = call.argument("entry_point");
isolate.isolateId = call.argument("isolate_id");
isolate.result = result;
queuedIsolates.add(isolate);
if (queuedIsolates.size() == 1) // no other pending isolate
startNextIsolate();
}
//略
}
android側の処理です。startNextIsolate()で環境設定が行われます。
FlutterIsolatePlugin.java
private void startNextIsolate() {
IsolateHolder isolate = queuedIsolates.peek();
FlutterMain.ensureInitializationComplete(context, null);
if (flutterPluginBinding == null)
isolate.view = new FlutterNativeView(context, true);
else
isolate.engine = new FlutterEngine(context);
//② Dart環境の構築
//③実行する関数の情報を取得
FlutterCallbackInformation cbInfo = FlutterCallbackInformation.lookupCallbackInformation(isolate.entryPoint);
FlutterRunArguments runArgs = new FlutterRunArguments();
runArgs.bundlePath = FlutterMain.findAppBundlePath(context);
runArgs.libraryPath = cbInfo.callbackLibraryPath;
runArgs.entrypoint = cbInfo.callbackName;
if (flutterPluginBinding == null) {
//略
} else {
isolate.controlChannel = new MethodChannel(isolate.engine.getDartExecutor().getBinaryMessenger(), NAMESPACE + "/control");
isolate.startupChannel = new EventChannel(isolate.engine.getDartExecutor().getBinaryMessenger(), NAMESPACE + "/event");
}
//④Flutter側に情報を送るチャネルを設定
isolate.startupChannel.setStreamHandler(this);
isolate.controlChannel.setMethodCallHandler(this);
if (flutterPluginBinding == null) {
//略
} else {
DartExecutor.DartCallback dartCallback = new DartExecutor.DartCallback(context.getAssets(), runArgs.bundlePath, cbInfo);
isolate.engine.getDartExecutor().executeDartCallback(dartCallback); //⑤dart関数を実行
}
}
flutterPluginBindingというのはプラグインが導入された時に導入先の実行環境などが設定される変数のようですが、これがnullになるのはどういう場合なのかいまいち分からないので今回は省略します。
②で新たにDart実行環境であるFlutterEngineを作成しています。
③では新しく作ったFlutterEngineで実行する関数の情報を取得しています。この場合は①でinvokeMethodの引数に渡した_flutterIsolateEntryPointのことです。
④ではFlutter側に情報を送るための設定をしています。
⑤でdart関数(_flutterIsolateEntryPoint)を実行します。
さらに、④の続きで、Flutter側に情報を送る設定をします。
@Override
public void onListen(Object o, EventChannel.EventSink sink) {
IsolateHolder isolate = queuedIsolates.remove();
sink.success(isolate.isolateId);
//⑥ Flutter側にisolateIdを送る
sink.endOfStream();
activeIsolates.put(isolate.isolateId, isolate);
isolate.result.success(null);
isolate.startupChannel = null;
isolate.result = null;
if (queuedIsolates.size() != 0)
startNextIsolate();
}
④でEventChannel(変数名はstartupChannel)に設定したStreamHandlerのonListenイベントを実装します。⑥でFlutter側にisolateIdが送られます。
(onListen()で受け取ったsinkをローカル変数に保存し、後ほど任意のタイミングでFlutter側に送信するのが本来の使い方ですが、ここでは情報を送信することだけが目的なのですぐに送り返しています(参考))
このようにして作られたFlutterEnginで_flutterIsolateEntryPointが実行されます。これはすぐさま別のメソッドを呼び出します。
flutter_isolate.dart
void _flutterIsolateEntryPoint() => FlutterIsolate._isolateInitialize();
_isolateInitializeでは、所望のメソッドの情報を最初の環境から受け取り、新たな環境で実行します。
以下詳しく見ていきましょう。
詳細:元の環境から実行するメソッドの情報を取得し、実行
flutter_isolate.dart
static Future spawn<T>(void entryPoint(T message), T message) async {
final userEntryPointId =
PluginUtilities.getCallbackHandle(entryPoint).toRawHandle();
final isolateId = Uuid().v4();
final isolateResult = Completer<FlutterIsolate>();
final setupReceivePort = ReceivePort();
IsolateNameServer.registerPortWithName(
setupReceivePort.sendPort, isolateId); //⑦ Portを開設
StreamSubscription setupSubscription;
setupSubscription = setupReceivePort.listen((data) {
final portSetup = (data as List<Object>);
SendPort setupPort = portSetup[0];
final remoteIsolate =
FlutterIsolate._(isolateId, portSetup[1], portSetup[2], portSetup[3]);
setupPort.send([userEntryPointId, message]);
// ⑩ 新しい環境にメソッドの情報を送信
setupSubscription.cancel();
setupReceivePort.close();
isolateResult.complete(remoteIsolate);
});
_control.invokeMethod("spawn_isolate", {
"entry_point":
PluginUtilities.getCallbackHandle(_flutterIsolateEntryPoint)
.toRawHandle(),
"isolate_id": isolateId
});
return isolateResult.future;
}
static void _isolateInitialize() {
WidgetsFlutterBinding.ensureInitialized();
window.onPlatformMessage = BinaryMessages.handlePlatformMessage;
StreamSubscription eventSubscription;
eventSubscription = _event.receiveBroadcastStream().listen((isolateId) {
//⑧FlutterEngineからisolateIdを受け取る
_current = FlutterIsolate._(isolateId, null, null);
final sendPort = IsolateNameServer.lookupPortByName(_current._isolateId);
final setupReceivePort = ReceivePort();
IsolateNameServer.removePortNameMapping(_current._isolateId);
sendPort.send([
setupReceivePort.sendPort,
Isolate.current.controlPort,
Isolate.current.pauseCapability,
Isolate.current.terminateCapability
]); //⑨ 最初の環境にこちらのportを送信
eventSubscription.cancel();
StreamSubscription setupSubscription;
setupSubscription = setupReceivePort.listen((data) {
final args = data as List<Object>;
final int userEntryPointHandle = args[0];
final userMessage = args[1];
Function userEntryPoint = PluginUtilities.getCallbackFromHandle(
CallbackHandle.fromRawHandle(userEntryPointHandle));
setupSubscription.cancel();
setupReceivePort.close();
userEntryPoint(userMessage); //11 実行
});
});
}
ここからはspawn()と_isolateInitialize()を行き来します。
まず初めに⑦で最初の環境側で通信用のポートを開設し、IsolateNameServerに登録します。これで他の環境やisolateから名前(今回はisolateId)でポートを検索できます。
時系列では次に新たな環境で_isolateInitialize()が実行されます。⑧でEventChannelのlistenを行うと、⑥で設定したonListen()イベントが発火し、platform側からisolateIdを受け取ります。(これは元々①で最初の環境からplatform側に渡していたものです)
上でも説明したように、platform側はすぐにisolateIdを送り返してきて、listen()の中が実行されます。
新しい環境でも通信用Portを開設し、platformから送られてきたisolateIdで最初の環境のPortを探し、こちらのPortを送信します。(⑨)
そうすると、spawn()で設定したデータ受信後のイベントが走ります。ここでは受け取った新しい環境のPortにspawn()が引数として受け取った、実行したいメソッドの情報を送り返します。(⑩)
そしてようやく新しい環境で所望のメソッドが実行されます(11)
通信の部分を図にまとめると以下のようになります(アドレスというのはあくまで比喩表現です)
最後に
この記事一つ書くだけでMethodChannelやらReceivePortやらFlutterEngineやら色々勉強できました。
これからもこういう研究は続けていきたいですね。
参考文献
最新記事
すべて表示やりたいこと TextFieldで入力フォームを作りたい。 例えば入力内容が金額の場合、3桁区切りで頭に¥を付けた表記にしたい。 ただしユーザにこれらを入力させるのではなく、ユーザはあくまで数字を入力するだけで、アプリ側で自動でフォーマットしたい。 方法...
現象 やってること iosシミュレータで画像をデバイスのローカルに保存 保存したパスをデータベースに保存 アプリ立ち上げ時にデータベースから画像パスを取得し、そのパスの画像を画面上に表示 起きている現象 iosシミュレータを再起動した場合、上記3で「ファイルパスが見つからな...
やりたいこと 初期値さえ決まればあとは不変な変数がある ただし、コンストラクタ起動時にはまだ決定できない このような変数について late finalで変数を定義 (何らかのタイミングで)初期化されたかどうかをチェックし、されていなければ値を入れる(チェックしないとfina...
Comments