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…