Jianwoo中的性能優(yōu)化 — 如何優(yōu)化到打開170個(gè)界面不卡頓

前言

簡物作為一個(gè)電商app,內(nèi)容最多的就是圖片,而在應(yīng)用性能優(yōu)化中,圖片又是一個(gè)非常關(guān)鍵的點(diǎn),因?yàn)樾阅軆?yōu)化涉及到兩個(gè)方面:1、繪制 2、內(nèi)存。而圖片所代表的內(nèi)存,則是電商app中比較大頭的一個(gè)優(yōu)化模塊,而在別的一些領(lǐng)域占內(nèi)存較多的可能不僅僅是圖片,還有音頻、視頻數(shù)據(jù)等等,簡物中所涉及到的內(nèi)存優(yōu)化,主要是針對(duì)圖片,當(dāng)然不僅僅是說用什么圖片加載框架,如果僅僅是這樣,那這篇文章就有點(diǎn)過于口水了

性能優(yōu)化是一個(gè)非常有的聊的話題內(nèi)容,不管是過度繪制還是內(nèi)存泄漏還是內(nèi)存溢出都有足夠你學(xué)習(xí)好幾天甚至一個(gè)月的內(nèi)容,深入到底層去了解更是有很多東西需要學(xué)習(xí),我作為寫這篇文章的作者,也有非常非常多的東西需要學(xué),如果這篇文章有什么地方講述的不對(duì),還請(qǐng)指正,本文所針對(duì)的優(yōu)化,都有實(shí)際結(jié)果作為支撐,如果你能理解我所說的那應(yīng)該會(huì)對(duì)你項(xiàng)目實(shí)際應(yīng)用有一定的幫助

簡物中的優(yōu)化過程&結(jié)果

第一階段1

  • 未做優(yōu)化能深度打開30-40個(gè)界面,由于詳情頁的圖片列表占內(nèi)存比較大,內(nèi)存會(huì)慢慢從25M左右飆升到270M直到變卡頓出現(xiàn)ANR/OOM
未處理內(nèi)存前的內(nèi)存折線飆升

其實(shí)對(duì)于首頁的不同F(xiàn)ragment已經(jīng)是懶加載,從Android Monitor的內(nèi)存占用來看,啟動(dòng)頁只占了12M(啟動(dòng)圖所占的內(nèi)存)

啟動(dòng)頁

在啟動(dòng)頁結(jié)束之后,進(jìn)入首頁只加載第一個(gè)Fragment,內(nèi)存占用是26M
首頁第一個(gè)Fragment加載

也就是我們?cè)诓患虞d其它Fragment界面的時(shí)候,首頁的內(nèi)存占用其實(shí)是不高的(這種列表頁只有不同模塊的加載,本身只可能存在過度繪制問題,不會(huì)有內(nèi)存占用過高的問題)

對(duì)于打開的界面是什么部分占用內(nèi)存過高,我們可以使用工具來監(jiān)測一下


檢測一段操作的內(nèi)存使用

通過幾次測試我們發(fā)現(xiàn),頁面中大部分占用內(nèi)存的地方都是來自于com.facebook.*的包,因?yàn)楹單锸褂玫氖荈resco,所以圖片加載正是用的它的SimpleDrawable,通過檢測我們也能看出,這個(gè)操作中最占內(nèi)存的就是圖片,那我們必須從圖片下手

第二階段:針對(duì)第一階段所出現(xiàn)的內(nèi)存飆升問題 ,做了如下處理

  • 在云存儲(chǔ)后臺(tái)設(shè)置圖片壓縮比例,在不影響圖片視覺感官的情況下降低圖片質(zhì)量
    處理結(jié)果:處理了圖片質(zhì)量后大概將內(nèi)存占用量降低到了70%,這也就是意味著,用戶可以打開更多界面,但僅僅是可以打開更多界面,在打開到45-60個(gè)界面左右的時(shí)候,APP基本就無響應(yīng)了,也就是沒有根本解決問題

第三階段 云存儲(chǔ)平臺(tái)設(shè)置圖片壓縮后并未完全解決問題,繼續(xù)往下處理

  • 對(duì)占內(nèi)存指數(shù)較高的商品詳情界面實(shí)行不可見時(shí)釋放內(nèi)存,這是實(shí)實(shí)在在能解決內(nèi)存飆升的解決方案
    分析:我們?cè)谑褂胊pp的時(shí)候其實(shí)可以發(fā)現(xiàn),在沒有內(nèi)存泄漏的前提下,我們打開兩三個(gè)商品詳情,然后依次退出,再從首頁別的列表進(jìn)入商品詳情,再退出,其實(shí)是不會(huì)造成app崩盤閃退的,為什么,因?yàn)檫@種情況下,你瀏覽完商品詳情后退出,系統(tǒng)會(huì)自動(dòng)回收資源,因?yàn)檫@個(gè)時(shí)候相當(dāng)于你的資源以及未使用,Activity釋放后,那些未被引用的對(duì)象也會(huì)相繼釋放,也就是,如果我們能釋放商品詳情的資源,那我們就能控制內(nèi)存的飆升不降了
    處理結(jié)果:在界面不可見的時(shí)候釋放內(nèi)存,效果非常顯著,從Android Monitor可以看到內(nèi)存的曲線波動(dòng),會(huì)非常有節(jié)奏的升降(注意:以下的內(nèi)存跳閘下降并非Activity退出
深層次打開界面的內(nèi)存波動(dòng)曲線圖

這里暫時(shí)只寫優(yōu)化結(jié)果,具體優(yōu)化細(xì)節(jié)以及要注意的問題在后面會(huì)有詳細(xì)的過程

第四階段

  • 其實(shí)現(xiàn)在打開深層次界面內(nèi)存飆升問題已經(jīng)解決了,但是在應(yīng)用打開app逐漸變多的時(shí)候,依然會(huì)出現(xiàn)Activity專場以及列表滑動(dòng)慢慢變的不流暢,直到出現(xiàn)ANR,內(nèi)存不是在Android Monitor已經(jīng)很有節(jié)奏的升降了嗎,但是卻依然ANR,通過Android Monitor可以看到,內(nèi)存這一欄目確實(shí)沒有飆升,但是在CPU這個(gè)欄目卻有非常大的波動(dòng),這個(gè)時(shí)候很明顯已經(jīng)不是內(nèi)存溢出的問題了,而是過度繪制問題
    處理結(jié)果:在處理完繪制問題后,性能得到了很明顯的提升,可以連續(xù)打開>100個(gè)界面無卡頓

以上都沒有寫詳細(xì)處理細(xì)節(jié),只是簡單的講述了一下流程,下面開始講解內(nèi)存泄漏、內(nèi)存溢出、以及過度繪制的問題,以及如何在Activity不可見的時(shí)候釋放內(nèi)存

注意

性能調(diào)優(yōu)是一件非常不簡單的活,尤其是在不知道什么地方出現(xiàn)問題導(dǎo)致響應(yīng)過慢或者無響應(yīng)或者內(nèi)存溢出的時(shí)候,這個(gè)時(shí)候可能需要查看log日志或者用到Traceview、Oprofile、MAT等工具來進(jìn)行檢查,這些工具能顯示出哪個(gè)函數(shù)消耗CPU時(shí)間最多,哪個(gè)對(duì)象占用內(nèi)存最高,Android Monitor僅僅是一個(gè)性能指數(shù)顯示的一個(gè)工具,只是調(diào)優(yōu)環(huán)節(jié)的一個(gè)顯示器而已
本文所講的內(nèi)存調(diào)優(yōu)并非針對(duì)毛病問題的排查,所以不會(huì)涉及到這些工具的使用,更多的是如何避免內(nèi)存泄漏、內(nèi)存溢出、過度繪制等問題,也是針對(duì)我處理過問題所做的一個(gè)筆記,在此分享出來

實(shí)踐

在優(yōu)化之前,要先弄清楚幾個(gè)問題,1:什么是內(nèi)存泄漏(memory leak) 2:什么是內(nèi)存溢出(out of memory) 3:什么是過度繪制(overdraw)、4、什么是ANR(application not responding)?,我們一邊理解這幾個(gè)問題一邊聊遇到的問題是怎么解決的

  • 1、什么是內(nèi)存泄漏
    我們使用的對(duì)象都有著他們自己的生命周期,在那些對(duì)象的生命周期完成各自的使命后我們希望它能被系統(tǒng)回收,但這個(gè)時(shí)候這個(gè)對(duì)象因?yàn)橐廊槐荒切┥芷诟L的對(duì)象持有引用而導(dǎo)致系統(tǒng)不能回收它,導(dǎo)致內(nèi)存一直被占用,這就是內(nèi)存泄漏
    為什么會(huì)出現(xiàn)不能被回收的現(xiàn)象,不能強(qiáng)制回收嗎?這就跟Java的垃圾回收機(jī)制有關(guān)了,Java的垃圾回收機(jī)制中一種算法是依據(jù)對(duì)象的引用計(jì)數(shù)器來判斷是否回收的,也就是說,如果一個(gè)對(duì)象始終被其它對(duì)象持有引用,那這個(gè)對(duì)象將無法被垃圾回收識(shí)別為垃圾,認(rèn)為這個(gè)對(duì)象還在使用,那這個(gè)時(shí)候這個(gè)對(duì)象就一直占著茅坑不拉屎,就導(dǎo)致了內(nèi)存泄漏,在我們Android開發(fā)中,什么情況下容易存在這種對(duì)象被無故引用的情況,上面其實(shí)已經(jīng)說,凡是生命周期短的被生命周期長的對(duì)象持有引用,都有可能存在內(nèi)存泄漏,我們來舉個(gè)例子

案例

public class MemoryLeakActivity extends AppCompatActivity {
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.sendMessageDelayed(Message.obtain(), 8000);
    }

    @Override
    protected void onStart() {
        super.onStart();
        finish();
    }

    Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            //@Todo
            return true;
        }
    });
}

為什么這段代碼會(huì)內(nèi)存泄漏?
首先Handler在這個(gè)Activity內(nèi)部不是靜態(tài)內(nèi)部類,導(dǎo)致Handler內(nèi)部持有外部Activity的一個(gè)引用,什么叫做持有一個(gè)引用?怎么能看出是不是持有引用?比如你在Activity內(nèi)部定義一個(gè)成員變量int a = 0;你可以在這個(gè)Handler內(nèi)部可以拿a的引用去賦值,這就是持有引用的一個(gè)表現(xiàn),那為什么在這里持有Activity的引用會(huì)導(dǎo)致內(nèi)存泄漏呢?因?yàn)樵谶@里,Handler的生命周期和Activity不一致
我們知道Activity是運(yùn)行與Android的主線程上,而主線程在創(chuàng)建的時(shí)候,內(nèi)部會(huì)自動(dòng)創(chuàng)建一個(gè)Looper對(duì)象,Looper創(chuàng)建的時(shí)候內(nèi)部會(huì)實(shí)例化一個(gè)MessageQueue(消息池),然后Looper會(huì)不斷的循環(huán)消息池,看里面有沒有消息,有消息的話就把消息拿出來處理,這就是消息隊(duì)列機(jī)制,那這跟Handler有啥關(guān)系呢?Handler內(nèi)部有一個(gè)Looper對(duì)象,在你初始化Handler的時(shí)候如果沒有在構(gòu)造函數(shù)中傳入一個(gè)Looper,那它會(huì)試圖從當(dāng)前線程取出一個(gè)Looper引用進(jìn)行賦值,也就是調(diào)用Looper.myLooper()來給當(dāng)前Looper對(duì)象賦值引用,那如果調(diào)用當(dāng)前線程沒有拿到Looper那就會(huì)拋出運(yùn)行時(shí)異常了,所以在非主線程中使用Handler時(shí),如果當(dāng)前線程沒有Looper對(duì)象,那是無法初始化Handler的,Handler離開Looper是無法使用的,那這其實(shí)也就是說明,我們的Handler內(nèi)部是持有主線程的Looper引用,所以導(dǎo)致Handler的生命周期跟隨整個(gè)主線程一致,從而導(dǎo)致和Activity的生命周期不一致,于是在我們finish Activity的時(shí)候,會(huì)導(dǎo)致Activity的資源無法使用,因?yàn)椤八€在被Handler引用著”
那針對(duì)上面那個(gè)大bug,我們要怎么改呢?

  • 1、使用靜態(tài)內(nèi)部類或外部類避開持有外部類的應(yīng)用
  • 2、如果需要傳遞使用Activity的引用,那使用弱引用
  • 3、在Activity結(jié)束時(shí)的生命周期函數(shù)釋放Handler中的消息隊(duì)列和回調(diào)
public class MemoryLeakActivity extends AppCompatActivity {

    MHandler mHandler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler = new MHandler(this);
        mHandler.sendMessageDelayed(Message.obtain(), 8000);
    }

    @Override
    protected void onStart() {
        super.onStart();
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }

    static class MHandler extends Handler{

        WeakReference<MemoryLeakActivity> mActivity;

        public MHandler(MemoryLeakActivity activity) {
            this.mActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            if(mActivity.get() != null){
                //@TODO
            }
        }
    }
}

以上是Android中內(nèi)存泄漏的一個(gè)栗子,凡是只要符合“生命周期短的對(duì)象被生命周期長的對(duì)象持有引用”的條件,均有可能發(fā)生內(nèi)存泄漏,比如Thread、AsyncTask、單例模式,當(dāng)然資源未關(guān)閉如File、Stream、Bitmap、Cursor等也會(huì)造成內(nèi)存泄漏

  • 2、什么是內(nèi)存溢出?
    內(nèi)存溢出是指程序在運(yùn)行期間所占用的內(nèi)存超出了虛擬機(jī)所分配的最大內(nèi)存,這個(gè)時(shí)候就會(huì)拋出out of memory,也就是內(nèi)存泄漏和內(nèi)存溢出并非直導(dǎo)關(guān)系,內(nèi)存泄漏雖然會(huì)導(dǎo)致對(duì)浪費(fèi)的內(nèi)存失去控制,但是如果內(nèi)存沒有超過程序所能支配的上限那也是不會(huì)內(nèi)存溢出的。怎樣的情況下會(huì)內(nèi)存溢出呢,比如你讀取一個(gè)本地大文件到內(nèi)存,只要你的文件足夠大,你又沒有做響應(yīng)的處理僅僅是讀進(jìn)內(nèi)存,那終究會(huì)有一個(gè)點(diǎn)讓你的程序內(nèi)存溢出;又比如我們?cè)谑褂胊pp的時(shí)候,不停的打開Activity,每個(gè)Activity都有很多圖片,那這個(gè)時(shí)候如果我們沒有做響應(yīng)的優(yōu)化處理,那終究也會(huì)有一個(gè)點(diǎn)你打開下一個(gè)Activity內(nèi)存溢出;再比如我們做圖片處理,如果讀取到內(nèi)存中的圖片沒有做裁剪壓縮,那也有可能在一些機(jī)型上因?yàn)閮?nèi)存分配不足而導(dǎo)致內(nèi)存溢出等等等等

  • 3、什么是過度繪制
    過度繪制是指一個(gè)像素點(diǎn)在一幀的刷新時(shí)間內(nèi)被繪制了多次,那這會(huì)導(dǎo)致什么問題呢,Android系統(tǒng)在正常的情況下回每隔16ms向系統(tǒng)發(fā)送刷新界面的信號(hào)以觸發(fā)系統(tǒng)對(duì)UI界面的渲染,在1秒(1000ms)的時(shí)間內(nèi)每隔16ms進(jìn)行一次刷新,如果刷新比較順暢那就能達(dá)到1000/16 ≈ 60幀的刷新頻率,那對(duì)于我們?nèi)庋蹃碚f是非常流暢的,但是如果因?yàn)榻缑媲短走^深,或者布局有著重疊的部分,并且重疊的部分均要繪制背景色,那這個(gè)時(shí)候就會(huì)出現(xiàn)“過度繪制”,過度繪制會(huì)導(dǎo)致CPU/GPU的性能損耗,導(dǎo)致16ms內(nèi)無法完成計(jì)算、繪制、渲染的操作,這個(gè)時(shí)候就會(huì)覺得應(yīng)用不流暢
    既然過度繪制可能會(huì)導(dǎo)致嚴(yán)重的問題,那我們?nèi)绾蝸聿榭催^度繪制情況呢?Android系統(tǒng)自帶的工具就能顯示我們的app的過度繪制情況,具體路徑在設(shè)置 ->
    開發(fā)者選項(xiàng) -> 調(diào)試GPU過度繪制 -> 顯示過度繪制區(qū)域(不同手機(jī)設(shè)置可能不一樣)

調(diào)試GPU過度繪制

那這個(gè)時(shí)候我們就可以來調(diào)試過度繪制問題了,打開APP你會(huì)發(fā)現(xiàn)大概有四種顏色的區(qū)域色塊(因?yàn)榻貓D顯示的不同色塊不好做標(biāo)注,所以從網(wǎng)上找了一張圖),從1x — 4x代表了過度繪制程度由低到高,大片紫色或者藍(lán)色說明你的布局優(yōu)化還是很不錯(cuò)的

過度繪制程度

簡物在沒有做過度繪制調(diào)試的時(shí)候也存在很明顯的過度繪制問題,在先后調(diào)試后有非常明顯的效果提升,比如 我的個(gè)人中心界面


overdraw

從左到右過度繪制的情況有很明顯的下降,那在這個(gè)過程中我做了什么呢?這里面顏色重疊比較深的區(qū)域均是因?yàn)樵O(shè)置了重復(fù)的背景色,或者底部看不到的Layout也設(shè)置了背景色,導(dǎo)致過度繪制,所以我只是依次檢測布局層次中的背景色設(shè)置,把不需要或者看不到背景色的設(shè)置背景色為以下透明色,就可以降低很多紅色區(qū)域

android:background="@color/translucent"

在這里有一個(gè)在設(shè)置主題上的一個(gè)巨坑,如果你的主題里面設(shè)置了這個(gè)參數(shù)

<item name="android:windowIsTranslucent">true</item>

這將會(huì)導(dǎo)致你的Activity在不可見的時(shí)候也加入繪制,導(dǎo)致頁面打開層次過多的時(shí)候非??D(即使內(nèi)存優(yōu)化做到了位,內(nèi)存不飆升,也會(huì)因?yàn)檫^度繪制導(dǎo)致CPU/GPU使用率過高而出現(xiàn)不流暢直至ANR)

過度繪制除了出現(xiàn)在以上問題中,還有什么地方會(huì)導(dǎo)致過度繪制?
在任何時(shí)候只要View的繪制內(nèi)容出現(xiàn)了變化,都會(huì)導(dǎo)致屏幕重新渲染,所以我們應(yīng)該盡量避免容器或者View的重新繪制、計(jì)算,這也是為什么LineaLayout會(huì)比RelativeLayout性能要高,因?yàn)镽elativeLayout的布局計(jì)算會(huì)因?yàn)閮?nèi)部的一個(gè)View的大小改變而影響到整個(gè)容器的寬高變化,從而導(dǎo)致要重新計(jì)算、繪制,同樣簡單的嵌套一個(gè)View,在LineaLayout沒有設(shè)置weight的情況下,RelativeLayout會(huì)調(diào)用兩次onMeasure,LineaLayout只調(diào)用一次,在同等嵌套層次的情況下盡量使用LineaLayout,在根布局也最好使用LineaLayout或者FrameLayout,當(dāng)然我們應(yīng)該盡量減少布局層次的潛逃,如果因?yàn)槭褂肔ineaLayout會(huì)導(dǎo)致層次加深,那可以考慮使用RelativeLayout來減少嵌套層次
另外有一個(gè)小技巧可以在這里提一下,在Activity生命周期的幾個(gè)函數(shù)中,不要在onCreate方法做初始化UI以及初始化數(shù)據(jù)的操作,onCreate只做屏幕參數(shù)以及setContentView的操作,在onStart方法里面去做UI綁定以及UI默認(rèn)數(shù)據(jù)初始化,以及界面數(shù)據(jù)初始化(不過要考慮onStart在生命周期中調(diào)用的場景)

  • 什么是ANR?
    ANR(Application not responding)其字面意思就是應(yīng)用程序無響應(yīng),ANR一般分為三類
    1:用戶輸入行為無響應(yīng),比如觸摸按鍵,控件監(jiān)聽事件(5秒內(nèi)未響應(yīng)Android會(huì)提示ANR窗口)
    2:BroadcastReceiver的onReciver方法未響應(yīng)(10秒時(shí)間未響應(yīng)會(huì)提示ANR)
    3:Service處理超時(shí)未響應(yīng)(20秒內(nèi))

以上三類情況,1是比較常見的,2、3可能相對(duì)較少
那什么情況下會(huì)導(dǎo)致以上情況ANR呢?可能是
1、主線程執(zhí)行了耗時(shí)操作
2、BroadcastReceiver中onReceive做耗時(shí)操作
3、主線程執(zhí)行IO
4、主線程使用了Thread.sleep Thead.wait方法,(應(yīng)該使用Handler來處理操作結(jié)果,而不是使用時(shí)間等待來阻塞線程)
5、Activity生命周期中onCreate、onResume執(zhí)行耗時(shí)操作
如何查看ANR出現(xiàn)的原因呢?在手機(jī)的目錄中/data/anr/traces.txt可以查看并且分析ANR出現(xiàn)的原因(IOwait、block、memoryleak)

以上四個(gè)問題已經(jīng)大概的了解了,那我們可以開始進(jìn)入簡物中優(yōu)化性能的正題,如何從打開30-40個(gè)界面優(yōu)化到打開測試到170個(gè)界面無卡頓

這里忘了提一個(gè)Activity啟動(dòng)模式的問題,對(duì)于電商App來說,詳情頁是打開量比較大的界面,有些人可能會(huì)建議使用SingleTop、SingleTask模式來作為詳情頁Activity的啟動(dòng)模式,但是SingleTop作為棧頂復(fù)用僅僅是在棧頂?shù)臅r(shí)候才在當(dāng)前Activity加載數(shù)據(jù),而對(duì)于SingleTask會(huì)將詳情頁以上的Activity實(shí)例全部銷毀,很明顯,這兩種模式都無法完美的解決我們的問題,反而會(huì)帶來一系列使用體驗(yàn)上的bug,那我們?yōu)榱送昝赖慕鉀Q問題來做一下流程分析

  • 1、首先,必須要保證程序沒有內(nèi)存泄漏,如果程序存在內(nèi)存泄漏,那及時(shí)你手動(dòng)去釋放資源,也無法將泄漏的對(duì)象釋放資源,因?yàn)槠鋵?duì)象依然被引用,Java無法將其視為垃圾內(nèi)存,上面已經(jīng)提到了如何避免內(nèi)存泄漏,我們?cè)陂_發(fā)的過程中就應(yīng)該避免寫有內(nèi)存泄漏風(fēng)險(xiǎn)的代碼,如果通過Android Monitor觀察內(nèi)存曲線以及在退出Activity的時(shí)候點(diǎn)擊GC按鈕內(nèi)存無法下降,那就可能存在內(nèi)存泄漏了,這可以使用LeakCanary工具檢查Activity內(nèi)存泄漏,這是一款非常實(shí)用的工具,可以自行百度查找相關(guān)使用方法,這篇文章篇幅過長,不作詳細(xì)介紹
    2、在前面我們已經(jīng)說了,電商App占用資源最多的是詳情頁和商品列表頁,如果我們的程序沒有內(nèi)存溢出,我們可以發(fā)現(xiàn)其實(shí)在Activity全部退出的時(shí)候內(nèi)存是能降下來的,不會(huì)因?yàn)榇蜷_的頁面堆積而退出也無法釋放內(nèi)存,那這其實(shí)是我們的一個(gè)突破點(diǎn)
    既然Activity退出可以下降內(nèi)存,那也就意味著Activity資源釋放后,那些被釋放資源的內(nèi)存是可以被標(biāo)記為垃圾內(nèi)存的,那我們能不能在Activity沒退出的時(shí)候就把資源釋放呢?答案是肯定可以的,如果我們?cè)贏ctivity不可見,或者檢測onTrimMemory的回調(diào)級(jí)別來釋放Activity的資源,那我們不就可以實(shí)現(xiàn)即使Activity仍然在任務(wù)棧,也可以釋放資源而不導(dǎo)致頁面疊加過多而導(dǎo)致OOM的需求嗎
    那主流的APP有哪些采用了這種形式:寫這篇文章比較匆忙,也沒有去做過多的測試,比較明顯的一個(gè)電商APP 禮物說 是采用釋放不可見資源來達(dá)到內(nèi)存優(yōu)化的目的(不過并非界面不可見立馬釋放,應(yīng)該是監(jiān)聽了onTrimMemory),當(dāng)界面再次加載時(shí)重載界面數(shù)據(jù)
    這里我貼一張簡物實(shí)現(xiàn)不可見界面資源釋放,重新加載是怎樣的效果(圖片可以明顯看到界面重載,實(shí)際應(yīng)用中可以使用一個(gè)loading效果來掩飾這個(gè)加載過程)
內(nèi)存優(yōu)化后的簡物

注意看,我在重載后還把界面位移還原了,實(shí)際情況可以有一個(gè)loading動(dòng)畫

那我們就朝著這個(gè)方向,去做分析,如何實(shí)現(xiàn)這個(gè)目標(biāo)(這里針對(duì)簡物的優(yōu)化實(shí)踐,不對(duì)其它情況(如音視頻的情況)做闡述)
1、對(duì)于電商App而言,毫無疑問圖片是占用內(nèi)存的主要元兇,我們這里分為兩種情況 1、詳情頁的圖片和主圖 2、商品列表頁的圖片(Recyclerview的圖片加載和釋放),注意:對(duì)于圖片至少要有一級(jí)本地緩存,釋放資源是釋放內(nèi)存中的資源,本地資源不作銷毀,因?yàn)樵俅渭虞d是從本地加載圖片,本地加載速度僅次于內(nèi)存
2、對(duì)于占用的圖片資源,除了釋放對(duì)應(yīng)的Bitmap之外,被其使用所顯示的View也要移除,否則資源被引用,Java是不會(huì)將其標(biāo)記為垃圾內(nèi)存的
3、因?yàn)槲覀冊(cè)俅胃陆缑鏁r(shí)需要使用到數(shù)據(jù)源,所以要考慮對(duì)網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)做備份 1、內(nèi)存不緊張可以存成員變量 2、內(nèi)存緊張序列化到本地文件/存儲(chǔ)數(shù)據(jù)庫,再次可見時(shí)取備用數(shù)據(jù)更新界面
分析完之后其實(shí)發(fā)現(xiàn),要做內(nèi)存釋放其實(shí)不難,主要是需要理解原理和操作對(duì)象

首先是詳情頁請(qǐng)求數(shù)據(jù)后需要對(duì)數(shù)據(jù)進(jìn)行備份,那我在這詳情頁的數(shù)據(jù)做了深克隆備份,在網(wǎng)絡(luò)回調(diào)的地方這樣處理


    SaleDetailBean.SaleDetail mSaleDetail;
    SaleDetailBean.SaleDetail mSaleDetailCopy;

    @Override
    public void getSaleDetailSucess(SaleDetailBean.SaleDetail saleDetail) {
        if(saleDetail == null){
            return;
        }

        /**
         * 初次請(qǐng)求數(shù)據(jù)不做主圖二次設(shè)置
         */
        if(mSaleDetail != null){
            mImage.setImageURI(Uri.parse(getTitleImage()));
        }
        this.mSaleDetail = saleDetail;
        this.mSaleDetailCopy = saleDetail.clone();
        updateViews();
    }

備份數(shù)據(jù)后可以開始寫資源釋放的方法,剛剛我們說了,對(duì)于電商類App來說,圖片是占用最多的,那我們應(yīng)該從返回網(wǎng)絡(luò)數(shù)據(jù)的圖片地址去釋放對(duì)應(yīng)的內(nèi)存資源,因?yàn)槊總€(gè)人的圖片加載以及緩存釋放方式都不一樣,這里以Fresco為例

    public void clear(){
       if(mSaleDetailCopy == null){
            return;
       }

        /**
         * 對(duì)圖片進(jìn)行清空,非圖片資源進(jìn)行默認(rèn)數(shù)據(jù)初始化
         */
        mTitle.setText("————");
        mDescribe.setText("——————————");
        mPrice.setText("——");
        mDiscount.setText("———");
        mImage.setImageURI(null);
        mPictures.clear();
        mSkuView.removeAllViews();
        mTagsView.removeAllViews();
        mLinkGoodsView.clear();

        /**
         * 釋放主圖資源
         */
        Fresco.getImagePipeline().evictFromMemoryCache(Uri.parse(getTitleImage()));

        /**
         * 釋放詳情圖列表資源
         */
        for(int i=0; i<mSaleDetailCopy.getImages().length; i++){
            Fresco.getImagePipeline().evictFromMemoryCache(Uri.parse(mSaleDetailCopy.getImages()[i]));
        }

        /**
         * 釋放相關(guān)推薦列表資源
         */
        for(int i=0; i<mSaleDetailCopy.getLink_goods().size(); i++){
            Fresco.getImagePipeline().evictFromMemoryCache(Uri.parse(mSaleDetailCopy.getLink_goods().get(i).getGoods_img()));
        }

        mSaleDetailCopy.clear();
        mSaleDetailCopy = null;
    }

mPictures.clear()方法內(nèi)部(因?yàn)閳D片高度是不一樣的,而SimpleDrawable是不能設(shè)定wrap_content自適應(yīng)網(wǎng)絡(luò)圖片,所以這里使用的重寫Linealayout來加載圖,在圖片回調(diào)成功后通過動(dòng)態(tài)設(shè)置LayoutParams來設(shè)置圖片寬高),界面嵌套關(guān)系-> LineaLayout -> ImageViews

    public void clear(){
        for(int i=0; i<getChildCount(); i++){
            if(getChildAt(i) instanceof ViewGroup){
                ((ViewGroup)getChildAt(i)).removeAllViews();
            }
        }
        removeAllViews();
    }

mLinkGoodsView.clear()

    public void clear(){
        if(mLinkGoodsScrollView != null){
            mLinkGoodsScrollView.clear();
            removeAllViews();
        }
    }

mLinkGoodsScrollView.clear()

    public void clear(){
        if(mContent != null){
            mContent.removeAllViews();
            removeView(mContent);
        }
    }

那我們?cè)谑裁吹胤秸{(diào)用呢?如果是Activity不可見就釋放資源,我們可以在onStop方法中調(diào)用,如果是監(jiān)聽內(nèi)存回調(diào)接口,我們可以在onTrimMemory方法調(diào)用,這里兩種都寫上

    @Override
    public void onTrimMemory(int level) {
        if(level == TRIM_MEMORY_UI_HIDDEN){
            doOnStop();
        }
    }

    @Override
    protected void doOnStop() {
        CURRENT_STATE |= READY_IN_STOP;
         getWindow().getDecorView().postDelayed(new Runnable() {
            @Override
            public void run() {
                if((CURRENT_STATE & READY_IN_STOP) == READY_IN_STOP){
                    getCategory().clear();
                    CURRENT_STATE |= IN_STOP;
                }
            }
        }, 250);
    }

界面再次可見時(shí),我們要恢復(fù)界面,以下代碼在Category中

    public void onResume(){
        /**
         * 拿一個(gè)當(dāng)前的界面view做判斷,是否被回收了
         * 如果mAnimationOver == true 說明詳情頁動(dòng)畫已經(jīng)加載過了,不是第一次加載界面
         */
        if(mSkuView.getChildCount() <=1 && mAnimationOver){
            /**
             * 請(qǐng)求回調(diào)的接口方法
             */
            getSaleDetailSucess(mSaleDetail);
        }
    }

Activity中的調(diào)用

    @Override
    protected void doOnResume() {
        if(!firstRunning && (CURRENT_STATE & IN_STOP) == IN_STOP){
            CURRENT_STATE &= ~READY_IN_STOP;
            CURRENT_STATE &= ~IN_STOP;
            getCategory().onResume();
        }
        if((CURRENT_STATE & READY_IN_STOP) == READY_IN_STOP){
            CURRENT_STATE &= ~READY_IN_STOP;
        }
    }

以上位運(yùn)算僅僅是區(qū)分Activity生命周期回調(diào)的順序,防止用戶快速點(diǎn)擊、返回而Runnable未執(zhí)行完成,導(dǎo)致界面被回收
前面貼過的詳情頁連續(xù)加載的效果圖


詳情頁加載退出的效果

那么在列表頁,如何回收內(nèi)存呢

    public void clear(){

        if(mDetail == null){
            return;
        }

        /**
         * 釋放標(biāo)題圖
         */
        Fresco.getImagePipeline().evictFromMemoryCache(Uri.parse(mDetail.getImage()));

        mZoomImage.setImageURI(null);

        /**
         * 將Adapter容器滯空
         */
        if(mAdapter != null && mAdapter.getItemCount()>0){
            mAdapter.notifyDataSetChangedNull();
        }

        /**
         * 清空列表圖片資源
         */
        for(int i=0; i<mAdapter.getItemCount(); i++){
            Fresco.getImagePipeline().evictFromMemoryCache(Uri.parse(mAdapter.getItem(i).getGoods_img()));
        }

    }

Adapter中是如何寫的(這里涉及到一個(gè)參數(shù)傳值的基礎(chǔ)知識(shí),因?yàn)榉椒▋?nèi)傳遞參數(shù)如果是引用類型,那傳遞是引用對(duì)象地址的拷貝值,所以重新復(fù)制新集合不會(huì)覆蓋外部對(duì)象的引用地址)

   public void notifyDataSetChangedNull(){
        this.datas = new ArrayList();
        this.notifyDataSetChanged();
    }

重新加載就比較簡單了

    public void onResume(){
        if(mAdapter.getItemCount() == 0 && mDetail != null){
             getThemeDetailSuccess(mDetail);
        }
    }

上一張列表頁釋放和加載的效果圖(回退主題列表頁是能看到圖片再次被加載的瞬間的)

主題列表頁的資源釋放

內(nèi)存波動(dòng)圖(注意有兩個(gè)曲線下降的折線點(diǎn),雖然后面內(nèi)存降低的內(nèi)存占用比0s - 5s要高,但是內(nèi)存下降的地方并非退出主題列表界面,而是打開了商品詳情,也就是我們打開商品詳情并沒有導(dǎo)致內(nèi)存上升,反而是把主題列表的圖片釋放了再加載詳情頁所以內(nèi)存有降低,而后面那個(gè)更低的是詳情頁退出,也就是我們釋放主題列表的資源是成功的),雖然退出詳情頁本身也是會(huì)有內(nèi)存波動(dòng)的,但是這里更多的是突出主題詳情到商品詳情跳轉(zhuǎn)的波動(dòng)不同

內(nèi)存波動(dòng)曲線

經(jīng)過優(yōu)化后的簡物實(shí)測幾次最高打開到170個(gè)界面沒有卡頓的感覺,后退界面均可重新加載

至此簡物中性能優(yōu)化過程就暫時(shí)告一段落了,這篇文章沒有梳理,如果寫的凌亂或者有錯(cuò)誤的地方請(qǐng)?jiān)谠u(píng)論中指正出來,我會(huì)做出相應(yīng)改正,謝謝!另外寫文章不易,希望不要隨意轉(zhuǎn)載,尊重版權(quán),需要轉(zhuǎn)載請(qǐng)私信我,同意后標(biāo)明來源即可

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,063評(píng)論 25 709
  • Android作為一種移動(dòng)設(shè)備,無論是內(nèi)存還是CPU性能都會(huì)有一定的限制,無法和PC設(shè)備相比擬,有鑒于此,Andr...
    乆丩乣閱讀 894評(píng)論 1 21
  • 性能優(yōu)化系列閱讀 Android性能優(yōu)化 性能優(yōu)化 - 消除卡頓 性能優(yōu)化- 內(nèi)存優(yōu)化 性能分析工具 - Trac...
    JackChen1024閱讀 1,451評(píng)論 1 20
  • 周星馳的電影《功夫》里面借火云邪神之口說出了一句至理名言:“天下武功,唯快不破”。 在移動(dòng)互聯(lián)網(wǎng)時(shí)代,同樣如此,如...
    lipy_閱讀 1,037評(píng)論 0 2
  • 無可奈何一歲去,與君相識(shí)二十載。往者不可諫,來者憂可追。東隅未逝,桑榆有足。時(shí)逢桃李年華,愿執(zhí)篤行以好學(xué),勤修學(xué)而...
    小曉笑_f081閱讀 413評(píng)論 0 1

友情鏈接更多精彩內(nèi)容