導(dǎo)語:flutter+native混合開發(fā)過程中,flutter可能需要共享native已有的資源,如app內(nèi)置資源、下載好的數(shù)據(jù)拘鞋、已緩存的內(nèi)存數(shù)據(jù)等,這里介紹幾種flutter共享native資源的方式矢门,包括通常的channel盆色、file,以及指針方式實(shí)現(xiàn)內(nèi)存共享祟剔。以安卓為例隔躲。
使用flutter開發(fā)全新app時,資源一般是放置在flutter工程中物延,ios宣旱、android兩端共享。但是在已有app中集成flutter進(jìn)行flutter+native的混合開發(fā)過程中叛薯,為了能復(fù)用app已有資源浑吟,flutter經(jīng)常需要向native拿取這些資源,如已內(nèi)置的圖片耗溜、文件等组力。本文主要介紹幾種flutter向native拿取資源的方式。以android為例抖拴。
目錄
- channel bytes流傳輸方式
- 文件路徑方式
- 內(nèi)存指針共享方式
- bitmap內(nèi)存指針共享
- 修改flutter engine直接讀取native內(nèi)置其他assets資源方式
先上小菜燎字,flutter如何與native進(jìn)行通信?
- flutter提供了platform channel與native進(jìn)行通信阿宅,官方介紹 , 別人家的原理剖析候衍。
- flutter、native雙方以channel作為橋梁家夺,以channel name作為標(biāo)識脱柱,將調(diào)用轉(zhuǎn)到對方指定代碼。
- 在native側(cè)注冊監(jiān)聽拉馋,等待flutter調(diào)用榨为,通過channel將native信息返回給flutter。
//android java監(jiān)聽
final MethodChannel channel = new MethodChannel(flutterView, "your_method_name");
channel.setMethodCallHandler(new MethodCallHandler() {//注冊監(jiān)聽
@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 調(diào)用
const MethodChannel _channel = const MethodChannel("your_channel_name");
final Map<dynamic, dynamic> reply = await _channel.invokeMapMethod('your_method_name', {
"arg1" : "haha"
});
- 同樣煌茴,也可以在flutter注冊監(jiān)聽随闺,等待native調(diào)用,通過channel將flutter信息傳遞給native蔓腐。
//flutter dart監(jiān)聽
const MethodChannel _channel = const MethodChannel("your_channel_name");
_channel.setMethodCallHandler((methodCall) async{//注冊監(jiān)聽
if(methodCall.method == "your_method_name"){
return "haha";//返回
}
return null;
});
//android java調(diào)用
new MethodChannel(flutterView, "your_channel_name")
.invokeMethod("your_method_name", your_args, new MethodChannel.Result(){//調(diào)用
@Override
public void success(Object o) {//返回值
}
@Override
public void error(String s, String s1, Object o) {
}
@Override
public void notImplemented() {
}
});
下面進(jìn)入正題
1. channel bytes流傳輸方式
- channel上可以傳遞多種數(shù)據(jù)格式矩乐,本質(zhì)上也都是bytes流,這種方式是把數(shù)據(jù)以bytes流方式通過channel傳給flutter回论。
- 例如native通過bytes流把native內(nèi)置drawable圖片傳給flutter散罕。
- flutter沒有直接的api可以讀取android native內(nèi)置的drawable、asset資源傀蓉,flutter只支持直接讀取在flutter側(cè)添加的flutter_assets資源欧漱。所以bytes流方式可以幫助實(shí)現(xiàn)對這些native內(nèi)置資源的訪問。
//android java側(cè)讀取資源葬燎,得到byte[]误甚,回傳給flutter
//flutter method call resName => resId
InputStream inputStream = context.getResources().openRawResource(resId);
//從inputStream種讀取資源,轉(zhuǎn)成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調(diào)用谱净,拿取byte[]窑邦。在flutter 側(cè) byte[]對應(yīng)Uint8List
Uint8List data = await _channel.invokeMethod('getNativeImage', {
"imageName" : "xxx",
});
//flutter Image.memory api 可以把這些Uint8List/byte[]展示成圖像
2. 文件路徑方式
- android apk內(nèi)置資源組織方式使得內(nèi)置圖片/文件在flutter側(cè)不能以file方式直接讀取,因?yàn)檫@些內(nèi)置資源是以數(shù)據(jù)塊方式存放在apk這個大文件中的一片段上壕探,通過android系統(tǒng)的assset_manager來管理和讀取冈钦。
- 不過可以通過app緩存目錄來中轉(zhuǎn),flutter需要時李请,native通過系統(tǒng)接口讀取并寫入到app緩存目錄or sdcard派继,告知flutter文件path, flutter以文件方式訪問。(PS: 在內(nèi)置資源沒有更新時可以不必重新寫入)
//android java讀取內(nèi)置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();//刷新緩沖區(qū)
inputStream.close();
fos.close();
//return outFile path
//flutter 拿取到文件path 以 Image.file 展示圖片
3. 內(nèi)存指針共享方式
- 在native讀取數(shù)據(jù)轉(zhuǎn)成byte[]后捻艳,如何傳輸給flutter驾窟,除上面兩種方式,還可以通過內(nèi)存指針共享方式认轨,把native側(cè)數(shù)據(jù)指針地址和length傳遞給flutter绅络,flutter依據(jù)內(nèi)存指針地址和length讀取處理數(shù)據(jù)。
- flutter是運(yùn)行于native所封裝的環(huán)境中嘁字,在同一個進(jìn)程恩急,內(nèi)存地址空間并沒有隔離,可以共享內(nèi)存空間纪蜒。但這里有兩個問題需要解決衷恭,由于java和dart語言中并沒有像c/c++那樣的指針用法,需要解決:1)在android java中拿到內(nèi)存指針傳給flutter dart纯续;2)在flutter dart中把指針轉(zhuǎn)換成dart數(shù)據(jù)結(jié)構(gòu)使用随珠。
- 1)如何拿到byte[]內(nèi)存指針灭袁?通過jni方式
//android java側(cè)拿取byte[]指針
jbyte *cData = env->GetByteArrayElements(bytes, &isCopy);
- 因?yàn)閖ava byte[]是在java堆上申請的,根據(jù)不同系統(tǒng)實(shí)現(xiàn)窗看,這種方式可能會導(dǎo)致數(shù)據(jù)在jni被復(fù)制一份茸歧,產(chǎn)生更多的內(nèi)存增量,參考NDK開發(fā)指導(dǎo):如何使用原生代碼共享原始數(shù)據(jù)显沈? 软瞎。推薦使用ByteBuffer.allocateDirect, 分配jni native byte[]。另外在內(nèi)存指針返回給flutter使用時拉讯,native側(cè)需要保證這份內(nèi)存數(shù)據(jù)不被回收掉涤浇,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 獲取內(nèi)存指針
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側(cè)如何使用native傳遞的指針只锭?dart:ffi
Pointer.fromAddress
(flutter>=1.9) 或 修改engine添加接口
//flutter dart 把指針轉(zhuǎn)換成dart數(shù)據(jù)結(jié)構(gòu)Uint8List
import 'dart:ffi';
Pointer<Uint8> pointer = Pointer<Uint8>.fromAddress(
rawDataAddress); //address是內(nèi)存地址
Uint8List bytes = pointer.asExternalTypedData(
count: rawDataLength);
//Uint8List bytes可以通過 Image.memory 接口顯示圖像
//建議參考MemoryImage重寫一個ImageProvider把對native內(nèi)存引用釋放羅加入
//之前調(diào)用native獲取指針,增加內(nèi)存引用計數(shù)1
PaintingBinding.instance.instantiateImageCodec(bytes) ;
//之后通知減除內(nèi)存引用1
//對于低版本不支持dart:ffi的估計是自定義engine了盖彭,可以自己添加接口纹烹,實(shí)現(xiàn)指針轉(zhuǎn)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串聯(lián)起來即可實(shí)現(xiàn)召边,指針方式的內(nèi)存共享铺呵。好處是沒有大塊數(shù)據(jù)通過channel拷貝傳遞,但需要注意內(nèi)存的引用和釋放隧熙。
4. bitmap內(nèi)存指針共享
- bitmap內(nèi)存共享與上一節(jié)相似片挂,共享的bitmap在內(nèi)存的pixel bytes。為什么要bitmap共享呢贞盯?flutter+native混合開發(fā)中音念,一些圖片已經(jīng)在native的內(nèi)存中加載了,如果flutter能夠復(fù)用這內(nèi)存躏敢,既能節(jié)省內(nèi)存闷愤,也能省去讀取文件和解碼圖片的過程,優(yōu)化性能件余。
- 網(wǎng)上也有通過紋理方式在native和flutter間進(jìn)行圖片共享的方法讥脐,這種方式需要在native維護(hù)一個GL線程,不是頻繁復(fù)用場景(如gallery/camera) 啼器,成本有點(diǎn)高旬渠。
- 字節(jié)跳動Flutter架構(gòu)實(shí)踐“圖片透傳優(yōu)化方案”一節(jié)也提出了通過改engine實(shí)現(xiàn)bitmap內(nèi)存共享,方案圖如下端壳,不過并沒有給出具體實(shí)現(xiàn)介紹告丢。
- 我們這種bitmap共享方式可以不依賴flutter-engine改造,可以在官方sdk上運(yùn)行损谦。
- 上一節(jié)中已經(jīng)看到可以使用內(nèi)存指針實(shí)現(xiàn)bytes內(nèi)存共享岖免,bitmap在內(nèi)存中也是pixels bytes岳颇,如果能拿到這塊內(nèi)存指針,那么bitmap內(nèi)存共享也不是問題觅捆。
- 如何拿到赦役?android jni 提供了 AndroidBitmap_lockPixels 可以幫助我們實(shí)現(xiàn)這一功能麻敌。
//android jni代碼栅炒。java bitmap object 轉(zhuǎn) pixels內(nèi)存指針
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 的像素數(shù)據(jù)塊到 native 內(nèi)存地址
void *addPtr;
if ((ret = AndroidBitmap_lockPixels(env, bitmap, &addPtr)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
return 0;
}
//unlock,保證不因這里獲取地址導(dǎo)致bitmap被鎖定
AndroidBitmap_unlockPixels(env, bitmap);
return (jlong)addPtr;
}
//android java調(diào)用术羔,并返回給flutter內(nèi)存指針信息
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 側(cè)使用
Pointer<Uint8> pointer = Pointer<Uint8>.fromAddress(pixelsDataAddress); //address是內(nèi)存地址
int bytesCount = pixelsDataHeight * pixelsDataWidth * 4;
Uint8List bytes = pointer.asExternalTypedData(count: bytesCount);//pixels bytes data
ui.PixelFormat format = ui.PixelFormat.rgba8888;
- flutter如何使用像素數(shù)據(jù),這里的bytes是解碼后的像素數(shù)據(jù)级历,不能使用Image.memory展示释移, Image.memory接收的是未解碼數(shù)據(jù)。但flutter提供了另一個接口 dart:ui.decodeImageFromPixels
- 這里提供了flutter顯示圖片pixels數(shù)據(jù)的例子 PixelMemoryImage 寥殖,同樣做好是重寫加入對bitmap的引用和釋放邏輯玩讳。ui.decodeImageFromPixels 之前去獲取指針,引用+1嚼贡,engine處理完回調(diào)后引用-1熏纯。
5. 修改flutter engine直接讀取native內(nèi)置其他assets資源方式
- 查看flutter讀取flutter添加的assets資源流程,即 Image.asset 調(diào)用流程粤策,可以發(fā)現(xiàn)樟澜,flutter是在engine層通過android jni結(jié)構(gòu)直接讀取的flutter_assets資源。那是否可以改造讓其也可以讀取native已有的內(nèi)置資源呢叮盘?
- 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,包含內(nèi)存指針和size
apk_asset_provider.cc#APKAssetProvider::GetAsMapping
//apk_asset_provider.cc中的實(shí)現(xiàn)
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層開發(fā)指定,合起來flutter_assets/asset_name
//這是flutter側(cè)添加的資源在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讀取數(shù)據(jù)
}
- flutter 是通過在engine層調(diào)用asset_manager讀取flutter側(cè)添加的資源毒费,其限定了讀取apk assets目錄下flutter_assets下的資源。所以flutter默認(rèn)api不能支持讀取native原生添加的assets或drawable資源愈魏。分析apk可以看到如下的結(jié)構(gòu):

image.png
- 如果對
APKAssetProvider::GetAsMapping
進(jìn)行如下簡單改造觅玻,可以讓其支持 ../ 格式,就能讀取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;//默認(rèn)方式
}
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", //默認(rèn)../格式是中不到資源的蝌戒,報錯
fit: BoxFit.fill,
width: 200,
);
- 這種方式對于跨平臺開發(fā)并不友好串塑,兩端資源位置路徑可能不一致,需要分平臺開發(fā)北苟。
- 對于如何修改engine直接讀取android native的drawable圖片資源桩匪,暫時還沒有找到比合適的方法,因?yàn)樽x取drawable資源的android實(shí)現(xiàn)是放在AssetManager2.cpp 友鼻,并沒有對應(yīng)的jni接口傻昙,asset_manager jni接口列表 闺骚。
- 參考AssetInputStream在c++層的使用方式,配合android AssetManager.java的nativeOpenNonAsset獲取Asset指針妆档,在engine層轉(zhuǎn)換成Asset* 用jni接口讀取看上去可行僻爽,就是有點(diǎn)復(fù)雜,暫時沒有場景值得這么去做贾惦。
- 編譯和應(yīng)用flutter engine胸梆,可以參考鏈接 Flutter Engine編譯和應(yīng)用介紹
最后,總結(jié)一下
- 本文提供了5中flutter共享native資源的方式须板,在flutter+native混合棧開發(fā)中可能會有一款適合你 : ) 碰镜。
- 通過channel傳bytes流方式
- 通過寫文件中轉(zhuǎn)方式
- 內(nèi)存指針方式,可以避免數(shù)據(jù)傳遞习瑰,但需要注意維護(hù)native的內(nèi)存數(shù)據(jù)的引用和釋放
- 針對bimap的內(nèi)存指針共享方式
- 嘗試從修改engine的方式支持flutter直接讀取native assets資源绪颖,但還不支持res/drawable資源。
最最后感謝閱讀~~
參考資料鏈接
- 深入理解Flutter的Platform Channel機(jī)制
- Platform channel data types support and codecs
- Android NDK:如何使用原生代碼共享原始數(shù)據(jù)甜奄?
- 萬萬沒想到——flutter這樣外接紋理柠横,咸魚紋理方式共享圖片
- 跨平臺技術(shù)趨勢及字節(jié)跳動 Flutter 架構(gòu)實(shí)踐
- Android NDK bitmap API androidbitmap_lockpixels
- flutter dart ui.decodeImageFromPixels
- Flutter顯示pixels data圖片,PixelMemoryImage
- Android NDK asset API
- Android源代碼搜索閱讀工具课兄,支持跳轉(zhuǎn)牍氛,解放磁盤,超級棒
- Flutter Engine編譯和應(yīng)用介紹
- 咸魚Flutter專題欄目
- 字節(jié)跳動Flutter分享專題