序文
実際、Flutter自体がすでにイメージをロードする機能を持っており、Imageコンポーネントはネットワークイメージ、ローカルイメージ、ファイルイメージのロードに対応しています。なぜ他のイメージ読み込みソリューションを実装する必要があるのでしょうか?実は、Flutterのイメージコンポーネントの機能に欠陥があるからです:
- イメージキャッシュには永続性がなく、ネットワークレス環境でのイメージ表示には対応していません。
- ファイルイメージがネイティブ環境と共有されないため、イメージリソースファイルが重複します。
そこで、日々の開発ニーズや最適化ポイントを満たすために、イメージコンポーネントの機能を満足のいくものにするためにできることがいくつかあります。次のステップは、Flutterネイティブコンポーネントから外部テクスチャへのイメージコンポーネント機能の進化についてゆっくりと学ぶことです。
Flutter ネイティブイメージコンポーネント
Flutterのネイティブイメージは複数の読み込み形式をサポートしています:
- Image.network
- Image.file
- Image.asset
- Image.memory
簡単なイメージ読み込みプロセス
Image.network(
String src, {
......不要なコードを省く
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
...... 不要なコードを省く
super(key: key);
......
const factory NetworkImage(String url, { double scale, Map<String, String> headers }) = network_image.NetworkImage;
- ステップ2:NetWorkImageは、他の形式のローディングと同様に、基本的にImageProviderを継承します。ImageProviderはイメージを処理する基本的な抽象クラスで、それを継承するローディングクラスは主にloadメソッドを実装し、さまざまな形式のローディング処理を実行します。
abstract class ImageProvider<T> {
const ImageProvider();
.......
@protected
ImageStreamCompleter load(T key, DecoderCallback decode);
.......
}
- ステップ3:たとえば、ネットワークのフォームは、Dartの層のネットワーク要求HttpClient要求のイメージを介して、ネットワークを介してイメージデータプロセスを取得する最終的なデータUint8Listを取得します。
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
.......
@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
];
},
);
}
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
image_provider.DecoderCallback decode,
) async {
try {
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok) {
PaintingBinding.instance.imageCache.evict(key);
throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
}
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
return decode(bytes);
} finally {
chunkEvents.close();
}
}
}
- ステップ4:イメージUint8Listデータを取得した後はデコード処理です。DecoderCallbackコールバックメソッドで生のイメージデータを取得した後、グローバルシングルトンデコーダPaintingBinding.instance.instantiateImageCodecに送られ、エンジンレイヤーC++のinstantiateImageCodecがデータを処理し、Flutterレイヤーでレンダリング表示できるイメージデータを返します。Flutterレイヤーのレンダリング表示イメージデータ
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => load(key, PaintingBinding.instance.instantiateImageCodec),
onError: handleError,
);
Future<ui.Codec> instantiateImageCodec(Uint8List bytes, {
int cacheWidth,
int cacheHeight,
}) {
assert(cacheWidth == null || cacheWidth > 0);
assert(cacheHeight == null || cacheHeight > 0);
return ui.instantiateImageCodec(
bytes,
targetWidth: cacheWidth,
targetHeight: cacheHeight,
);
}
String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
native 'instantiateImageCodec';
- ステップ5:codec.ccにあるエンジン層のc++デコーダーは、SkiaのSkCodecを呼び出してイメージデータの処理を行います。デコーダーによる内部処理の後、ToDartを実行してui_codecをDartレイヤーに返します。
/// Dart
_String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
native 'instantiateImageCodec';
/// c++
static void InstantiateImageCodec(Dart_NativeArguments args) {
UIDartState::ThrowIfUIOperationsProhibited();
Dart_Handle callback_handle = Dart_GetNativeArgument(args, 1);
.......コードの一部を省略する
Dart_Handle image_info_handle = Dart_GetNativeArgument(args, 2);
std::optional<ImageDecoder::ImageInfo> image_info;
/// イメージ情報が空であるかどうか、いくつかの処理を行うために空ではない
if (!Dart_IsNull(image_info_handle)) {
auto image_info_results = ConvertImageInfo(image_info_handle, args);
if (auto value =
std::get_if<ImageDecoder::ImageInfo>(&image_info_results)) {
image_info = *value;
} else if (auto error = std::get_if<std::string>(&image_info_results)) {
Dart_SetReturnValue(args, tonic::ToDart(*error));
return;
}
}
sk_sp<SkData> buffer;
{
/// イメージデータを処理する
Dart_Handle exception = nullptr;
tonic::Uint8List list =
tonic::DartConverter<tonic::Uint8List>::FromArguments(args, 0,
exception);
if (exception) {
Dart_SetReturnValue(args, exception);
return;
}
/// イメージデータのコピー
buffer = MakeSkDataWithCopy(list.data(), list.num_elements());
}
if (image_info) {
const auto expected_size =
image_info->row_bytes * image_info->sk_info.height();
if (buffer->size() < expected_size) {
Dart_SetReturnValue(
args, ToDart("Pixel buffer size does not match image size"));
return;
}
}
/// イメージの幅と高さを取得する
const int targetWidth =
tonic::DartConverter<int>::FromDart(Dart_GetNativeArgument(args, 3));
const int targetHeight =
tonic::DartConverter<int>::FromDart(Dart_GetNativeArgument(args, 4));
std::unique_ptr<SkCodec> codec;
bool single_frame;
if (image_info) {
single_frame = true;
} else {
/// 基礎となるデコーダーはSkCodecデコーダーで、これは基礎となるAndroidレイヤーでも使われている。
codec = SkCodec::MakeFromData(buffer);
if (!codec) {
Dart_SetReturnValue(args, ToDart("Could not instantiate image codec."));
return;
}
single_frame = codec->getFrameCount() == 1;
}
/// デコーダーは同時にフレームをデコードし、イメージが動画かどうかを判断する。
fml::RefPtr<Codec> ui_codec;
if (single_frame) {
ImageDecoder::ImageDescriptor descriptor;
descriptor.decompressed_image_info = image_info;
if (targetWidth > 0) {
descriptor.target_width = targetWidth;
}
if (targetHeight > 0) {
descriptor.target_height = targetHeight;
}
descriptor.data = std::move(buffer);
ui_codec = fml::MakeRefCounted<SingleFrameCodec>(std::move(descriptor));
} else {
ui_codec = fml::MakeRefCounted<MultiFrameCodec>(std::move(codec));
}
/// 最後に、デコーダーの結果がDartレイヤーに返される。
tonic::DartInvoke(callback_handle, {ToDart(ui_codec)});
}
外部テクスチャレンダリングイメージ
FlutterにはTextureというコンポーネントがありますが、これは入力パラメータがtextureIdしかないので、数行のコードで外部テクスチャを実装する方法を理解するのは難しいです。外部テクスチャの原理を分析する前に、まず外部テクスチャでイメージをレンダリングする機能を理解する必要があります。
テクスチャコンポーネントの使用
- Javaレイヤーは、ChannelプラグインPluginRegistry.Registrarを通してSurface、textureIdを作成します。
/// テクスチャ登録者を取得するプラグインインターフェース
TextureRegistry textureRegistry = registrar.textures();
/// Textureインスタンスを作成する
TextureRegistry.SurfaceTextureEntry surfaceTextureEntry = textureRegistry.createSurfaceTexture();
long textureId = surfaceTextureEntry.id();
SurfaceTexture surfaceTexture = surfaceTextureEntry.surfaceTexture();
/// イメージのアドレスを取得する
String url = call.argument("url");
...... イメージリクエストのロード処理を省く
/// ロードするSurfaceインスタンスを作成するsurfaceTexture
Surface surface = new Surface(surfaceTexture);
/// キャンバス描画 ビットマップ テクスチャマッピング
Canvas canvas = surface.lockCanvas(rect);
canvas.drawBitmap(bitmap, null, rect, null);
bitmap.recycle();
surface.unlockCanvasAndPost(canvas);
/// Dart textureId
Map<String, Object> maps = new HashMap<>();
maps.put("textureId", textureId);
result.success(maps);
- Dartレイヤーは、読み込んだイメージパスをNativeレイヤーに渡すためのMethodChannelを作成します。
static const MethodChannel _channel = const MethodChannel('texture_channel');
/// オリジナルのロードイメージインターフェース
static Future<Map> loadTexture({String url}) async {
var args = <String, dynamic>{
"url": url,
};
return await _channel.invokeMethod("loadTexture", args);
}
/// 読み込みを実行する
Map _textureResult = await TexturePlugin.loadTexture(
url: _uri.toString(),
width: url.width,
height: url.height,
);
/// ネイティブが生成したtextureId
int id = _textureResult['textureId'];
/// イメージを表示するためにテクスチャコンポーネントをインスタンス化する
Texture( textureId: id);
コード解決
ダーツレイヤー
- Texture
class Texture extends LeafRenderObjectWidget {
const Texture({
Key key,
@required this.textureId,
}) : assert(textureId != null),
super(key: key);
final int textureId;
@override
TextureBox createRenderObject(BuildContext context) => TextureBox(textureId: textureId);
@override
void updateRenderObject(BuildContext context, TextureBox renderObject) {
renderObject.textureId = textureId;
}
}
- TextureBox
class TextureBox extends RenderBox {
TextureBox({ @required int textureId })
: assert(textureId != null),
_textureId = textureId;
...... コードを省略する
@override
void paint(PaintingContext context, Offset offset) {
if (_textureId == null)
return;
context.addLayer(TextureLayer(
rect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
textureId: _textureId,
));
}
}
- TextureLayer
class TextureLayer extends Layer {
TextureLayer({
@required this.rect,
@required this.textureId,
this.freeze = false,
}) : assert(rect != null),
assert(textureId != null);
......
@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
final Rect shiftedRect = layerOffset == Offset.zero ? rect : rect.shift(layerOffset);
builder.addTexture(
textureId,
offset: shiftedRect.topLeft,
width: shiftedRect.width,
height: shiftedRect.height,
freeze: freeze,
);
}
}
- SceneBuilder
class SceneBuilder extends NativeFieldWrapperClass2 {
void addTexture(
int textureId, {
Offset offset = Offset.zero,
double width = 0.0,
double height = 0.0,
bool freeze = false,
}) {
_addTexture(offset.dx, offset.dy, width, height, textureId, freeze);
}
/// SceneBuilder_addTexture scene_builder.cc SceneBuilder::addTexture
void _addTexture(double dx, double dy, double width, double height, int textureId, bool freeze)
native 'SceneBuilder_addTexture';
- scene_builder.cc
void SceneBuilder::addTexture(double dx,
double dy,
double width,
double height,
int64_t textureId,
bool freeze) {
auto layer = std::make_unique<flutter::TextureLayer>(
SkPoint::Make(dx, dy), SkSize::Make(width, height), textureId, freeze);
AddLayer(std::move(layer));
}
- texture_layer.cc
/// テクスチャレイヤーオブジェクトを作成する
TextureLayer::TextureLayer(const SkPoint& offset,
const SkSize& size,
int64_t texture_id,
bool freeze)
: offset_(offset), size_(size), texture_id_(texture_id), freeze_(freeze) {}
/// テクスチャーオブジェクトの描画メソッド
void TextureLayer::Paint(PaintContext& context) const {
TRACE_EVENT0("flutter", "TextureLayer::Paint");
/// textureオブジェクトが描画されると、GetTextureメソッドのマップからテクスチャーオブジェクトが見つかり、描画される。
std::shared_ptr<Texture> texture =
context.texture_registry.GetTexture(texture_id_);
if (!texture) {
TRACE_EVENT_INSTANT0("flutter", "null texture");
return;
}
texture->Paint(*context.leaf_nodes_canvas, paint_bounds(), freeze_,
context.gr_context);
}
Javaレイヤー
- FlutterRenderer
public class FlutterRenderer implements TextureRegistry {
public FlutterRenderer(@NonNull FlutterJNI flutterJNI) {
this.flutterJNI = flutterJNI;
this.flutterJNI.addIsDisplayingFlutterUiListener(flutterUiDisplayListener);
}
@Override
public SurfaceTextureEntry createSurfaceTexture() {
/// SurfaceTexture
final SurfaceTexture surfaceTexture = new SurfaceTexture(0);
surfaceTexture.detachFromGLContext();
final SurfaceTextureRegistryEntry entry =
new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture);
registerTexture(entry.id(), surfaceTexture);
return entry;
}
///ネイティブJNIレイヤーに登録する
private void registerTexture(long textureId, @NonNull SurfaceTexture surfaceTexture) {
flutterJNI.registerTexture(textureId, surfaceTexture);
}
}
- FlutterJNI
public void registerTexture(long textureId, @NonNull SurfaceTexture surfaceTexture) {
/// ここでは、メインスレッドで実行する必要がありますに注意を払う必要があるか、エラーが報告される!
ensureRunningOnMainThread();
ensureAttachedToNative();
nativeRegisterTexture(nativePlatformViewId, textureId, surfaceTexture);
}
C++レイヤー
- platform_view_android_jni.cc
{
.name = "nativeRegisterTexture",
.signature = "(JJLandroid/graphics/SurfaceTexture;)V",
.fnPtr = reinterpret_cast<void*>(&RegisterTexture),
}
static void RegisterTexture(JNIEnv* env,
jobject jcaller,
jlong shell_holder,
jlong texture_id,
jobject surface_texture) {
ANDROID_SHELL_HOLDER->GetPlatformView()->RegisterExternalTexture(
static_cast<int64_t>(texture_id), //
fml::jni::JavaObjectWeakGlobalRef(env, surface_texture) //
);
}
- platform_view_android.cc
void PlatformViewAndroid::RegisterExternalTexture(
int64_t texture_id,
const fml::jni::JavaObjectWeakGlobalRef& surface_texture) {
RegisterTexture(
std::make_shared<AndroidExternalTextureGL>(texture_id, surface_texture));
}
- texture.cc
// テクスチャ方式を登録する
void TextureRegistry::RegisterTexture(std::shared_ptr<Texture> texture) {
if (!texture) {
return;
}
// 内部マップ保存テクスチャの例
mapping_[texture->Id()] = texture;
}
// テクスチャオブジェクトを取得し、マップから取り出す。
std::shared_ptr<Texture> TextureRegistry::GetTexture(int64_t id) {
auto it = mapping_.find(id);
return it != mapping_.end() ? it->second : nullptr;
}
- android_external_texture_gl.cc
/// 外部テクスチャ・オブジェクトの例
AndroidExternalTextureGL::AndroidExternalTextureGL(
int64_t id,
const fml::jni::JavaObjectWeakGlobalRef& surfaceTexture)
: Texture(id), surface_texture_(surfaceTexture), transform(SkMatrix::I()) {}
AndroidExternalTextureGL::~AndroidExternalTextureGL() {
if (state_ == AttachmentState::attached) {
glDeleteTextures(1, &texture_name_);
}
}
/// 描画方法
void AndroidExternalTextureGL::Paint(SkCanvas& canvas,
const SkRect& bounds,
bool freeze,
GrContext* context) {
if (state_ == AttachmentState::detached) {
return;
}
if (state_ == AttachmentState::uninitialized) {
glGenTextures(1, &texture_name_);
Attach(static_cast<jint>(texture_name_));
state_ = AttachmentState::attached;
}
if (!freeze && new_frame_ready_) {
Update();
new_frame_ready_ = false;
}
GrGLTextureInfo textureInfo = {GL_TEXTURE_EXTERNAL_OES, texture_name_,
GL_RGBA8_OES};
GrBackendTexture backendTexture(1, 1, GrMipMapped::kNo, textureInfo);
sk_sp<SkImage> image = SkImage::MakeFromTexture(
canvas.getGrContext(), backendTexture, kTopLeft_GrSurfaceOrigin,
kRGBA_8888_SkColorType, kPremul_SkAlphaType, nullptr);
if (image) {
SkAutoCanvasRestore autoRestore(&canvas, true);
canvas.translate(bounds.x(), bounds.y());
canvas.scale(bounds.width(), bounds.height());
if (!transform.isIdentity()) {
SkMatrix transformAroundCenter(transform);
transformAroundCenter.preTranslate(-0.5, -0.5);
transformAroundCenter.postScale(1, -1);
transformAroundCenter.postTranslate(0.5, 0.5);
canvas.concat(transformAroundCenter);
}
canvas.drawImage(image, 0, 0);
}
}
- JavaレイヤーのFlutterRendererはSurfaceTextureとtextureIdを作成します。
- surfaceTexture と textureId を JNI 経由でエンジンレイヤーに登録します。
- メソッドのレイヤーを通してエンジン登録プロセスへ、そして最終的にtexture.ccのTextureRegistryで、インスタンスオブジェクトをキャッシュするためのキーと値のペアの形式でマップによって。
- 画面外に描画するイメージを SurfaceTexture 上に表示する必要があります。
- Javaレイヤーで作成されたtextureIdは、Channelを通してDartレイヤーにTextureコンポーネントのエントリーパラメーターとして渡されます。
- DartのTextureコンポーネントは、textureIdの入力を受け取り、それを下位コンポーネントにインスタンス化します。
- (vii) SceneBuilder が addTexture を呼び出すと、実行エンジン層が TextureLayer を作成します。
イメージVSテクスチャー
ImageコンポーネントとTextureコンポーネントは基本的にRenderBoxの実装で、Flutterのレンダリングツリーとして知られているRenderObjectがFlutterエンジンを通して最終的にLayerにマージされ、画面上に描画され、ページのコンテンツを表示します。両者の違いはRenderObjectのペイントの実装にあり、Imageの実装はui.Canvasに描画されるui.Imageコンテンツ、Textureはui.Sceneに追加されるTextureLayerです。違いは、Imageは描画操作を行い、Textureは追加操作を行うことで、イメージのロードと描画のテクスチャスキームは完全にネイティブプラットフォームで完了します。