导语:flutter+native混合开发过程中,flutter可能需要共享native已有的资源,如app内置资源、下载好的数据、已缓存的内存数据等,这里介绍几种flutter共享native资源的方式,包括通常的channel、file,以及指针方式实现内存共享。以安卓为例。
使用flutter开发全新app时,资源一般是放置在flutter工程中,ios、android两端共享。但是在已有app中集成flutter进行flutter+native的混合开发过程中,为了能复用app已有资源,flutter经常需要向native拿取这些资源,如已内置的图片、文件等。本文主要介绍几种flutter向native拿取资源的方式。以android为例。
目录
- channel bytes流传输方式
- 文件路径方式
- 内存指针共享方式
- bitmap内存指针共享
- 修改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可以看到如下的结构:
- 如果对
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混合栈开发中可能会有一款适合你 : ) 。
-
- 通过channel传bytes流方式
-
- 通过写文件中转方式
-
- 内存指针方式,可以避免数据传递,但需要注意维护native的内存数据的引用和释放
-
- 针对bimap的内存指针共享方式
-
- 尝试从修改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分享专题