Android性能優化(二)內存優化

        上一篇文章,我總結了一下app啓動優化的一些知識。這篇文章,總結一下內存優化相關的一些知識。內存優化,相比於啓動優化,可能沒那麼明顯。爲什麼這麼說呢?啓動快慢,我們體驗一次就能體驗出來。而內存增長,我們操作一次,兩次,三次......如果不借助什麼工具或者命令的話,我們的肉眼並不能發現什麼。因此,在很多情況下,我們會忽視掉app內存這一塊。可能大部分程序員,知道自己的app存在內存問題,往往是因爲QA的monkey test中,存在着某個oom的崩潰,或者bugly上頻繁上報oom的崩潰。那這個時候,我們意識到,原來我們的app並沒有想象中那麼完美。

一、前言

        在正式的總結內存優化相關知識之前,我還是說一些我自己的經歷。如下圖所示,在這個照片中,記錄的是我以前參與開發的一個功能。當我們自信心滿滿地填完測試用例,提測後,發現沒什麼大的問題。然後,QA下班跑monkey,第二天一大早,各種crash日誌打包給你發過來了。打開一看,啥,我的應用跑了一晚上monkey內存增長了400M?我靠,這麼牛逼!我不信,我試試。於是,我就試了試,下圖就是我當年的記錄:

        怎麼樣?你跌倒了沒?崩潰了沒?崩潰了,徹底崩潰了。尤其以前我們做手機,非常強調整體的內存。於是,我們抓緊聯繫QA:小姐姐,這個問題不大,我們一會就修復好了。說完,立馬去查看代碼。這塊內存釋放了嗎?釋放了。這裏的對象回收了嗎?回收了。這個容器清空了嗎?清空了。好像沒問題啊!!!但是,你自己的測試結果都在那!!!所以,我們迴歸到那句話,內存問題,不像是啓動問題或者是一些直觀的UI問題,往往容易被忽視。而且,根據代碼去排查根本不好排查。一個是因爲代碼就是我們自己寫的,再一個就是我們不可能一行一行代碼去找,我們只能大體的去找一些回收函數,或者猜測可能出問題的模塊。

二、三種內存問題

1、內存抖動

        記得剛工作的時候,我做好自己的工作,就會去看我導師和我主管頭上掛着的BUG。記得在我主管那看到一個性能組的BUG:XXX模塊XXX操作時,存在卡頓,內存圖如下。我看了一下,內存圖這不是很正常嗎,有增加有減少,整體內存沒有太大的增長啊。我於是就得出了結論:沒問題。於是,我繼續往下滑,看到了我主管的comment:內存抖動。啥?內存抖動?這是個啥?於是,我百度了一下,大體瞭解到:

        內存抖動,由於短時間內頻繁的創建和銷燬對象,頻繁觸發GC操作導致的內存問題。對象創建並用完後及時銷燬,這是沒問題的。但是如果頻繁的創建和銷燬,就會頻繁的觸發GC操作。其實,GC操作是非常消耗CPU的,進而會導致卡頓等問題。

        我寫個demo模擬下內存抖動的情況,我在界面上放了兩個Button,一個叫創建,一個叫銷燬,點擊創建會創建一個bitmap,點擊銷燬則銷燬該bitmap。代碼如下:

        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inJustDecodeBounds = false;
                mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
                Log.d("TTTT","create");
            }
        });
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mBitmap != null && !mBitmap.isRecycled()) {
                    Log.d("TTTT","recycle");
                    mBitmap.recycle();
                    mBitmap = null;
                }
            }
        });

        我們使用profiler監控一下內存變化,放大後,如下圖所示。很明顯,內存增減交錯,呈現鋸齒狀,但由於我們有創建,有回收,整體的內存沒有大幅度的增加:

2、內存泄漏

        接下來,說一下什麼是內存泄漏。內存泄漏,簡單來說,就是內存有申請,沒釋放,或者說沒有完全釋放,導致可用內存逐漸減少,當到達一定的程度時,也會頻繁的觸發GC操作。因此,內存泄漏也會導致卡頓等性能問題。

        我依然通過一個小例子,演示一下內存泄漏。我還是使用上面的例子稍微改一下。我每次點擊創建都會創建一個不同大小的Bitmap。而且,我只點創建,不點銷燬,代碼如下:

 button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inJustDecodeBounds = false;
                mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
                size++;
                Bitmap bitmap = Bitmap.createScaledBitmap(mBitmap, 54 * size, 96 * size, false);
                Log.d("TTTT", "create");
            }
        });

        我們使用profiler抓一下內存,我們可以看到,隨着我一直點擊創建,內存一直在增加,雖然也有驟降的地方,但是整體趨勢是增加的。從64M一直增加到了190M,且沒有停止的趨勢:

3、內存溢出

        內存溢出,我接着上面的內存泄漏來說。我們從內存泄漏可以知道,內存一直申請一直增加,沒有釋放的趨勢。當達到Android系統爲每個app設置的內存閾值後,會如何?毫無疑問,會因爲oom(OutOfMemoryError),導致崩潰。

        首先,我們在程序中獲取一下內存閾值,很簡單的代碼:

Runtime runtime = Runtime.getRuntime();
Log.d("TTTT", "maxMemory:" + runtime.maxMemory());

        看一下打印的結果:

2020-03-07 22:19:24.926 4482-4482/? D/TTTT: maxMemory:201326592

        我們計算一下201326592子節是多少M:201326592/(1024^2) = 192M。也就是說,我當前手機的內存閾值是192M。當然,我們有方法可以將內存擴展爲更大,在這裏我先不多說。

        事實上,在上面的代碼中,我已經點到崩潰了,那我接着上面的圖,把後面的內存圖貼一下。如下所示,可以看到,在內存在128M-192M之間的時候,已經開始頻繁的觸發GC操作。再往後,我們發現,內存直接降到了0,不動了。爲什麼?因爲,他真的達到內存閾值了,他真的崩潰了。

 

         我們把鼠標移動到圖的某一幀位置,可以查看到此時的內存情況,可以看到,最後崩潰前,JAVA內存佔用達到了186M,總內存達到233.6M。而我們app剛啓動的時候,JAVA內存只有6M,總內存46.8M,如下圖所示:

                                              

        我們看一下崩潰日誌,果不其然,分配內存失敗,OOM:

 

三、內存優化

        首先,在總結內存優化的方法前,我們先總結一下幾種內存問題的原因:

(1)對象頻繁創建和銷燬導致內存抖動,觸發GC操作,導致卡頓甚至OOM

(2)只申請內存,沒有釋放內存,導致系統可用內存減少,觸發GC操作,導致卡頓直至OOM

        因此,內存優化的兩個方向就是:避免內存抖動和內存泄漏。那麼,如何避免內存抖動和內存泄漏呢?其實,我們在上面的demo中,做了兩種錯誤的操作。我們也可以據此給出幾種避免內存抖動和內存泄漏的方法。由於,內存優化的方法太多了,而且需要根據具體的項目,我們不一一展開。在此,我總結一下我用過的以及以前從資料上查到的一些優化技巧。大家可以據此考慮一下自己的項目中,是否存在某些地方不合理。

1、Bitmap的使用

        大家可以看到,我的demo中使用的是Bitmap來演示一些錯誤的代碼。其實,對於大多數內存泄漏問題,都跟Bitmap有關係。因爲。這玩意如果使用不當,將會帶來非常大的內存消耗。可能大家現在展示圖片基本都使用Glide等第三方庫,很少去使用BitmapFactory去解碼Bitmap。但是,對於Bitmap的使用,我還是重點總結一下:

(1)使用BitmapFactory來decode一塊比較大的Bitmap時,需要注意inSampleSize的計算。通常,我使用的方法是:

  • 設置inJustDecodeBounds爲true,可以在不解碼Bitmap的情況下獲取Bitmap的實際寬高(sourceWidth,sourceHeight);
  • 選取圖片顯示區域的寬(objWidth)或高(objHeight)作爲參考值,根據fitin算法,計算合適的inSampleSize;
  • 選取合適的解碼位數,32位(ARGB8888),16位(RGB565)等,如果沒有特殊需求,使用RGB565即可;
  • 根據計算得到的inSampleSize,設置inJustDecodeBounds爲false,解碼Bitmap。

(2)Bitmap使用完後,及時的回收,回收的方法如下:

        if (mBitmap != null && !mBitmap.isRecycled()) {
            mBitmap.recycle();
            mBitmap = null;
        }

2、static對象謹慎使用

3、cursor及時關閉

4、各種監聽器的註銷

5、SharedPreferences謹慎使用

6、第三方庫謹慎使用

        總結一下,這篇博客從我的親身經歷出發,介紹了三個內存問題及其原因。並根據導致內存問題的幾個原因,提出了一些內存優化的建議和方向。當然,我以前接觸比較多的就是Bitmap,因此我花了較多的篇幅圍繞Bitmap來寫demo和提出建議。後面,我會逐漸的豐富和總結其他的一些內存優化注意事項。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章