Android效能優化,Startalk會話頁GIF記憶體優化實踐

Startalk星語發表於2019-02-19

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共享時間線檢視

Android效能優化,Startalk會話頁GIF記憶體優化實踐

                                  (圖片來自developer.android.com)

Android Profiler現在顯示一個共享時間線檢視,其中包括一個帶有CPU、MEMORY和NETWORK使用情況實時圖表的時間線。該視窗還包括時間線縮放控制元件 3,用於跳轉到實時更新的按鈕 4 以及顯示活動狀態,使用者輸入事件和螢幕旋轉事件 5 的事件時間線,1 是連線的裝置,2當前所選程式。

3.問題分析

瞭解了Android Profiler後,我們通過MEMORY時間線看一下在我們進入會話頁後&當會話頁有較多較大的GIF時我們的IM APP記憶體佔用對比情況,首先看我們剛進入沒有GIF的會話頁記憶體佔用如下

Android效能優化,Startalk會話頁GIF記憶體優化實踐

說明:

•Total:當前所選程式佔用的總記憶體大小

•Java:從Java或Kotlin程式碼分配的物件的記憶體

•Native:從C或C ++程式碼分配的物件的記憶體

•Graphics:用於圖形緩衝區佇列的記憶體

•Stack:應用程式中堆疊和Java堆疊使用的記憶體,這通常與您的應用執行的執行緒數有關

•Code:應用程式使用程式碼和資源的記憶體,例如dex位元組碼,優化或編譯的dex程式碼,.so庫和字型

•Others:應用程式使用的記憶體,系統不知道如何分類

接著我們看一下在我進入一個Gif比較多(個別Gif圖很大20M左右)會話後,滑動會話頁後記憶體佔用如下圖:

Android效能優化,Startalk會話頁GIF記憶體優化實踐

從MEMORY時間線可以看到Native增加了將近70M,並且在顯示之前已經展示過的Gif時Native記憶體同樣還是在增長,結束會話頁後記憶體一直保持在一定值沒有下降。通過上面的分析得出的結論是在載入Gif的時候程式不斷的在申請記憶體,前面背景中提到我們的Gif時Glide+FrameSequenceDrawable載入的,所以C&C++申請記憶體的操作於應該時在FrameSequence中,看一下FrameSequenceDrawable原始碼,發現這三個native 申請記憶體方法。

Android效能優化,Startalk會話頁GIF記憶體優化實踐

再看一下我們程式裡面是如何使用的

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檢視上面同樣的操作記憶體情況

Android效能優化,Startalk會話頁GIF記憶體優化實踐

在我退出會話頁若干秒或者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…


相關文章