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<String, Object> reply = new HashMap<String, Object>();
      reply.put("result", "haha2");
      result.success(reply);//返回值
    }
  }
//flutter dart 调用
const MethodChannel _channel = const MethodChannel("your_channel_name");
final Map<dynamic, dynamic> 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<String, Object> reply = new HashMap<String, Object>();
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<Uint8> pointer = Pointer<Uint8>.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<long>::FromDart(Dart_GetNativeArgument(args, 0));
void* ptr = reinterpret_cast<char*>(address);
const int bytes_size = tonic::DartConverter<int>::FromDart(Dart_GetNativeArgument(args, 1));
tonic::DartInvoke(callback_handle,{
    tonic::DartConverter<tonic::Uint8List>::ToDart(reinterpret_cast<const u_int8_t*>(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<String, Object> reply = new HashMap<String, Object>();
    reply.put("pixelsDataAddress", address);
    reply.put("pixelsDataWidth", bitmap.getWidth());
    reply.put("pixelsDataHeight", bitmap.getHeight());
    //cacheObj(nativeImageID, bitmap);//需要缓存一下,以保证flutter使用时没有被释放
    result.success(reply);
}
//flutter 侧使用
Pointer<Uint8> pointer = Pointer<Uint8>.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<fml::Mapping> 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<APKAssetMapping>(asset);//最终通过AAsset_getBuffer读取数据
}
  • flutter 是通过在engine层调用asset_manager读取flutter侧添加的资源,其限定了读取apk assets目录下flutter_assets下的资源。所以flutter默认api不能支持读取native原生添加的assets或drawable资源。分析apk可以看到如下的结构:
image.png
image.png
  • 如果对APKAssetProvider::GetAsMapping 进行如下简单改造,可以让其支持 ../ 格式,就能读取flutter_assets之外的assets资源
std::unique_ptr<fml::Mapping> 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<APKAssetMapping>(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资源。

最最后感谢阅读~~

参考资料链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容

  • 在 2019 年,Flutter 推出了多个正式版本,支持的终端越来越多,使用的项目也越来越多。Flutter 正...
    zhx喜籽阅读 4,023评论 0 9
  • 一、初识flutter image 在讲解源码之前,我们先看下面几个例子,回顾一下flutter加载图片资源的方式...
    五月_f6d4阅读 11,191评论 3 23
  • 前几天朋友闲聊时偶然说了一句话“小心把你带进臭水沟里去。”随后又无聊的加了句“这两天下雨了,沟里水涨了,小心被淹到...
    黑色的水阅读 333评论 0 0
  • 长街长,烟花繁 槐火纷乱,寒烟微凉, 挑灯回看: 黄泉,紫陌,碧落,红尘 ——灵魂空旷 若水犹离,...
    白落落阅读 880评论 0 3
  • 华灯束缚,长街光转,银汉飞星弄舞。 他年日盼夜相逢,可添是、别离天妒。 佳期如故,风光流里,莫道鹊桥住处。 来年若...
    浪哥你个浪阅读 299评论 0 1