天天看點

燃燒我的卡路裡 ---- Flutter瘦記憶體瘦包之圖檔元件

作者:閑魚技術-爐軍

背景

在電商類APP裡,圖檔到現在為止仍然是最重要的資訊承載媒介,不得不說逛淘寶的過程,其實就是一個看圖檔的過程。而商品詳情頁中的圖檔,通常是頁面中記憶體占用最多的内容,占用了整個頁面記憶體的超過 50%。

閑魚在Flutter化的過程中,選擇了商品詳情頁作為第一個落地的場景。通過多版本的疊代完善,基于Flutter的詳情頁已經在閑魚穩定運作。然而正因為詳情頁的圖檔量大,導緻Flutter裡圖檔相關的問題一直揮之不去。

1:記憶體問題 --- 連續push flutter界面記憶體累積

2:安裝包問題 --- 過渡時期兩份重複資源檔案。

3:尋址緩存問題 --- 原有的尋址緩存政策無法複用。

4:圖檔複用問題 --- Native和Flutter重複下載下傳相同圖檔。

解決方案----FXTexImage_V1

為了解決這些問題,我們嘗試着尋找一種新的思路,一種能夠将flutter與native串聯起來的思路。而之前做視訊播放器的方案給了我們啟發。

熟悉Flutter的同學應該都知道,Flutter的視訊元件是基于一個Flutter提供的一個叫“外接紋理”的技術實作的,關于flutter外接紋理,本人另外有一篇文章有更詳細的論述,這裡不再贅述。

https://mp.weixin.qq.com/s/KkCsBvnRayvpXdI35J3fnw

我們将每一張圖檔假想成一個:靜态的視訊。圖檔的内容由一個external_texture來負責顯示,而這個external_texture則由native端提供具體的渲染資料。

燃燒我的卡路裡 ---- Flutter瘦記憶體瘦包之圖檔元件

通過這種方案,我們便可以通過external_texture這座橋梁,将flutter作為native端圖檔的一個最終展示場所。而所有的下載下傳、緩存、裁剪等邏輯都可以複用原來的native圖檔庫。

基于這個基本架構,我們形成了我們第一版本的圖檔渲染元件:FXTexImage----V1。這個元件很好的解決了Flutter引入的安裝包、圖檔緩存、圖檔複用等問題。

但是圖檔最大的問題:記憶體問題,并沒有得到解決。

記憶體優化----FXTexImage_V2

為了使用者體驗,通常會有連續push若幹個界面的場景(比如閑魚的詳情頁,點選底部的推薦清單,可以一直往下push新的詳情頁),這種場景下,每一個界面都有大量的圖檔展示。是以在引入flutter以後,閑魚在iPhone 6P等機型上通常隻能push10個左右詳情頁就挂了。

在考慮到在顯示過程中,真正使用者可見的頁面,其實隻有目前棧頂的兩個頁面,基于這個特征我們就做了優化邏輯:

1:在push詳情頁過程中,我們隻保留了目前展示頁和目前頁的前一頁的圖檔資源,而之前的資源全部都做了釋放(隻是圖檔資源的釋放,整個頁面還有頁面中的其他元素還是做了保留)。

2:為了做到使用者無感覺,我們在pop過程中,會預先去加載目前界面下一個界面的圖檔資源。

燃燒我的卡路裡 ---- Flutter瘦記憶體瘦包之圖檔元件

通過這種方式,理論上我們可以釋放掉不可見的資源,進而保證在持續Push界面過程中記憶體緩慢增長,但是實踐過程中發現記憶體仍然持續增長。

經過排查,我們發現flutter 1.0版本以及0.8.2版本裡,SurfaceTextureRegistry提供了release方法,這裡将會把建立的SurfaceTexture進行釋放。然而測試過程中發現,單單對SurfaceTexture釋放,并沒有完全釋放記憶體,當反複建立對象時仍然會閃退。為此,我們在AndroidExternalTextureGL的析構函數中增加了紋理的釋放glDeleteTextures邏輯。

然而,AndroidExternalTextureGL的析構是在flutter的GPU線程調用的,而external_texture的release方法通常是在主線程,也就是PlatForm線程調用的。不同線程調用的問題就是會導緻一個詭異的問題:

燃燒我的卡路裡 ---- Flutter瘦記憶體瘦包之圖檔元件

推測是不同線程釋放的邏輯影響了GL環境,導緻文字渲染出了問題。

是以,為了解決該問題,我們删除了SurfaceTextureRegistry的release方法裡面SufaceTexture的釋放邏輯,并且将SurfaceTexture的釋放,放到AndroidExternalTextureGL析構階段,通過Jni調用java方法實作資源釋放。

AndroidExternalTextureGL::~AndroidExternalTextureGL(){
   if (state_ == AttachmentState::attached) {
      Detach();
      if (texture_name_ != 0)
      {
          glDeleteTextures(1, &texture_name_);
          texture_name_ = 0;
      }
    }
   Release();
   state_ = AttachmentState::detached;
}           
void AndroidExternalTextureGL::Release() {
    JNIEnv* env = fml::jni::AttachCurrentThread();
    fml::jni::ScopedJavaLocalRef<jobject> surfaceTexture =
    surface_texture_.get(env);
    if (!surfaceTexture.is_null()) {
        SurfaceTextureRelease(env, surfaceTexture.obj());
    }
}           
void SurfaceTextureRelease(JNIEnv* env, jobject obj) {
   env->CallVoidMethod(obj, g_release_method);
   FML_CHECK(CheckException(env));
}           
g_release_method = env->GetMethodID(g_surface_texture_class->obj(), "release", "()V");           

CPU優化----FXTexImage_V3

通過外界紋理渲染圖檔+不可見頁面資源釋放,我們解決了上述提出的一系列問題,但是又引入了新的問題:CPU偏高,滑動幀率偏低。通過測試,在詳情頁滑動過程中,IOS和Android的CPU都比Flutter原生元件高10%以上,這個顯然無法應用。

經過排查,發現CPU高的原因是:

IOS端: iOS的IOSExternalTextureGL模型是一個拉資料的模型,native端register一個CVPixelBuffer的生産者,當需要繪制時,都會調用一次這個生産者的copyPixelbuffer方法去拉一次資料。然後将拉到的CVPixelBuffer對象轉換成GPU Texture。這裡每一次轉換都換造成CPU較大開銷。

并且這種拉資料的機制就要求這個生産者的必須一直保留着這個CVPixelBuffer對象(否則界面重刷以後,圖檔區域就顯示白屏)。

Android端: android 的資料存儲在SurfaceTexture中。每一次external_texture layer需要繪制時候都會從SurfaceTexture中去update 資料到紋理中,由于SurfaceTexture使用基于EGLImage共享記憶體,是以雖然沒有雙份記憶體的問題,但是每一次update 都會帶來較大的CPU開銷。

在之前外接紋理的文章中,我們提出了一種新的基于共享上下文的外接紋理方案。并在我們視訊的拍攝和編輯中得到了很好的應用。該方案當初提出來,是為了解決視訊資料從CPU -> GPU -> CPU -> GPU 輸送的問題而提出來的。

但是在圖檔這個場景下, 新的外接紋理方案下,一張圖檔在native端加載完成以後,立刻被轉換成一個OpenGL的Texture,然後圖檔的資源馬上被釋放。當界面重新整理時,對于同一張圖檔的重新渲染,IOSExternalTextureGL不需要再去做将資料(CVPixelBuffer或者SurfaceTexture)轉換到Texture的邏輯,而是直接使用之前建立好的Texture。

經過這一步優化,我們很好的限制了iOS的CPU和記憶體,Android的CPU。

通過測試對比,V3版本的圖檔元件,相比于Flutter原生圖檔元件,在詳情頁正常滑動操作過程中,平均CPU高出3%左右,雖然仍差于原生元件,單相對是可以接受的。

結果

記憶體: 基于新圖檔元件,我們很好的限制住了連續push 下的記憶體增長速度,順利的将iPhone 6P上的詳情頁push 最大數量從10個增加到了30個以上。

在同一線上版本中,我們通過控制ABTest開關,測試新的圖檔渲染方案和Flutter自帶圖檔元件方案的Abort率,發現新圖檔元件下閑魚的Abort率降低20%。

安裝包: 新元件下,所有的資源元件與原來native資源共用,所有flutter期間新引入的資源出了gif圖,全部删除,減少安裝包900k+。并且後續可以不用繼續新增。

尋址政策: 複用native圖檔元件,基于阿裡系自己的圖檔下載下傳元件,這樣可以做到随着集團元件更新版本,相容版本過程中各種新的尋址方式和圖檔格式。

圖檔複用:複用native圖檔元件,當圖檔位址命中緩存,可直接緩存加載,尺寸不一緻時可以預先傳回緩存圖同時加載大圖,這樣大大增強詳情頁大圖預覽的浏覽體驗。

遺留問題

圖檔元件已經在閑魚上全量部署,然而還是有一些問題沒有得到很好的解決,上文提到過CPU比原生圖檔元件高3%左右,雖然使用者沒有感官體驗,但是還是有優化空間。

還有就是Flutter針對ExternalTexture的紋理渲染時沒有開啟抗鋸齒,導緻小圖在大區域渲染時比原生元件效果要差。這裡還需要繼續排查原因。

最後,FXTexImage元件還在持續優化中,當解決上述遺留問題以後便會在Github上開源。

繼續閱讀