安卓性能優(yōu)化3——內(nèi)存優(yōu)化

通常情況下我們說的內(nèi)存是指手機(jī)的RAM耳胎,它主要包括一下幾個(gè)部分
1.寄存器 :速度最快的存儲場所睛廊,因?yàn)榧拇嫫魑挥谔幚砥鲀?nèi)部,所以在程序中我們無法控制

  1. 棧(Stack) :存放基本類型的對象和引用逞盆,但是對象本身不存放在棧中蚕苇,而是存放在堆中
    變量其實(shí)是分為兩部分的:一部分叫變量名,另外一部分叫變量值检眯,對于局部變量(基本類型的變量和對象的引用變量)而言厘擂,統(tǒng)一都存放在棧中,但是變量值中存儲的內(nèi)容就有在一定差異了:Java中存在8大基本類型锰瘸,他們的變量值中存放的就是具體的數(shù)值刽严,而其他的類型都叫做引用類型(對象也是引用類型,你只要記住除了基本類型避凝,都是引用類型)他們的變量值中存放的是他們在堆中的引用(內(nèi)存地址)舞萄。
    在函數(shù)執(zhí)行的時(shí)候眨补,函數(shù)內(nèi)部的局部變量就會在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束的時(shí)候這些存儲單元會被自動釋放倒脓。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中是一塊連續(xù)的內(nèi)存區(qū)域撑螺,效率很高,速度快把还,但是大小是操作系統(tǒng)預(yù)定好的所以分配的內(nèi)存容量有限实蓬。

3.堆(Heap)

在堆上分配內(nèi)存的過程稱作 內(nèi)存動態(tài)分配過程。在java中堆用于存放由new創(chuàng)建的對象和數(shù)組吊履。堆中分配的內(nèi)存安皱,由java虛擬機(jī)自動垃圾回收器(GC)來管理(可見我們要進(jìn)行的內(nèi)存優(yōu)化主要就是對堆內(nèi)存進(jìn)行優(yōu)化)。堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲空閑內(nèi)存地址艇炎,自然不是連續(xù)的)酌伊,堆大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G)

4.靜態(tài)存儲區(qū)/方法區(qū)(Static Field)
是指在固定的位置上存放應(yīng)用程序運(yùn)行時(shí)一直存在的數(shù)據(jù),java在內(nèi)存中專門劃分了一個(gè)靜態(tài)存儲區(qū)域來管理一些特殊的數(shù)據(jù)變量如靜態(tài)的數(shù)據(jù)變量缀踪。
5.常量池(Constant Pool)
顧名思義專門存放常量的居砖。注意 String s = "java"中的“java”也是常量。JVM虛擬機(jī)為每個(gè)已經(jīng)被轉(zhuǎn)載的類型維護(hù)一個(gè)常量池驴娃。常量池就是該類型所有用到地常量的一個(gè)有序集合包括直接常量(基本類型奏候,String)和對其他類型、字段和方法的符號引用唇敞。

定義一個(gè)局部變量的時(shí)候蔗草,java虛擬機(jī)就會在棧中為其分配內(nèi)存空間,局部變量的基本數(shù)據(jù)類型和引用存儲于棧中疆柔,引用的對象實(shí)體存儲于堆中咒精。因?yàn)樗鼈儗儆诜椒ㄖ械淖兞浚芷陔S方法而結(jié)束旷档。成員變量全部存儲與堆中(包括基本數(shù)據(jù)類型模叙,引用和引用的對象實(shí)體),因?yàn)樗鼈儗儆陬愋悓ο蠼K究是要被new出來使用的范咨。當(dāng)堆中對象的作用域結(jié)束的時(shí)候,這部分內(nèi)存也不會立刻被回收谐区,而是等待系統(tǒng)GC進(jìn)行回收湖蜕。所謂的內(nèi)存分析,就是分析Heap中的內(nèi)存狀態(tài)

簡單例子

比如說這個(gè)類

public class People{
    int a = 1;
    Student s1 = new Student();
    public void XXX(){
        int b = 1;
        Student s2 = new Student();
    }
}

請問a的內(nèi)存在哪里宋列,b的內(nèi)存在哪里昭抒,s1,s2的內(nèi)存在哪里?記住下面兩句話灭返。

成員變量全部存儲在堆中(包括基本數(shù)據(jù)類型盗迟,引用及引用的對象實(shí)體),因?yàn)樗麄儗儆陬愇鹾悓ο笞罱K還是要被new出來的罚缕。
局部變量的基本數(shù)據(jù)類型和引用存儲于棧當(dāng)中,引用的對象實(shí)體存儲在堆中怎静。因?yàn)樗麄儗儆诜椒ó?dāng)中的變量邮弹,生命周期會隨著方法一起結(jié)束。
所以答案就是a蚓聘,s1,s2對象都堆中腌乡,b和s2對象引用在棧中

內(nèi)存模型

普通的Linux中啟動的應(yīng)用通常和登陸用戶相關(guān)聯(lián),同一用戶的UID相同夜牡。但是Android中給不同的應(yīng)用都賦予了不同的UID与纽,這樣不同的應(yīng)用將不能相互訪問資源。對應(yīng)用而言塘装,這樣會更加封閉急迂,安全。
在Android和Java中都存在著一個(gè)Generational系統(tǒng)會根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的GC操作

Generational Heap Memory模型主要由:Young Generation(新生代)蹦肴、Old Generation(舊生代)僚碎、Permanent
image.png

其中Young Generation區(qū)域存放的是最近被創(chuàng)建對象,此區(qū)域最大的特點(diǎn)就是創(chuàng)建的快阴幌,被銷毀的也很快听盖。當(dāng)對象在Young Generation區(qū)域停留的時(shí)間到達(dá)一定程度的時(shí)候,它就會被移動到Old Generation區(qū)域中裂七,同理,最后他將會被移動到Permanent Generation區(qū)域中

每一個(gè)區(qū)域的大小都是有固定值的仓坞,當(dāng)進(jìn)入的對象總大小到達(dá)某一級內(nèi)存區(qū)域閥值的時(shí)候就會觸發(fā)GC機(jī)制背零,進(jìn)行垃圾回收,騰出空間以便其他對象進(jìn)入

不僅如此无埃,不同級別的Generation區(qū)域GC是需要的時(shí)間也是不同的徙瓶。同等對象數(shù)目下,Young Generation GC所需時(shí)間最短嫉称,Old Generation次之侦镇,Permanent Generation 需要的時(shí)間最長。當(dāng)然GC執(zhí)行的長短也和當(dāng)前Generation區(qū)域中的對象數(shù)目有關(guān)织阅。遍歷查找20000個(gè)對象比起遍歷50個(gè)對象自然是要慢很多的壳繁。

GC機(jī)制概述

與C++不用,在Java中,內(nèi)存的分配是由程序完成的闹炉,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection蒿赢,GC)完成的,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存渣触,但也隨之帶來了內(nèi)存泄漏的可能羡棵。簡單點(diǎn)說:對于 C++ 來說,內(nèi)存泄漏就是new出來的對象沒有 delete嗅钻,俗稱野指針;而對于 java 來說皂冰,就是 new 出來的 Object 放在 Heap 上無法被GC回收

Android使用的主要開發(fā)語言是Java所以二者的GC機(jī)制原理也大同小異,所以我們只對于常見的JVM GC機(jī)制的分析养篓,就能達(dá)到我們的目的

新生代GC算法

由于Young Generation通常存活的時(shí)間比較短秃流,所以Young Generation采用了Copying算法進(jìn)行回收,Copying算法就是掃描出存活的對象觉至,并復(fù)制到一塊新的空間中剔应,Young Generation采用空閑指針的方式來控制GC觸發(fā),指針保存最后一個(gè)分配在Young Generation中分配空間地對象的位置语御。當(dāng)有新的對象要分配內(nèi)存空間的時(shí)候峻贮,就會主動檢測空間是否足夠,不夠的情況下就出觸發(fā)GC应闯,當(dāng)連續(xù)分配對象時(shí)纤控,對象會逐漸從Eden移動到Survivor,最后移動到Old Generation碉纺。


image.png

舊生代GC算法

Old Generation與Young Generation不同船万,對象存活的時(shí)間比較長,比較穩(wěn)固骨田,因此采用標(biāo)記(Mark)算法來進(jìn)行回收耿导。所謂標(biāo)記就是掃描出存活的對象,然后在回收未必標(biāo)記的對象态贤〔丈耄回收后的剩余空間要么進(jìn)行合并,要么標(biāo)記出來便于下次進(jìn)行分配悠汽,總之就是要減少內(nèi)存碎片帶來的效率損耗

如何判斷對象是否可以被回收

引用計(jì)數(shù)器

引用計(jì)數(shù)器是垃圾收集器中的早起策略箱吕。這種方法中,每個(gè)對象實(shí)體(不是它的引用)都有一個(gè)引用計(jì)數(shù)器柿冲。當(dāng)一個(gè)對象創(chuàng)建的時(shí)候茬高,且將該對象分配給一個(gè)每分配給一個(gè)變量,計(jì)數(shù)器就+1假抄,當(dāng)一個(gè)對象的某個(gè)引用超過了生命周期或者被設(shè)置一個(gè)新值時(shí)怎栽,對象計(jì)數(shù)器就-1丽猬,任何引用計(jì)數(shù)器為 0 的對象可以被當(dāng)作垃圾收集。當(dāng)一個(gè)對象被垃圾收集時(shí)婚瓜,引用的任何對象技術(shù) - 1宝鼓。

優(yōu)點(diǎn):執(zhí)行快,交織在程序運(yùn)行中巴刻,對程序不被長時(shí)間打斷的實(shí)時(shí)環(huán)境比較有利愚铡。

缺點(diǎn):無法檢測出循環(huán)引用。比如:對象A中有對象B的引用胡陪,而B中同時(shí)也有A的引用

跟蹤收集器

現(xiàn)在的垃圾回收機(jī)制已經(jīng)不太使用引用計(jì)數(shù)器的方法判斷是否可回收沥寥,而是使用跟蹤收集器方法。

現(xiàn)在大多數(shù)JVM采用對象引用遍歷機(jī)制從程序的主要運(yùn)行對象(如靜態(tài)對象/寄存器/棧上指向的堆內(nèi)存對象等)開始檢查引用鏈柠座,去遞歸判斷對象收否可達(dá)邑雅,如果不可達(dá),則作為垃圾回收妈经,當(dāng)然在便利階段淮野,GC必須記住那些對象是可達(dá)的,以便刪除不可到達(dá)的對象吹泡,這稱為標(biāo)記(marking)對象骤星。

下一步,GC就要刪除這些不可達(dá)的對象爆哑,在刪除時(shí)未必標(biāo)記的對象洞难,釋放它們的內(nèi)存的過程叫做清除(sweeping),而這樣會造成內(nèi)存碎片化揭朝,布局已分配給新的對象队贱,但是他們集合起來還很大。所以很多GC機(jī)制還要重新組織內(nèi)存中的對象潭袱,并進(jìn)行壓縮柱嫌,形成大塊、可利用的空間
為了達(dá)到這個(gè)目的屯换,GC需要停止程序的其他活動慎式,阻塞進(jìn)程。這里我們要注意的是:不要頻繁的引發(fā)GC趟径,執(zhí)行GC操作的時(shí)候,任何線程的任何操作都會需要暫停癣防,等待GC操作完成之后蜗巧,其他操作才能夠繼續(xù)運(yùn)行, 故而如果程序頻繁GC, 自然會導(dǎo)致界面卡頓. 通常來說,單個(gè)的GC并不會占用太多時(shí)間蕾盯,但是大量不停的GC操作則會顯著占用幀間隔時(shí)間(16ms)如果在幀間隔時(shí)間里面做了過多的GC操作幕屹,那么自然其他類似計(jì)算,渲染等操作的可用時(shí)間就變得少了。

Android內(nèi)存泄漏分析

對于 C++ 來說望拖,內(nèi)存泄漏就是new出來的對象沒有 delete渺尘,俗稱野指針;而對于 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收

為什么不能被回收

GC過程與對象的引用類型是嚴(yán)重相關(guān)的说敏,下面我們就看看Java中(Android中存在差異)對于引用的四種分類:

  • 強(qiáng)引用(Strong Reference):JVM寧愿拋出OOM鸥跟,也不會讓GC回收的對象,Jvm停止運(yùn)行才死亡

  • 軟引用(Soft Reference) :只有內(nèi)存不足時(shí),才會被GC回收盔沫。

  • 弱引用(weak Reference):在GC時(shí)医咨,一旦發(fā)現(xiàn)弱引用,立即回收

  • 虛引用(Phantom Reference):任何時(shí)候都可以被GC回收架诞,當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對象時(shí)拟淮,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前谴忧,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中很泊。程序可以通過判斷引用隊(duì)列中是否存在該對象的虛引用,來了解這個(gè)對象是否將要被回收沾谓∥欤可以用來作為GC回收Object的標(biāo)志。
    在Android開發(fā)過程中搏屑,我們常常使用HasMap保存對象争涌,但是為了防止內(nèi)存泄漏,在保存內(nèi)存占用較大辣恋、生命周期較長的對象的時(shí)候亮垫,盡量使用LruCache代替HasMap用于保存對象

//指定最大緩存空間

private static final int MAX_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);

LruCache mBitmapLruCache = new LruCache<>(MAX_SIZE);  
而造成不能回收的根本原因就是:堆內(nèi)存中長生命周期的對象持有短生命周期對象的強(qiáng)/軟引用,盡管短生命周期對象已經(jīng)不再需要伟骨,但是因?yàn)殚L生命周期對象持有它的引用而導(dǎo)致不能被回收

如何的監(jiān)聽系統(tǒng)發(fā)生GC

系統(tǒng)每進(jìn)行一次GC操作時(shí)饮潦,都會在LogCat中打印一條日志,我們只要去分析這條日志就可以了携狭,日志的基本格式如下所示:

DVM中

D/dalvikvm(30615): GC FOR ALLOC freed 4442K, 25% free 20183K/26856K, paused 24ms , total 24ms

ART中

I/art(198): Explicit concurrent mark sweep GC freed 700(30KB) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 186us total 12.763ms

觸發(fā)GC操作的原因

GC_CONCURRENT: 當(dāng)我們應(yīng)用程序的堆內(nèi)存快要滿的時(shí)候继蜡,系統(tǒng)會自動觸發(fā)GC操作來釋放內(nèi)存(同時(shí)發(fā)生)
GC_FOR_MALLOC: 當(dāng)我們的應(yīng)用程序需要分配更多內(nèi)存,可是現(xiàn)有內(nèi)存已經(jīng)不足的時(shí)候逛腿,系統(tǒng)會進(jìn)行GC
GC_HPROF_DUMP_HEAP: 當(dāng)生成HPROF文件的時(shí)候稀并,系統(tǒng)會進(jìn)行GC操作,關(guān)于HPROF文件我們下面會講到
GC_EXPLICIT: 這種情況就是我們剛才提到過的单默,主動通知系統(tǒng)去進(jìn)行GC操作碘举,比如調(diào)用System.gc()方法來通知系統(tǒng)「槔或者在DDMS中引颈,通過工具按鈕也是可以顯式地告訴系統(tǒng)進(jìn)行GC操作的

GC操作釋放了多少內(nèi)存

Heap_stats中會顯示當(dāng)前內(nèi)存的空閑比例以及使用情況(活動對象所占內(nèi)存 / 當(dāng)前程序總內(nèi)存)
Pause_time表示這次GC操作導(dǎo)致應(yīng)用程序暫停的時(shí)間耕皮。**關(guān)于這個(gè)暫停的時(shí)間,Android在2.3的版本當(dāng)中進(jìn)行過一次優(yōu)化蝙场,在2.3之前GC操作是不能并發(fā)進(jìn)行的凌停,也就是系統(tǒng)正在進(jìn)行GC,那么應(yīng)用程序就只能阻塞住等待GC結(jié)束售滤。雖說這個(gè)阻塞的過程并不會很長罚拟,也就是幾百毫秒,但是用戶在使用我們的程序時(shí)還是有可能會感覺到略微的卡頓
2.3之后趴泌,GC操作改成了并發(fā)的方式進(jìn)行舟舒,就是說GC的過程中不會影響到應(yīng)用程序的正常運(yùn)行,但是在GC操作的開始和結(jié)束的時(shí)候會短暫阻塞一段時(shí)間嗜憔,不過優(yōu)化到這種程度秃励,用戶已經(jīng)是完全無法察覺到了

導(dǎo)致GC頻繁執(zhí)行有兩個(gè)原因

  1. Memory Churn(內(nèi)存抖動),內(nèi)存抖動是因?yàn)榇罅康膶ο蟊粍?chuàng)建又在短時(shí)間內(nèi)馬上被釋放
盡量避免在循環(huán)體內(nèi)創(chuàng)建對象吉捶,應(yīng)該把對象創(chuàng)建移到循環(huán)體外夺鲜。
注意自定義View的onDraw()方法會被頻繁調(diào)用,所以在這里面不應(yīng)該頻繁的創(chuàng)建對象呐舔。
當(dāng)需要大量使用Bitmap的時(shí)候币励,試著把它們緩存在數(shù)組中實(shí)現(xiàn)復(fù)用。
對于能夠復(fù)用的對象珊拼,同理可以使用對象池將它們緩存起來
  1. 瞬間產(chǎn)生大量的對象會嚴(yán)重占用Young Generation的內(nèi)存區(qū)域食呻,當(dāng)達(dá)到閥值,剩余空間不夠的時(shí)候澎现,也會觸發(fā)GC仅胞。即使每次分配的對象占用了很少的內(nèi)存,但是他們疊加在一起會增加 Heap的壓力剑辫,從而觸發(fā)更多其他類型的GC干旧。這個(gè)操作有可能會影響到幀率,并使得用戶感知到性能問題

內(nèi)存泄漏的檢測與處理

Android Studio界面

一般分析內(nèi)存泄露, 首先運(yùn)行程序,打開日志控制臺,有一個(gè)標(biāo)簽Memory ,我們可以在這個(gè)界面分析當(dāng)前程序使用的內(nèi)存情況, 一目了然, 我們再也不需要苦苦的在logcat中尋找內(nèi)存的日志了

圖中藍(lán)色區(qū)域妹蔽,就是程序使用的內(nèi)存椎眯, 灰色區(qū)域就是空閑內(nèi)存, 當(dāng)然胳岂,Android內(nèi)存分配機(jī)制是對每個(gè)應(yīng)用程序逐步增加, 比如你程序當(dāng)前使用30M內(nèi)存, 系統(tǒng)可能會給你分配40M, 當(dāng)前就有10M空閑, 如果程序使用了50M了,系統(tǒng)會緊接著給當(dāng)前程序增加一部分,比如達(dá)到了80M编整, 當(dāng)前你的空閑內(nèi)存就是30M了。 當(dāng)然,系統(tǒng)如果不能再給你分配額外的內(nèi)存,程序自然就會OOM(內(nèi)存溢出)了乳丰。 每個(gè)應(yīng)用程序最高可以申請的內(nèi)存和手機(jī)密切相關(guān)闹击,比如我當(dāng)前使用的華為Mate7,極限大概是200M,算比較高的了, 一般128M 就是極限了, 甚至有的手機(jī)只有可憐的16M或者32M,這樣的手機(jī)相對于內(nèi)存溢出的概率非常大了


image.png

檢測內(nèi)存泄露

首先需要明白一個(gè)概念, 內(nèi)存泄露就是指,本應(yīng)該回收的內(nèi)存,還駐留在內(nèi)存中成艘。 一般情況下,高密度的手機(jī),一個(gè)頁面大概就會消耗20M內(nèi)存,如果發(fā)現(xiàn)退出界面,程序內(nèi)存遲遲不降低的話,可能就發(fā)生了嚴(yán)重的內(nèi)存泄露赏半。 我們可以反復(fù)進(jìn)入該界面,然后點(diǎn)擊dump Java heap 這個(gè)按鈕,然后Android Studio就開始干活了,下面的圖就是正在dump


image.png

dump成功后會自動打開 hprof文件,文件以Snapshot+時(shí)間來命名


image.png

image.png

MAT

通過Android Studio自帶的界面,查看內(nèi)存泄露還不是很智能,我們可以借助第三方工具,常見的工具就是MAT了,下載地址 https://eclipse.org/mat/downloads.php ,這里我們需要下載獨(dú)立版的MAT. 下圖是MAT一開始打開的界面, 這里需要提醒大家的是淆两,MAT并不會準(zhǔn)確地告訴我們哪里發(fā)生了內(nèi)存泄漏断箫,而是會提供一大堆的數(shù)據(jù)和線索,我們需要自己去分析這些數(shù)據(jù)來去判斷到底是不是真的發(fā)生了內(nèi)存泄漏秋冰。

image.png

接下來我們需要用MAT打開內(nèi)存分析的文件, 上文給大家介紹了使用Android Studio生成了 hprof文件, 這個(gè)文件在呢, 在Android Studio中的Captrues這個(gè)目錄中,可以找到


image.png

注意,這個(gè)文件不能直接交給MAT, MAT是不識別的, 我們需要右鍵點(diǎn)擊這個(gè)文件,轉(zhuǎn)換成MAT識別的仲义。

image.png

image.png

LeakCanary

LeakCanary會檢測應(yīng)用的內(nèi)存回收情況,如果發(fā)現(xiàn)有垃圾對象沒有被回收剑勾,就會去分析當(dāng)前的內(nèi)存快照埃撵,也就是上邊MAT用到的.hprof文件,找到對象的引用鏈虽另,并顯示在頁面上暂刘。這款插件的好處就是,可以在手機(jī)端直接查看內(nèi)存泄露的地方,可以輔助我們檢測內(nèi)存泄露


image.png

在build.gradle文件中添加,不同的編譯使用不同的引用:

dependencies {

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'

releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'

}

在應(yīng)用的Application onCreate方法中添加LeakCanary.install(this)捂刺,如下

public class ExampleApplication extends Application

@Override

public void onCreate() {

super.onCreate();

LeakCanary.install(this);

}

}

在應(yīng)用的Application onCreate方法中添加LeakCanary.install(this)谣拣,如下

dependencies {

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'

releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'

}



public class ExampleApplication extends Application

@Override

public void onCreate() {

super.onCreate();

LeakCanary.install(this);

}

}

追蹤內(nèi)存分配

如果我們想了解內(nèi)存分配更詳細(xì)的情況,可以使用Allocation Traker來查看內(nèi)存到底被什么占用了。 用法很簡單:


image.png

如果我們要觀測方法執(zhí)行的時(shí)間,就需要來到CPU界面

點(diǎn)擊Start Method Tracking, 一段時(shí)間后再點(diǎn)擊一次, trace文件被自動打開,
非獨(dú)占時(shí)間: 某函數(shù)占用的CPU時(shí)間,包含內(nèi)部調(diào)用其它函數(shù)的CPU時(shí)間族展。 獨(dú)占時(shí)間: 某函數(shù)占用CPU時(shí)間,但不含內(nèi)部調(diào)用其它函數(shù)所占用的CPU時(shí)間森缠。

我們?nèi)绾闻袛嗫赡苡袉栴}的方法?

通過方法的調(diào)用次數(shù)和獨(dú)占時(shí)間來查看,通常判斷方法是:

如果方法調(diào)用次數(shù)不多仪缸,但每次調(diào)用卻需要花費(fèi)很長的時(shí)間的函數(shù)贵涵,可能會有問題。

如果自身占用時(shí)間不長恰画,但調(diào)用卻非常頻繁的函數(shù)也可能會有問題宾茂。


image.png

常見內(nèi)存泄漏

單例(Singleton)

為了完美解決我們在程序中反復(fù)創(chuàng)建同一對象的問題,我們選用了單例模式锣尉,單例在我們的程序中隨處可見刻炒,但是由于單例模式的靜態(tài)特性,使得它的生命周期和我們的應(yīng)用一樣長自沧,一不小心讓單例無限制的持有Activity的強(qiáng)引用就會導(dǎo)致內(nèi)存泄漏坟奥。例如:

public class SingleTon{

    private Context context;

    private static SingleTon singleTon;

    public static final SingleTon getInstance(Context context){

        this.context = context;

        return SingleHolder.INSTANCE;

    }

    private static class SingleHolder{

        private static final SingleTon INSTANCE = new SingleTon();

    }

}

運(yùn)行到手機(jī):


image.png

轉(zhuǎn)屏后多出來一些實(shí)際占用內(nèi)存,5.48 -4.66 = 0.82M內(nèi)存拇厢,如下:


image.png

尋找問題

解決辦法:

這個(gè)錯(cuò)誤很普遍爱谁,這個(gè)是一個(gè)很正常的單利模式,但是由于傳入了一個(gè)Context孝偎,而這個(gè)Context的生命周期就的長短就尤為重要了访敌。如果我們傳入的是某個(gè)Activity的Context,而當(dāng)這個(gè)Activity推出的時(shí)候衣盾,由于該Context的強(qiáng)引用被單例持有寺旺,那么這個(gè)Activity就等同于擁有了整個(gè)程序的生命周期爷抓。這種情況下,當(dāng)Activity退出的時(shí)候內(nèi)存并沒有被回收阻塑,這就造成了內(nèi)存泄漏蓝撇。

正確的做法就是應(yīng)該把傳入的Context改為同應(yīng)用生命周期一樣長的Application中的Context。

public class BaseApplication extends Application{

    private static BaseApplication baseApplication;

    @Override

    public void onCreate(){

        super.onCreate();

        baseApplication = this;

    }

    public static Context getContext{

        baseApplication.getApplicationContext();

    }

}

當(dāng)然我們可以直接重寫Application陈莽,提供getContext方法,不必在依靠傳入的參數(shù):

  public static final SingleTon getInstance(Context context) {

        this.context = context.getApplicationContext;

        return SingleHolder.INSTANCE;

  }

Handler引起的內(nèi)存泄漏

Handler引起的內(nèi)存泄漏在我們開發(fā)中最為常見的渤昌。我們知道Handler、Message走搁、MessageQueue都是相互關(guān)聯(lián)在一起的独柑,萬一Handler發(fā)送的Message尚未被處理,那么該Message以及發(fā)送它的Handler對象都會被線程MessageQueue一直持有私植。

由于Handler屬于TLS(Thread Local Storage)變量忌栅,生命周期和Activity是不一致的,因此這種實(shí)現(xiàn)方式很難保證跟Activity的生命周期一直兵琳,所以很容易無法釋放內(nèi)存狂秘。比如:

 public class HandlerBadActivity extends AppCompatActivity {

    private final Handler handler = new Handler(){

       @Override

      public void handleMessage(Message msg) {

          super.handleMessage(msg);

     }

     };

     @Override

     protected void onCreate(Bundle savedInstanceState) {

         super.onCreate(savedInstanceState);

         setContentView(R.layout.activity_handler_bad); 

         // 延遲5min發(fā)送一個(gè)消息

         handler.postDelayed(new Runnable() {

             @Override

             public void run() {

                 // write something

             }

         },1000*60*5);

         this.finish();

     }

 }

我們在例子中生命了一個(gè)延時(shí)5分鐘執(zhí)行的Message,當(dāng)該Activity退出的時(shí)候躯肌,延時(shí)任務(wù)(Message)還在主線成的MessageQueue中等待者春,此時(shí)的Message持有Handler的強(qiáng)引用,并且由于Handler是HandlerBadActivity的非靜態(tài)內(nèi)部類清女,所以Handler會持有HandlerBadActivity的強(qiáng)引用钱烟,此時(shí)HandlerBadActivity退出時(shí)無法進(jìn)行內(nèi)存回收,造成內(nèi)存泄漏嫡丙。

解決辦法:

將Handler生命為靜態(tài)內(nèi)部類拴袭,這樣它就不會持有外部來的引用了。這樣以來Handler的的生命周期就與Activity無關(guān)了曙博。不過倘若用到Context等外部類的非static對象拥刻,還是應(yīng)該通過使用Application中與應(yīng)用同生命周期的Context比較合適。比如:

    public class HandlerGoodActivity extends AppCompatActivity {

    private static final class MyHandler extends Handler {

        private Context mActivity;

        public MyHandler(HandlerGoodActivity activity) {

            //使用生命周期與應(yīng)用同長的getApplicationContext

            this.mActivity = activity.getApplicationContext();

        }

        @Override

        public void handleMessage(Message msg) {

            super.handleMessage(msg);

            if (mActivity != null) {

                // write something

            }

        }

    }

    private final MyHandler myHandler = new MyHandler(this);

    // 匿名內(nèi)部類在static的時(shí)候絕對不會持有外部類的引用

    private static final Runnable RUNNABLE = new Runnable() {

        @Override

        public void run() {

        }

    };

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_handler_good);

        myHandler.postDelayed(RUNNABLE, 1000 * 60 * 5);

    }

雖然我們結(jié)局了Activity的內(nèi)存泄漏問題父泳,但是經(jīng)過Handler發(fā)送的延時(shí)消息還在MessageQueue中般哼,Looper也在等待處理消息,所以我們要在Activity銷毀的時(shí)候處理掉隊(duì)列中的消息惠窄。

   @Override

    protected void onDestroy() {

        super.onDestroy();

        //傳入null蒸眠,就表示移除所有Message和Runnable

        myHandler.removeCallbacksAndMessages(null);

    }

匿名內(nèi)部類在異步線程中的使用

它們方便卻暗藏殺機(jī)。Android開發(fā)經(jīng)常會繼承實(shí)現(xiàn) Activity 或者 Fragment 或者 View杆融。如果你使用了匿名類楞卡,而又被異步線程所引用,那得小心,如果沒有任何措施同樣會導(dǎo)致內(nèi)存泄漏的:

public class MainActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_inner_bad);

    Runnable runnable1 = new MyRunnable();

    Runnable runnable2 = new Runnable() {

        @Override

        public void run() {

        }

    };

}

private static class MyRunnable implements Runnable{

    @Override

    public void run() {

    }

}

}

runnable1 和 runnable2的區(qū)別就是蒋腮,runnable2使用了匿名內(nèi)部類淘捡,我們看看引用時(shí)的引用內(nèi)存


image.gif

可以看到,runnable1是沒有什么特別的池摧。但runnable2多出了一個(gè)MainActivity的引用案淋,若是這個(gè)引用再傳入到一個(gè)異步線程,此線程在和Activity生命周期不一致的時(shí)候险绘,也就造成了Activity的泄露。

善用static成員變量

從前面的介紹我們知道誉碴,static修飾的變量位于內(nèi)存的靜態(tài)存儲區(qū)宦棺,此變量與App的生命周期一致
這必然會導(dǎo)致一系列問題,如果你的app進(jìn)程設(shè)計(jì)上是長駐內(nèi)存的黔帕,那即使app切到后臺代咸,這部分內(nèi)存也不會被釋放。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制成黄,占內(nèi)存較大的后臺進(jìn)程將優(yōu)先回收呐芥,因?yàn)槿绻薬pp做過進(jìn)程互保保活奋岁,那會造成app在后臺頻繁重啟思瘟。當(dāng)手機(jī)安裝了你參與開發(fā)的app以后一夜時(shí)間手機(jī)被消耗空了電量、流量闻伶,你的app不得不被用戶卸載或者靜默滨攻。

這里修復(fù)的方法是:
不要在類初始時(shí)初始化靜態(tài)成員±逗玻可以考慮lazy初始化(延遲加載)光绕。架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做,盡量避免畜份。如果架構(gòu)需要這么設(shè)計(jì)诞帐,那么此對象的生命周期你有責(zé)任管理起來。

避免使用

在我們的日常代碼中爆雹,這樣的情況似乎很常見停蕉,及直接寫一個(gè)class就這么光禿禿的情況


image.png

這樣就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動Activity時(shí)都會使用該單例的數(shù)據(jù)顶别,這樣雖然避免了資源的重復(fù)創(chuàng)建谷徙,不過這種寫法卻會造成內(nèi)存泄漏,因?yàn)榉庆o態(tài)內(nèi)部類默認(rèn)會持有外部類的引用驯绎,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個(gè)靜態(tài)的實(shí)例完慧,該實(shí)例的生命周期和應(yīng)用的一樣長,這就導(dǎo)致了該靜態(tài)實(shí)例一直會持有該Activity的引用剩失,導(dǎo)致Activity的內(nèi)存資源不能正城幔回收册着。正確的做法為:

將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例,如果需要使用Context脾歧,請按照上面推薦的使用Application 的 Context甲捏。當(dāng)然,Application 的 context 不是萬能的鞭执,所以也不能隨便亂用司顿,對于有些地方則必須使用 Activity 的 Context,對于Application兄纺,Service大溜,Activity三者的Context的應(yīng)用場景如下:

image.png

其中: NO1表示 Application 和 Service 可以啟動一個(gè) Activity,不過需要創(chuàng)建一個(gè)新的 task 任務(wù)隊(duì)列估脆。而對于 Dialog 而言钦奋,只有在 Activity 中才能創(chuàng)建

集合引發(fā)的內(nèi)存泄漏

我們通常會把一些對象的引用加入到集合容器(比如ArrayList)中,當(dāng)我們不再需要該對象時(shí)疙赠,并沒有把它的引用從集合中清理掉付材,當(dāng)集合中的內(nèi)容過于大的時(shí)候,并且是static的時(shí)候就造成了內(nèi)存泄漏圃阳,所有我們最好在onDestory情況并讓其不可達(dá)

private List<String> nameList;

    private List<Fragment> list;

    @Override

    public void onDestroy() {

        super.onDestroy();

        if (nameList != null){

            nameList.clear();

            nameList = null;

        }

        if (list != null){

            list.clear();

            list = null;

        }

    }

webView引發(fā)的內(nèi)存泄漏

WebView解析網(wǎng)頁時(shí)會申請Native堆內(nèi)存用于保存頁面元素厌衔,當(dāng)頁面較復(fù)雜時(shí)會有很大的內(nèi)存占用。如果頁面包含圖片限佩,內(nèi)存占用會更嚴(yán)重葵诈。并且打開新頁面時(shí)妙黍,為了能快速回退辐啄,之前頁面占用的內(nèi)存也不會釋放。有時(shí)瀏覽十幾個(gè)網(wǎng)頁霞势,都會占用幾百兆的內(nèi)存晕城。這樣加載網(wǎng)頁較多時(shí)泞坦,會導(dǎo)致系統(tǒng)不堪重負(fù),最終強(qiáng)制關(guān)閉應(yīng)用砖顷,也就是出現(xiàn)應(yīng)用閃退或重啟贰锁。

由于占用的都是Native堆內(nèi)存,所以實(shí)際占用的內(nèi)存大小不會顯示在常用的DDMS Heap工具中(這里看到的只是Java虛擬機(jī)分配的內(nèi)存滤蝠,一般即使Native堆內(nèi)存已經(jīng)占用了幾百兆豌熄,這里顯示的還只是幾兆或十幾兆)。只有使用adb shell中的一些命令比如dumpsys meminfo 包名物咳,或者在程序中使用Debug.getNativeHeapSize()才能看到锣险。
具體可以參考下 我的另一個(gè)adb看內(nèi)存:http://www.reibang.com/writer#/notebooks/11604270/notes/27577456
據(jù)說由于WebView的一個(gè)BUG,即使它所在的Activity(或者Service)結(jié)束也就是onDestroy()之后,或者直接調(diào)用WebView.destroy()之后芯肤,它所占用這些內(nèi)存也不會被釋放巷折。

解決這個(gè)問題最直接的方法是:把使用了WebView的Activity(或者Service)放在單獨(dú)的進(jìn)程里。然后在檢測到應(yīng)用占用內(nèi)存過大有可能被系統(tǒng)干掉或者它所在的Activity(或者Service)結(jié)束后崖咨,調(diào)用System.exit(0)锻拘,主動Kill掉進(jìn)程。由于系統(tǒng)的內(nèi)存分配是以進(jìn)程為準(zhǔn)的击蹲,進(jìn)程關(guān)閉后署拟,系統(tǒng)會自動回收所有內(nèi)存。

關(guān)于WebView的跟多內(nèi)容請參見 : Android WebView Memory Leak WebView內(nèi)存泄漏

其他常見的引起內(nèi)存泄漏原因

  • 構(gòu)造Adapter時(shí)歌豺,沒有使用緩存的 convertView

  • Bitmap在不使用的時(shí)候沒有使用recycle()釋放內(nèi)存

  • 非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例容易造成內(nèi)存泄漏:即一個(gè)類中如果你不能夠控制它其中內(nèi)部類的生命周期(譬如Activity中的一些特殊Handler等)芯丧,則盡量使用靜態(tài)類和弱引用來處理(譬如ViewRoot的實(shí)現(xiàn))。

  • 警惕線程未終止造成的內(nèi)存泄露世曾;譬如在Activity中關(guān)聯(lián)了一個(gè)生命周期超過Activity的Thread,在退出Activity時(shí)切記結(jié)束線程谴咸。一個(gè)典型的例子就是HandlerThread的run方法是一個(gè)死循環(huán)轮听,它不會自己結(jié)束,線程的生命周期超過了Activity生命周期岭佳,我們必須手動在Activity的銷毀方法中中調(diào)運(yùn)thread.getLooper().quit();才不會泄露血巍。

  • 對象的注冊與反注冊沒有成對出現(xiàn)造成的內(nèi)存泄露;譬如注冊廣播接收器珊随、注冊觀察者(典型的譬如數(shù)據(jù)庫的監(jiān)聽)等述寡。

  • 創(chuàng)建與關(guān)閉沒有成對出現(xiàn)造成的泄露;譬如Cursor資源必須手動關(guān)閉叶洞,WebView必須手動銷毀鲫凶,流等對象必須手動關(guān)閉等。

  • 不要在執(zhí)行頻率很高的方法或者循環(huán)中創(chuàng)建對象(比如onMeasure)衩辟,可以使用HashTable等創(chuàng)建一組對象容器從容器中取那些對象螟炫,而不用每次new與釋放。

  • 避免代碼設(shè)計(jì)模式的錯(cuò)誤造成內(nèi)存泄露艺晴;譬如循環(huán)引用昼钻,A持有B,B持有C封寞,C持有A然评,這樣的設(shè)計(jì)誰都得不到釋放。

總結(jié)

  • Android內(nèi)存優(yōu)化主要是針對堆(Heap)而言的狈究,當(dāng)堆中對象的作用域結(jié)束的時(shí)候碗淌,這部分內(nèi)存也不會立刻被回收,而是等待系統(tǒng)GC進(jìn)行回收。

  • Java中造成內(nèi)存泄漏的根本原因是:堆內(nèi)存中長生命周期的對象持有短生命周期對象的強(qiáng)/軟引用贯莺,盡管短生命周期對象已經(jīng)不再需要风喇,但是因?yàn)殚L生命周期對象持有它的引用而導(dǎo)致不能被回收。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缕探,一起剝皮案震驚了整個(gè)濱河市魂莫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爹耗,老刑警劉巖耙考,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異潭兽,居然都是意外死亡倦始,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門山卦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞋邑,“玉大人,你說我怎么就攤上這事账蓉∶锻耄” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵铸本,是天一觀的道長肮雨。 經(jīng)常有香客問我,道長箱玷,這世上最難降的妖魔是什么怨规? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮锡足,結(jié)果婚禮上波丰,老公的妹妹穿的比我還像新娘。我一直安慰自己舶得,他們只是感情好呀舔,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扩灯,像睡著了一般媚赖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上珠插,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天惧磺,我揣著相機(jī)與錄音,去河邊找鬼捻撑。 笑死磨隘,一個(gè)胖子當(dāng)著我的面吹牛缤底,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播番捂,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼个唧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了设预?” 一聲冷哼從身側(cè)響起徙歼,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鳖枕,沒想到半個(gè)月后魄梯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宾符,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年酿秸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片魏烫。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辣苏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出哄褒,到底是詐尸還是另有隱情考润,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布读处,位于F島的核電站,受9級特大地震影響唱矛,放射性物質(zhì)發(fā)生泄漏罚舱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一绎谦、第九天 我趴在偏房一處隱蔽的房頂上張望管闷。 院中可真熱鬧,春花似錦窃肠、人聲如沸包个。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碧囊。三九已至,卻和暖如春纤怒,著一層夾襖步出監(jiān)牢的瞬間糯而,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工泊窘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留熄驼,地道東北人像寒。 一個(gè)月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像瓜贾,于是被迫代替她去往敵國和親诺祸。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容