[紙上談兵2]——App內(nèi)存優(yōu)化

0 App內(nèi)存優(yōu)化

紙上談兵系列第二期,關(guān)于App的內(nèi)存優(yōu)化。

0.1 內(nèi)存管理

Android系統(tǒng)是基于Linux內(nèi)核開發(fā)的開源操作系統(tǒng),而linux系統(tǒng)的內(nèi)存管理有其獨(dú)特的動(dòng)態(tài)存儲(chǔ)管理機(jī)制。不過Android系統(tǒng)對Linux的內(nèi)存管理機(jī)制進(jìn)行了優(yōu)化,Linux系統(tǒng)會(huì)在進(jìn)程活動(dòng)停止后就結(jié)束該進(jìn)程,而Android把這些進(jìn)程都保留在內(nèi)存中,直到系統(tǒng)需要更多內(nèi)存為止。這些保留在內(nèi)存中的進(jìn)程通常情況下不會(huì)影響整體系統(tǒng)的運(yùn)行速度,并且當(dāng)用戶再次激活這些進(jìn)程時(shí),提升了進(jìn)程的啟動(dòng)速度。

0.2 垃圾回收

無論是ART還是Dalvik虛擬機(jī),都和眾多Java虛擬機(jī)一樣,屬于一種托管內(nèi)存環(huán)境(程序員不需要顯示的管理內(nèi)存的分配與回收,交由系統(tǒng)自動(dòng)管理)。托管內(nèi)存環(huán)境會(huì)跟蹤每個(gè)內(nèi)存分配, 一旦確定程序不再使用一塊內(nèi)存,它就會(huì)將其釋放回堆中,而無需程序員的任何干預(yù)。 回收托管內(nèi)存環(huán)境中未使用內(nèi)存的機(jī)制稱為垃圾回收。
Android的內(nèi)存堆是分代式(Generational)的,意味著它會(huì)將所有分配的對象進(jìn)行分代,然后分代跟蹤這些對象。 例如,最近分配的對象屬于年輕代(Young Generation)。 當(dāng)一個(gè)對象長時(shí)間保持活動(dòng)狀態(tài)時(shí),它可以被提升為年老代(Older Generation),之后還能進(jìn)一步提升為永久代(Permanent Generation),這一點(diǎn)和JVM的垃圾回收基本一致。

0.3 內(nèi)存限制

Dalvik堆被限制為每個(gè)應(yīng)用程序進(jìn)程的單個(gè)虛擬內(nèi)存范圍。這定義了邏輯堆大小,它可以根據(jù)需要增長,但最多只能達(dá)到系統(tǒng)為每個(gè)應(yīng)用程序定義的限制。
Dalvik堆不會(huì)壓縮堆的邏輯大小,這意味著Android不會(huì)對堆進(jìn)行碎片整理以關(guān)閉空間。 Android只能在堆末尾有未使用的空間時(shí)縮小邏輯堆大小。但是,系統(tǒng)仍然可以減少堆使用的物理內(nèi)存。垃圾收集后,Dalvik遍歷堆并找到未使用的頁面,然后使用madvise將這些頁面返回到內(nèi)核。因此,大塊的配對分配和解除分配應(yīng)該導(dǎo)致回收所有(或幾乎所有)所使用的物理內(nèi)存。但是,從小分配中回收內(nèi)存可能效率低得多,因?yàn)橛糜谛》峙涞捻撁嫒钥赡芘c尚未釋放的其他內(nèi)容共享。

0.4 限制應(yīng)用的內(nèi)存

為了維護(hù)高效的多任務(wù)環(huán)境,Android為每個(gè)應(yīng)用程序設(shè)置了堆大小的硬性限制。 該限制因設(shè)備而異,取決于設(shè)備總體可用的RAM。 如果應(yīng)用程序已達(dá)到該限制并嘗試分配更多內(nèi)存,則會(huì)收到 OutOfMemoryError 。
在某些情況下,你可能希望查詢系統(tǒng)以準(zhǔn)確確定當(dāng)前設(shè)備上可用的堆空間大小,例如,確定可以安全地保留在緩存中的數(shù)據(jù)量。 你可以通過調(diào)用 getMemoryClass() 來查詢系統(tǒng)中的這個(gè)數(shù)字。 此方法返回一個(gè)整數(shù),指示應(yīng)用程序堆可用的兆字節(jié)數(shù)。

0.5 切換應(yīng)用

當(dāng)用戶在應(yīng)用程序之間切換時(shí),Android會(huì)將非前臺(tái)應(yīng)用程序(即用戶不可見或并沒有運(yùn)行諸如音樂播放等前臺(tái)服務(wù)的進(jìn)程)緩存到一個(gè)最近最少使用緩存(LRU Cache)中。例如,當(dāng)用戶首次啟動(dòng)應(yīng)用程序時(shí),會(huì)為其創(chuàng)建一個(gè)進(jìn)程; 但是當(dāng)用戶離開應(yīng)用程序時(shí),該進(jìn)程不會(huì)退出。 系統(tǒng)會(huì)緩存該進(jìn)程。 如果用戶稍后返回應(yīng)用程序,系統(tǒng)將重新使用該進(jìn)程,從而使應(yīng)用程序切換更快。
如果你的應(yīng)用程序具有緩存進(jìn)程并且它保留了當(dāng)前不需要的內(nèi)存,那么即使用戶未使用它,你的應(yīng)用程序也會(huì)影響系統(tǒng)的整體性能。 當(dāng)系統(tǒng)內(nèi)存不足時(shí),就會(huì)從最近最少使用的進(jìn)程開始,終止LRU Cache中的進(jìn)程。另外,系統(tǒng)還會(huì)綜合考慮保留了最多內(nèi)存的進(jìn)程,并可能終止它們以釋放RAM。
當(dāng)系統(tǒng)開始終止LRU Cache中的進(jìn)程時(shí),它主要是自下而上的。 系統(tǒng)還會(huì)考慮哪些進(jìn)程占用更多內(nèi)存,因?yàn)樵谒粴r(shí)會(huì)為系統(tǒng)提供更多內(nèi)存增益。 因此在整個(gè)LRU列表中消耗的內(nèi)存越少,保留在列表中并且能夠快速恢復(fù)的機(jī)會(huì)就越大。

原文鏈接

1 內(nèi)存抖動(dòng)

現(xiàn)象:內(nèi)存鋸齒狀,頻繁GC導(dǎo)致卡頓。
原因:頻繁創(chuàng)建對象和回收對象,導(dǎo)致GC過于頻繁。
構(gòu)造帶有內(nèi)存抖動(dòng)的代碼:

final Handler handler = new Handler();
handler.post(new Runnable() {
    @Override
    public void run() {
        String s = "";
        for (int i = 0; i < 1000; i++) {
            s += i;
        }
        Log.d(TAG, "run: " + s);

        handler.post(this);
    }
});

這里會(huì)在for循環(huán)時(shí)不斷的進(jìn)行String的拼接操作,會(huì)造成不斷創(chuàng)建對象并且回收對象,所以這就是很好的內(nèi)存抖動(dòng)的代碼。
內(nèi)存抖動(dòng)分析圖:


內(nèi)存抖動(dòng).jpg

使用Android Studio自帶的Profile工具進(jìn)行Memory分析,可以看到內(nèi)存下面有很多回收的標(biāo)記(垃圾桶)。選擇一段時(shí)間,此時(shí)可以看到內(nèi)存的分配(方框1)和釋放數(shù)量(方框2)。點(diǎn)擊數(shù)量對應(yīng)的類,可以在右邊查看調(diào)用棧,我們可以看到調(diào)用棧為MainActivity的匿名內(nèi)部類的run方法。

避免內(nèi)存抖動(dòng)需要注意:

  1. for/onDraw循環(huán)以及其他會(huì)被多次調(diào)用的方法里不要直接new對象
  2. 對于那些無法避免需要?jiǎng)?chuàng)建對象的情況,我們可以考慮對象池模型,通過對象池來解決頻繁創(chuàng)建與銷毀的問題,但是這里需要注意結(jié)束使用之后,需要手動(dòng)釋放對象池中的對象。
  3. 使用StringBuilder代替頻繁的"+"號(hào)操作

2 內(nèi)存泄露

內(nèi)存泄漏(Memory Leak)是指程序中己動(dòng)態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。[百度百科]
在Android中,內(nèi)存泄露可能會(huì)由于內(nèi)存無法被釋放,導(dǎo)致內(nèi)存使用逐漸增加,最終可能會(huì)引起OOM(OutOfMemoryError)。

2.1 Android中檢測內(nèi)存的泄露

在App的開發(fā)階段,使用Proflie工具進(jìn)行內(nèi)存分析,檢查打開某個(gè)頁面并且關(guān)閉后,強(qiáng)制GC完成,內(nèi)存并沒有減少或者回到主頁面后,GC完成,內(nèi)存與最初始內(nèi)存相差很大如果上述現(xiàn)象發(fā)生,那么可以考慮是由于內(nèi)存泄漏引起的。

2.2 內(nèi)存泄露定位

  1. 如果明確知道是某個(gè)頁面導(dǎo)致的內(nèi)存泄露,可以使用Profile工具進(jìn)行分析,在GC完成后,查看當(dāng)前內(nèi)存中存在的對象。查找并不應(yīng)該存在的對象,通過查找引用樹即可定位。
    內(nèi)存泄露.jpg
  2. 開發(fā)階段使用LeakCannary,可以有效地在開發(fā)階段發(fā)現(xiàn)內(nèi)存泄露。有關(guān)LeakCannary原理,查看這里
  3. 線上內(nèi)存泄露檢測比較難,而且需要消耗流量,總體來說弊大于利,不建議線上檢測。
    • 設(shè)定場景線上Dump(例如超過最大內(nèi)存80%),使用Debug.dumpHprofData(“fileName”);進(jìn)行獲取當(dāng)前堆棧信息,并上傳服務(wù)器,最終通過MAT手動(dòng)分析。
    • 將LeakCannary帶到線上,發(fā)生內(nèi)存泄露時(shí)上報(bào)服務(wù)器,并且將文件上傳。

3 內(nèi)存泄露需要關(guān)注點(diǎn)

  1. 通過addListener/addXXXz形式添加的各種回調(diào),需要在相關(guān)的位置remove。
  2. 通過Handler執(zhí)行post/postDelay時(shí),可以考慮在destory位置remove相關(guān)回調(diào)。
  3. 使用匿名內(nèi)部類/內(nèi)部類時(shí)需謹(jǐn)慎。匿名內(nèi)部類/會(huì)隱式持有外部類的引用,例如我們在定義成員變量時(shí),new Handler(){}并重寫方法,此時(shí)Android Studio會(huì)提示我們This Handler class should be static or leaks might occur (anonymous android.os.Handler)。我們可以使用靜態(tài)匿名內(nèi)部類/內(nèi)部類,通過WeakReference將需要的外部對象包裝,從而解決上面的問題。
  4. 使用Application Context代替Activity Context(允許的情況下)
  5. Bitmap對象需要及時(shí)回收
  6. 游標(biāo)(Cursor)注意關(guān)閉
  7. 謹(jǐn)慎使用static對象

3 內(nèi)存溢出

做了幾年的Android開發(fā),沒有遇到過幾次OOM都不好意思說自己做過Android。OOM的原因上面也說了,Android系統(tǒng)會(huì)為每個(gè)App分配最大的內(nèi)存,當(dāng)App使用的內(nèi)存超過了該最大內(nèi)存,就會(huì)出現(xiàn)OutOfMemoryError。
OOM出現(xiàn)的原因基本上由以下幾點(diǎn)造成:

  1. Bitmap
  2. 內(nèi)存泄露
  3. 過多的對象存儲(chǔ),并且不能釋放(ListView/RecyclerView數(shù)據(jù)特別多,導(dǎo)致內(nèi)存中存在特別多的對象)

如何優(yōu)化內(nèi)存占用,從而降低OOM的發(fā)生呢?可以從以下幾點(diǎn)考慮:

  1. 圖片加載/處理時(shí)要慎重,可以通過壓縮/緩存/設(shè)置inBitmap等等方式減少內(nèi)存消耗,關(guān)于Bitmap使用的優(yōu)化,可以查看google官方文檔
  2. 使用輕量級的數(shù)據(jù)結(jié)構(gòu)
    例如:可以考慮使用SparseArray代替Map,SparseArray是Android對特殊Map的優(yōu)化。
  3. 資源壓縮
    可以考慮將設(shè)計(jì)給的資源圖片進(jìn)行壓縮(在可壓縮的前提下),從而降低內(nèi)存的使用。
  4. 可以嘗試使用對象池進(jìn)行內(nèi)存的復(fù)用
  5. ListView/GridView/RecyclerView要將item進(jìn)行復(fù)用
  6. 優(yōu)化布局層級,減少內(nèi)存消耗
  7. 使用protobuffer/flatbuffers進(jìn)行序列化數(shù)據(jù)

4 思考

關(guān)于App的內(nèi)存優(yōu)化,是個(gè)持續(xù)的過程。隨著時(shí)間的推移,會(huì)有更多更先進(jìn)的技術(shù)出現(xiàn),任何技術(shù)都需要更新。因此關(guān)于App的內(nèi)存優(yōu)化并沒有一勞永逸的優(yōu)化方案,只有持續(xù)并且不斷尋找出優(yōu)化點(diǎn)才是最好的解決方案。

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

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