Android效能優化,Startalk會話頁GIF記憶體優化實踐
Startalk(星語)現已在GitHub上全面開源,邀君一起添磚加瓦~~~
Startalk(星語)官方網站: im.qunar.com/new/#/home
Startalk(星語)開原始碼地址: github.com/qunarcorp/q…
***********************************************************************************
1.背景
做為IM的核心部分,會話頁的展示和流暢度十分影響使用者體驗,本次優化的內容正是會話裡面的Gif圖片的展示,Android原生是沒有View直接支援Gif圖片播放的,Startalk使用Glide+FrameSequenceDrawable實現對Gif的支援,但是在使用過程中發現了一些問題,例如在一個會話裡面Gif圖過多過大,IM在執行一段時間後記憶體吃緊,造成頁面開始卡頓,甚至OOM等問題,為了解決這個問題我們通過Android Studio 3.0開始內建的Android Profiler工具來檢測Memory的變化,從而發現問題所在並實施優化。
2.Android Profiler介紹
首先看一下Android Profiler共享時間線檢視
(圖片來自developer.android.com)
Android Profiler現在顯示一個共享時間線檢視,其中包括一個帶有CPU、MEMORY和NETWORK使用情況實時圖表的時間線。該視窗還包括時間線縮放控制元件 3 ,用於跳轉到實時更新的按鈕 4 以及顯示活動狀態,使用者輸入事件和螢幕旋轉事件 5 的事件時間線, 1 是連線的裝置, 2 當前所選程序。
3.問題分析
瞭解了Android Profiler後,我們通過MEMORY時間線看一下在我們進入會話頁後&當會話頁有較多較大的GIF時我們的IM APP記憶體佔用對比情況,首先看我們剛進入沒有GIF的會話頁記憶體佔用如下
說明:
•Total: 當前所選程序佔用的總記憶體大小
•Java: 從Java或Kotlin程式碼分配的物件的記憶體
•Native :從C或C ++程式碼分配的物件的記憶體
•Graphics :用於圖形緩衝區佇列的記憶體
•Stack :應用程式中堆疊和Java堆疊使用的記憶體,這通常與您的應用執行的執行緒數有關
•Code :應用程式使用程式碼和資源的記憶體,例如dex位元組碼,優化或編譯的dex程式碼,.so庫和字型
•Others: 應用程式使用的記憶體,系統不知道如何分類
接著我們看一下在我進入一個Gif比較多(個別Gif圖很大20M左右)會話後,滑動會話頁後記憶體佔用如下圖:
從MEMORY時間線可以看到Native增加了將近70M, 並且在顯示之前已經展示過的Gif時Native記憶體同樣還是在增長,結束會話頁後記憶體一直保持在一定值沒有下降 。通過上面的分析得出的結論是在載入Gif的時候程式不斷的在申請記憶體,前面背景中提到我們的Gif時Glide+FrameSequenceDrawable載入的,所以C&C++申請記憶體的操作於應該時在FrameSequence中,看一下FrameSequenceDrawable原始碼,發現這三個native 申請記憶體方法。
再看一下我們程式裡面是如何使用的
Glide.with(context) .load(url) .asGif() .toBytes() .diskCacheStrategy(DiskCacheStrategy.ALL) .dontAnimate() .into(new ViewTarget<LoadingImgView, byte[]>(mLoadingImgView) { @Override public void onResourceReady(byte[] resource, GlideAnimation<? super byte[]> glideAnimation) { FrameSequence fs = FrameSequence.decodeByteArray(resource); FrameSequenceDrawable drawable = new FrameSequenceDrawable(fs); view.setImageDrawable(drawable); } });複製程式碼
這段程式碼是在會話列表的adapter中執行的,FrameSequence.decodeByteArray(resource)每次這個view展示的時候都會被呼叫到,也就意味著每次都會申請建立 byte[] resource長度大小的記憶體,這也是重複顯示同一個Gif時記憶體不斷增加的原因。
接下來我們對這段程式碼進行優化,使用Cache策略(LruCache)確保同一個url對應一個FrameSequenceDrawable。
Glide.with(context) .load(url) .asGif() .toBytes() .diskCacheStrategy(DiskCacheStrategy.ALL)//快取全尺寸 .dontAnimate() .into(new ViewTarget<LoadingImgView, byte[]>(mLoadingImgView) { @Override public void onResourceReady(byte[] resource, GlideAnimation<? super byte[]> glideAnimation) { WeakReference<Parcelable> cached = new WeakReference<>(MemoryCache.getMemoryCache(url)); if(cached.get() == null){ FrameSequence fs = FrameSequence.decodeByteArray(resource); FrameSequenceDrawable drawable = new FrameSequenceDrawable(fs); drawable.setByteCount(resource.length); view.setImageDrawable(drawable); MemoryCache.addObjToMemoryCache(url,drawable); }else { if(cached.get() instanceof FrameSequenceDrawable){ FrameSequenceDrawable fsd = (FrameSequenceDrawable)cached.get(); view.setImageDrawable(fsd); } } } });複製程式碼
其中MemoryCache為LruCache封裝的工具類,同時使用了WeakReference來保證FrameSequenceDrawable更容易被回收,回收的好處是native申請的記憶體可以被銷燬釋放
protected void finalize() throws Throwable { try { if (mNativeFrameSequence != 0) nativeDestroyFrameSequence(mNativeFrameSequence); } finally { super.finalize(); } }複製程式碼
我們在Application的onTrimMemory(level)方法來清空MemoryCache裡面的快取,觸發GC(備註:onTrimMemory(level)方法會在程式記憶體吃緊的時候回撥到又不通的level級別),我們這裡設定 level >= TRIM_MEMORY_RUNNING_MODERATE,這樣在我們Home出程式的時候會被執行。
public void onTrimMemory(int level) { super.onTrimMemory(level); if (level >= TRIM_MEMORY_RUNNING_MODERATE) { QIMSdk.getInstance().clearMemoryCache(); } }複製程式碼
然後我們重新通過Android Profiler檢視上面同樣的操作記憶體情況
在我退出會話頁若干秒或者Home出去後,Native記憶體瞬間降下來了,大概回到進會話前大小。
通過Android Profiler對記憶體的分析我們優化了Gif的記憶體消耗問題,其實通過這個工具我們還能分析出程式的不足地方,本次針對的主要是Native的記憶體部分,而我們記憶體的另一大開銷Java堆記憶體也是我們優化的重點。
問題:在分析FrameSequenceDrawable原始碼的時候我們發現Android7.0及以上當view隱藏的時候回撥不到 setVisible方法,只做了臨時處理,有知道的小夥伴可以評論回覆我。
public boolean setVisible(boolean visible, boolean restart) { boolean changed = super.setVisible(visible, restart); //TODO 7.0及以上特殊處理 暫時沒找到其他好辦法 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ if(visible && !isRunning() && !restart){ restart = true; } } if (!visible) { super.setVisible(visible, restart); stop(); } else if (restart || changed) { stop(); start(); } return changed; }複製程式碼
****************************************************************************************
Startalk(星語)官方網站: im.qunar.com/new/#/home
Startalk(星語)開原始碼地址: github.com/qunarcorp/q…