flutter共享native资源的多种姿(fang)势(shi)

导语:flutter+native混合开发过程中,flutter可能需要共享native已有的资源,如app内置资源、下载好的数据、已缓存的内存数据等,这里介绍几种flutter共享native资源的方式,包括通常的channel、file,以及指针方式实现内存共享。以安卓为例。


使用flutter开发全新app时,资源一般是放置在flutter工程中,ios、android两端共享。但是在已有app中集成flutter进行flutter+native的混合开发过程中,为了能复用app已有资源,flutter经常需要向native拿取这些资源,如已内置的图片、文件等。本文主要介绍几种flutter向native拿取资源的方式。以android为例。

目录

  1. channel bytes流传输方式
  2. 文件路径方式
  3. 内存指针共享方式
  4. bitmap内存指针共享
  5. 修改flutter engine直接读取native内置其他assets资源方式

先上小菜,flutter如何与native进行通信?

  • flutter提供了platform channel与native进行通信,官方介绍 , 别人家的原理剖析。
  • flutter、native双方以channel作为桥梁,以channel name作为标识,将调用转到对方指定代码。
  • 在native侧注册监听,等待flutter调用,通过channel将native信息返回给flutter。
//android java监听
final MethodChannel channel = new MethodChannel(flutterView, "your_method_name");
channel.setMethodCallHandler(new MethodCallHandler() {//注册监听
  @Override
  public void onMethodCall(MethodCall methodCall, Result result) {
    if(methodCall.method.equals("your_method_name")) {
      String arg1 = methodCall.argument("arg1");
      Map reply = new HashMap();
      reply.put("result", "haha2");
      result.success(reply);//返回值
    }
  }
//flutter dart 调用
const MethodChannel _channel = const MethodChannel("your_channel_name");
final Map reply = await _channel.invokeMapMethod('your_method_name', {
              "arg1" : "haha"
            });
  • 同样,也可以在flutter注册监听,等待native调用,通过channel将flutter信息传递给native。
//flutter dart监听
const MethodChannel _channel = const MethodChannel("your_channel_name");
_channel.setMethodCallHandler((methodCall) async{//注册监听
  if(methodCall.method == "your_method_name"){
    return "haha";//返回
  }
  return null;
});
//android java调用
new MethodChannel(flutterView, "your_channel_name")
    .invokeMethod("your_method_name", your_args, new MethodChannel.Result(){//调用
        @Override
        public void success(Object o) {//返回值
        }
        @Override
        public void error(String s, String s1, Object o) {
        }
        @Override
        public void notImplemented() {
        }
    });

下面进入正题

1. channel bytes流传输方式

  • channel上可以传递多种数据格式,本质上也都是bytes流,这种方式是把数据以bytes流方式通过channel传给flutter。
  • 例如native通过bytes流把native内置drawable图片传给flutter。
  • flutter没有直接的api可以读取android native内置的drawable、asset资源,flutter只支持直接读取在flutter侧添加的flutter_assets资源。所以bytes流方式可以帮助实现对这些native内置资源的访问。
//android java侧读取资源,得到byte[],回传给flutter
//flutter method call resName => resId
InputStream inputStream = context.getResources().openRawResource(resId);
//从inputStream种读取资源,转成bytes
int size = inputStream.available();
byte[] bytes = new byte[size];
byte[] buffer = new byte[Math.min(1024, size)];
int index = 0;
int len = inputStream.read(buffer);//读取资源到byte[]
while (len != -1){
    System.arraycopy(buffer, 0, bytes, index, len);
    index += len;
    len = inputStream.read(buffer);
}
result.success(bytes);//把bytes写到channel种返回给flutter
inputStream.close();
//flutter调用,拿取byte[]。在flutter 侧 byte[]对应Uint8List
Uint8List data = await _channel.invokeMethod('getNativeImage', {
    "imageName" : "xxx",
  });
//flutter Image.memory api 可以把这些Uint8List/byte[]展示成图像

2. 文件路径方式

  • android apk内置资源组织方式使得内置图片/文件在flutter侧不能以file方式直接读取,因为这些内置资源是以数据块方式存放在apk这个大文件中的一片段上,通过android系统的assset_manager来管理和读取。
  • 不过可以通过app缓存目录来中转,flutter需要时,native通过系统接口读取并写入到app缓存目录or sdcard,告知flutter文件path, flutter以文件方式访问。(PS: 在内置资源没有更新时可以不必重新写入)
//android java读取内置drawable写入缓存目录/sdcard
//flutter method call resName => resId
InputStream inputStream = context.getResources().openRawResource(resId);
File parent = outFile.getParentFile();
if(!parent.exists()){
    parent.mkdirs();
}
FileOutputStream fos = new FileOutputStream(outFile);
byte[] buffer = new byte[1024];
int byteCount = inputStream.read(buffer);
while ((byteCount) != -1) {
    fos.write(buffer, 0, byteCount);
    byteCount = inputStream.read(buffer);
}
fos.flush();//刷新缓冲区
inputStream.close();
fos.close();
//return outFile path
//flutter 拿取到文件path 以  Image.file 展示图片

3. 内存指针共享方式

  • 在native读取数据转成byte[]后,如何传输给flutter,除上面两种方式,还可以通过内存指针共享方式,把native侧数据指针地址和length传递给flutter,flutter依据内存指针地址和length读取处理数据。
  • flutter是运行于native所封装的环境中,在同一个进程,内存地址空间并没有隔离,可以共享内存空间。但这里有两个问题需要解决,由于java和dart语言中并没有像c/c++那样的指针用法,需要解决:1)在android java中拿到内存指针传给flutter dart;2)在flutter dart中把指针转换成dart数据结构使用。
  • 1)如何拿到byte[]内存指针?通过jni方式
//android java侧拿取byte[]指针
jbyte *cData = env->GetByteArrayElements(bytes, &isCopy);
  • 因为java byte[]是在java堆上申请的,根据不同系统实现,这种方式可能会导致数据在jni被复制一份,产生更多的内存增量,参考NDK开发指导:如何使用原生代码共享原始数据? 。推荐使用ByteBuffer.allocateDirect, 分配jni native byte[]。另外在内存指针返回给flutter使用时,native侧需要保证这份内存数据不被回收掉,flutter用完时需通知native释放。
//android java代码
InputStream inputStream = context.getResources().openRawResource(resId);
int size = inputStream.available();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(size);
//...read inputStream to byteBuffer
long ptr = JniInterface.native_getByteBufferPtr(byteBuffer);
Map reply = new HashMap();
reply.put("rawDataAddress", ptr);
reply.put("rawDataLength", totalLength);
//cacheObj(nativeImageID, byteBuffer);//需要缓存一下,以保证flutter使用时没有被释放
result.success(reply);
inputStream.close();
//android jni 获取内存指针
Java_com_xxxx_JniInterface_native_1getByteBufferPtr(
        JNIEnv *env, jclass clazz, jobject byte_buffer) {
    jbyte *cData = (jbyte*)env->GetDirectBufferAddress(byte_buffer);//获取指针
    return (jlong)cData;
}
  • 2)flutter侧如何使用native传递的指针?dart:ffi Pointer.fromAddress(flutter>=1.9) 或 修改engine添加接口
//flutter dart 把指针转换成dart数据结构Uint8List
import 'dart:ffi';
Pointer pointer = Pointer.fromAddress(
    rawDataAddress); //address是内存地址
Uint8List bytes = pointer.asExternalTypedData(
    count: rawDataLength);
//Uint8List bytes可以通过 Image.memory 接口显示图像
//建议参考MemoryImage重写一个ImageProvider把对native内存引用释放罗加入
//之前调用native获取指针,增加内存引用计数1
PaintingBinding.instance.instantiateImageCodec(bytes) ;
//之后通知减除内存引用1
//对于低版本不支持dart:ffi的估计是自定义engine了,可以自己添加接口,实现指针转Uint8List
const long address = tonic::DartConverter::FromDart(Dart_GetNativeArgument(args, 0));
void* ptr = reinterpret_cast(address);
const int bytes_size = tonic::DartConverter::FromDart(Dart_GetNativeArgument(args, 1));
tonic::DartInvoke(callback_handle,{
    tonic::DartConverter::ToDart(reinterpret_cast(ptr), bytes_size)
});
  • 这两个问题解决后,通过channel串联起来即可实现,指针方式的内存共享。好处是没有大块数据通过channel拷贝传递,但需要注意内存的引用和释放。

4. bitmap内存指针共享

  • bitmap内存共享与上一节相似,共享的bitmap在内存的pixel bytes。为什么要bitmap共享呢?flutter+native混合开发中,一些图片已经在native的内存中加载了,如果flutter能够复用这内存,既能节省内存,也能省去读取文件和解码图片的过程,优化性能。
  • 网上也有通过纹理方式在native和flutter间进行图片共享的方法,这种方式需要在native维护一个GL线程,不是频繁复用场景(如gallery/camera) ,成本有点高。
  • 字节跳动Flutter架构实践“图片透传优化方案”一节也提出了通过改engine实现bitmap内存共享,方案图如下,不过并没有给出具体实现介绍。
  • 我们这种bitmap共享方式可以不依赖flutter-engine改造,可以在官方sdk上运行。
  • 上一节中已经看到可以使用内存指针实现bytes内存共享,bitmap在内存中也是pixels bytes,如果能拿到这块内存指针,那么bitmap内存共享也不是问题。
  • 如何拿到?android jni 提供了 AndroidBitmap_lockPixels 可以帮助我们实现这一功能。
//android jni代码。java bitmap object 转 pixels内存指针
Java_com_xxxx_JniInterface_native_1getBitmapPixelDataMemoryPtr(
        JNIEnv *env, jclass clazz, jobject bitmap) {
    AndroidBitmapInfo bitmapInfo;
    int ret;
    if ((ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return 0;
    }
    // 读取 bitmap 的像素数据块到 native 内存地址
    void *addPtr;
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &addPtr)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return 0;
    }
    //unlock,保证不因这里获取地址导致bitmap被锁定
    AndroidBitmap_unlockPixels(env, bitmap);
    return (jlong)addPtr;
}
//android java调用,并返回给flutter内存指针信息
long address = JniInterface.getBitmapPixelDataMemoryPtr(bitmap);
if (address != 0) {
    Map reply = new HashMap();
    reply.put("pixelsDataAddress", address);
    reply.put("pixelsDataWidth", bitmap.getWidth());
    reply.put("pixelsDataHeight", bitmap.getHeight());
    //cacheObj(nativeImageID, bitmap);//需要缓存一下,以保证flutter使用时没有被释放
    result.success(reply);
}
//flutter 侧使用
Pointer pointer = Pointer.fromAddress(pixelsDataAddress); //address是内存地址
int bytesCount = pixelsDataHeight * pixelsDataWidth * 4;
Uint8List bytes = pointer.asExternalTypedData(count: bytesCount);//pixels bytes data
ui.PixelFormat format = ui.PixelFormat.rgba8888;
  • flutter如何使用像素数据,这里的bytes是解码后的像素数据,不能使用Image.memory展示, Image.memory接收的是未解码数据。但flutter提供了另一个接口 dart:ui.decodeImageFromPixels
  • 这里提供了flutter显示图片pixels数据的例子 PixelMemoryImage ,同样做好是重写加入对bitmap的引用和释放逻辑。ui.decodeImageFromPixels 之前去获取指针,引用+1,engine处理完回调后引用-1。

5. 修改flutter engine直接读取native内置其他assets资源方式

  • 查看flutter读取flutter添加的assets资源流程,即 Image.asset 调用流程,可以发现,flutter是在engine层通过android jni结构直接读取的flutter_assets资源。那是否可以改造让其也可以读取native已有的内置资源呢?
  • Image.asset流程:
Image.asset
  AssetImage
    AssetBundleImageProvider#load
       AssetBundleImageProvider#_loadAsync
          asset_bundle.dart#PlatformAssetBundle#load
             defaultBinaryMessenger.send('flutter/assets', asset_name)
                engine.cc#HandlePlatformMessage  //flutter engine层
                   engine.cc#HandleAssetPlatformMessage
                      asset_manager_->GetAsMapping(asset_name)//返回mapping,包含内存指针和size
                         apk_asset_provider.cc#APKAssetProvider::GetAsMapping
//apk_asset_provider.cc中的实现
std::unique_ptr APKAssetProvider::GetAsMapping(
    const std::string& asset_name) const {
  std::stringstream ss;
  ss << directory_.c_str() << "/" << asset_name;  //dir是flutter_assets,asset_name是flutter层开发指定,合起来flutter_assets/asset_name
  //这是flutter侧添加的资源在android apk中的位置,打包在native assets目录下
  //AAssetManager_open是android jni接口,位于android/asset_manager_jni.h
  AAsset* asset =
      AAssetManager_open(assetManager_, ss.str().c_str(), AASSET_MODE_BUFFER);
  if (!asset) {
    return nullptr;
  }
  return std::make_unique(asset);//最终通过AAsset_getBuffer读取数据
}
  • flutter 是通过在engine层调用asset_manager读取flutter侧添加的资源,其限定了读取apk assets目录下flutter_assets下的资源。所以flutter默认api不能支持读取native原生添加的assets或drawable资源。分析apk可以看到如下的结构:
image.png
  • 如果对APKAssetProvider::GetAsMapping 进行如下简单改造,可以让其支持 ../ 格式,就能读取flutter_assets之外的assets资源
std::unique_ptr APKAssetProvider::GetAsMapping(
    const std::string& asset_name) const {
  std::stringstream ss;
  if(asset_name.size() > 3 && asset_name.compare(0, 3, "../") == 0){
    ss << asset_name.substr(3);//支持 ../ 读取native assets下资源
  } else {
    ss << directory_.c_str() << "/" << asset_name;//默认方式
  }
  AAsset* asset =
      AAssetManager_open(assetManager_, ss.str().c_str(), AASSET_MODE_BUFFER);
  if (!asset) {
    return nullptr;
  }
  return std::make_unique(asset);
}
  • flutter使用如下:定位到apk/assets/earth.jpg图片
Image image = Image.asset(
  "../earth.jpg", //默认../格式是中不到资源的,报错
  fit: BoxFit.fill,
  width: 200,
);
  • 这种方式对于跨平台开发并不友好,两端资源位置路径可能不一致,需要分平台开发。
  • 对于如何修改engine直接读取android native的drawable图片资源,暂时还没有找到比合适的方法,因为读取drawable资源的android实现是放在AssetManager2.cpp ,并没有对应的jni接口,asset_manager jni接口列表 。
  • 参考AssetInputStream在c++层的使用方式,配合android AssetManager.java的nativeOpenNonAsset获取Asset指针,在engine层转换成Asset* 用jni接口读取看上去可行,就是有点复杂,暂时没有场景值得这么去做。
  • 编译和应用flutter engine,可以参考链接 Flutter Engine编译和应用介绍

最后,总结一下

  • 本文提供了5中flutter共享native资源的方式,在flutter+native混合栈开发中可能会有一款适合你 : ) 。
    1. 通过channel传bytes流方式
    1. 通过写文件中转方式
    1. 内存指针方式,可以避免数据传递,但需要注意维护native的内存数据的引用和释放
    1. 针对bimap的内存指针共享方式
    1. 尝试从修改engine的方式支持flutter直接读取native assets资源,但还不支持res/drawable资源。

最最后感谢阅读~~

参考资料链接

  • 深入理解Flutter的Platform Channel机制
  • Platform channel data types support and codecs
  • Android NDK:如何使用原生代码共享原始数据?
  • 万万没想到——flutter这样外接纹理,咸鱼纹理方式共享图片
  • 跨平台技术趋势及字节跳动 Flutter 架构实践
  • Android NDK bitmap API androidbitmap_lockpixels
  • flutter dart ui.decodeImageFromPixels
  • Flutter显示pixels data图片,PixelMemoryImage
  • Android NDK asset API
  • Android源代码搜索阅读工具,支持跳转,解放磁盘,超级棒
  • Flutter Engine编译和应用介绍
  • 咸鱼Flutter专题栏目
  • 字节跳动Flutter分享专题

你可能感兴趣的:(flutter共享native资源的多种姿(fang)势(shi))