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閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容

  • 在 2019 年,Flutter 推出了多個正式版本,支持的終端越來越多,使用的項目也越來越多。Flutter 正...
    zhx喜籽閱讀 4,057評論 0 9
  • 一、初識flutter image 在講解源碼之前,我們先看下面幾個例子,回顧一下flutter加載圖片資源的方式...
    五月_f6d4閱讀 11,223評論 3 23
  • 前幾天朋友閑聊時偶然說了一句話“小心把你帶進臭水溝里去。”隨后又無聊的加了句“這兩天下雨了,溝里水漲了,小心被淹到...
    黑色的水閱讀 337評論 0 0
  • 長街長,煙花繁 槐火紛亂,寒煙微涼, 挑燈回看: 黃泉,紫陌,碧落,紅塵 ——靈魂空曠 若水猶離,...
    白落落閱讀 893評論 0 3
  • 華燈束縛,長街光轉,銀漢飛星弄舞。 他年日盼夜相逢,可添是、別離天妒。 佳期如故,風光流里,莫道鵲橋住處。 來年若...
    浪哥你個浪閱讀 305評論 0 1