Android性能優(yōu)化大法——內(nèi)存優(yōu)化

作者:layz4android

內(nèi)存,是Android應(yīng)用的生命線檬嘀,一旦在內(nèi)存上出現(xiàn)問(wèn)題槽驶,輕者內(nèi)存泄漏,重者直接crash鸳兽,因此一個(gè)應(yīng)用保持健壯掂铐,內(nèi)存這塊的工作是持久戰(zhàn),而且從寫代碼這塊就需要注意合理性揍异,所以想要了解內(nèi)存優(yōu)化如何去做全陨,要先從基礎(chǔ)知識(shí)開始。

1 JVM內(nèi)存原理

這一部分確實(shí)很枯燥衷掷,但是對(duì)于我們理解內(nèi)存模型非常重要烤镐,這一塊也是面試的常客

從上圖中棍鳖,我將JVM的內(nèi)存模塊分成了左右兩大部分,左邊屬于共享區(qū)域(方法區(qū)碗旅、堆區(qū))渡处,所有的線程都能夠訪問(wèn),但也會(huì)帶來(lái)同步問(wèn)題祟辟,這里就不細(xì)說(shuō)了医瘫;右邊屬于私有區(qū)域,每個(gè)線程都有自己獨(dú)立的區(qū)域旧困。

1.1 方法執(zhí)行流程

class MainActivity : AppCompatActivity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)
 execute()
 }

 private fun execute(){

 val a = 2.5f
 val b = 2.5f
 val c = a + b

 val method = Method()

 val d = getD()
}

 private fun getD(): Int {
 return 0
 }

}

class Method{
 private var a:Int = 0
}

我們看到在MainActivity的onCreate方法中醇份,執(zhí)行了execute方法,因?yàn)楫?dāng)前是UI線程吼具,每個(gè)線程都有一個(gè)Java虛擬機(jī)棧僚纷,從上圖中可以看到,那么每執(zhí)行一個(gè)方法拗盒,在Java虛擬機(jī)棧中都對(duì)應(yīng)一個(gè)棧幀怖竭。

每次調(diào)用一個(gè)方法,都代表一個(gè)棧幀入棧陡蝇,當(dāng)onCreate方法執(zhí)行完成之后痊臭,會(huì)執(zhí)行execute方法,那么我們看下execute方法登夫。

execute方法在Java虛擬機(jī)棧中代表一個(gè)棧幀广匙,棧幀是由四部分組成:

(1)局部變量表:局部變量是聲明在方法體內(nèi)的,例如a恼策,b鸦致,c,在方法執(zhí)行完成之后,也會(huì)被回收蹋凝; (2)操作數(shù)棧:在任意方法中鲁纠,涉及到變量之間運(yùn)算等操作都是在操作數(shù)棧中進(jìn)行;例如execute方法中:

val a = 2.5f

當(dāng)執(zhí)行這句代碼時(shí)鳍寂,首先會(huì)將 2.5f壓入操作數(shù)棧改含,然后給a賦值,依次類推
(3)返回地址:例如在execute調(diào)用了getD方法迄汛,那么這個(gè)方法在執(zhí)行到到return的時(shí)候就結(jié)束了捍壤,當(dāng)一個(gè)方法結(jié)束之后,就要返回到該方法的被調(diào)用處鞍爱,那么該方法就攜帶一個(gè)返回地址鹃觉,告訴JVM給誰(shuí)賦值,然后通過(guò)操作數(shù)棧給d賦值
(4)動(dòng)態(tài)鏈接:在execute方法中睹逃,實(shí)例化了Method類盗扇,在這里,首先會(huì)給Method中的一些靜態(tài)變量或者方法進(jìn)行內(nèi)存分配沉填,這個(gè)過(guò)程可以理解為動(dòng)態(tài)鏈接疗隶。

1.2 從單例模式了解對(duì)象生命周期

單例模式,可能是眾多設(shè)計(jì)模式中翼闹,我們使用最頻繁的一個(gè)斑鼻,但是單例真是就這么簡(jiǎn)單嗎,使用不慎就會(huì)造成內(nèi)存泄漏猎荠!

interface IObserver {

    fun send(msg:String)

}

class Observable : IObserver {

    private val observers: MutableList<IObserver> by lazy {
        mutableListOf()
    }

    fun register(observer: IObserver) {
        observers.add(observer)
    }

    fun unregister(observer: IObserver) {
        observers.remove(observer)
    }

    override fun send(msg: String) {
        observers.forEach {
            it.send(msg)
        }
    }

    companion object {
        val instance: Observable by lazy {
            Observable()
        }
    }
}

這里是寫了一個(gè)觀察者坚弱,這個(gè)被觀察者是一個(gè)單例,instance是存放在方法區(qū)中关摇,而創(chuàng)建的Observable對(duì)象則是存在堆區(qū)荒叶,看下圖

因?yàn)榉椒▍^(qū)屬于常駐內(nèi)存,那么其中的instance引用會(huì)一直跟堆區(qū)的Observable連接输虱,導(dǎo)致這個(gè)單例對(duì)象會(huì)存在很長(zhǎng)的時(shí)間

btnRegister.setOnClickListener {
    Observable.instance.register(this)
}
btnSend.setOnClickListener {
    Observable.instance.send("發(fā)送消息")
}

在MainActivity中停撞,點(diǎn)擊注冊(cè)按鈕,注意這里傳入的值悼瓮,是當(dāng)前Activity戈毒,那么這個(gè)時(shí)候退出,會(huì)發(fā)生什么横堡?我們先從profile工具里看一下埋市,退出之后,有2個(gè)內(nèi)存泄漏的地方命贴,如果使用的leakcannary(后面會(huì)介紹)就應(yīng)該會(huì)明白

那么在MainActivity中道宅,哪個(gè)地方發(fā)生的了內(nèi)存泄漏呢食听?我們緊跟一下看看GcRoot的引用,發(fā)現(xiàn)有這樣一條引用鏈污茵,MainActivity在一個(gè)list數(shù)組中樱报,而且這個(gè)數(shù)組是Observable中的observers,而且是被instance持有泞当,前面我們說(shuō)到迹蛤,instance的生命周期很長(zhǎng),所以當(dāng)Activity準(zhǔn)備被銷毀時(shí)襟士,發(fā)現(xiàn)被instance持有導(dǎo)致回收失敗盗飒,發(fā)生了內(nèi)存泄漏。

那么這種情況陋桂,我們?cè)撛趺刺幚砟啬嫒ぃ恳话銇?lái)說(shuō),有注冊(cè)就有解注冊(cè)嗜历,所以我們?cè)诜庋b的時(shí)候一定要注意單例中傳入的參數(shù)

override fun onDestroy() {
    super.onDestroy()
    Observable.instance.unregister(this)
}

再次運(yùn)行我們發(fā)現(xiàn)宣渗,已經(jīng)不存在內(nèi)存泄漏了

1.3 GcRoot

前面我們提到了,因?yàn)閕nstance是Gcroot梨州,導(dǎo)致其引用了observers痕囱,observers引用了MainActivity,MainActivity退出的時(shí)候沒有被回收摊唇,那么什么樣的對(duì)象能被看做是GcRoot呢?

(1)靜態(tài)變量涯鲁、常量:例如instance巷查,其內(nèi)存是在方法區(qū)的,在方法區(qū)一般存儲(chǔ)的都是靜態(tài)的常量或者變量抹腿,其生命周期非常長(zhǎng)岛请;
(2)局部變量表:在Java虛擬機(jī)棧的棧幀中,存在局部變量表警绩,為什么局部變量表能作為gcroot崇败,原因很簡(jiǎn)單,我們看下面這個(gè)方法

private fun execute() {

    val a = 2.5f
    val method = Method()
    val d = getD()
}

a變量就是一個(gè)局部變量表中的成員肩祥,我們想一下后室,如果a不是gcroot,那么垃圾回收時(shí)就有可能被回收混狠,那么這個(gè)方法還有什么意義呢岸霹?所以當(dāng)這個(gè)方法執(zhí)行完成之后,gcroot被回收将饺,其引用也會(huì)被回收贡避。

2 OOM

在之前我們簡(jiǎn)單介紹了內(nèi)存泄漏的場(chǎng)景痛黎,那么內(nèi)存泄漏一旦發(fā)生,就會(huì)導(dǎo)致OOM嗎刮吧?其實(shí)并不是湖饱,內(nèi)存泄漏一開始并不會(huì)導(dǎo)致OOM,而是逐漸累計(jì)的杀捻,當(dāng)內(nèi)存空間不足時(shí)井厌,會(huì)造成卡頓、耗電等不良體驗(yàn)水醋,最終就會(huì)導(dǎo)致OOM旗笔,app崩潰

那么什么情況下會(huì)導(dǎo)致OOM呢?
(1)Java堆內(nèi)存不足
(2)沒有連續(xù)的內(nèi)存空間
(3)線程數(shù)超出限制

其實(shí)以上3種狀況拄踪,前兩種都有可能是內(nèi)存泄漏導(dǎo)致的蝇恶,所以如何避免內(nèi)存泄漏,是我們內(nèi)存優(yōu)化的重點(diǎn)

2.1 leakcanary使用

首先在module中引入leakcanary的依賴惶桐,關(guān)于leakcanary的原理撮弧,之后會(huì)單獨(dú)寫一篇博客介紹,這里我們的主要工作是分析內(nèi)存泄漏

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'

配置依賴之后姚糊,重新運(yùn)行項(xiàng)目贿衍,會(huì)看到一個(gè)leaks app,這個(gè)app就是用來(lái)監(jiān)控內(nèi)存泄漏的工具

那我們執(zhí)行之前的應(yīng)用救恨,打開leaks看一下gcroot的引用贸辈,是不是跟我們?cè)赼s的profiler中看到的是一樣的

如果使用過(guò)leakcanary的伙伴們應(yīng)該知道,leakcanary會(huì)生成一個(gè)hprof文件肠槽,那么通過(guò)MAT工具擎淤,可以分析這個(gè)hprof文件,查找內(nèi)存泄漏的位置秸仙,下面的鏈接能夠下載MAT工具 www.eclipse.org/mat/downloa

2.2 內(nèi)存泄漏的場(chǎng)景

1. 資源性的對(duì)象沒有關(guān)閉

例如嘴拢,我們?cè)谧鲆粋€(gè)相機(jī)模塊,通過(guò)camera拿到了一幀圖片寂纪,通常我們會(huì)將其轉(zhuǎn)換為bitmap席吴,在使用完成之后,如果沒有將其回收捞蛋,那么就會(huì)造成內(nèi)存泄漏孝冒,具體使用完該怎么辦呢?

if(bitmap != null){
    bitmap?.recycle()
    bitmap = null
}

調(diào)用bitmap的recycle方法拟杉,然后將bitmap置為null

2. 注冊(cè)的對(duì)象沒有注銷

這種場(chǎng)景其實(shí)我們已經(jīng)很常見了迈倍,在之前也提到過(guò),就是注冊(cè)跟反注冊(cè)要成對(duì)出現(xiàn)捣域,例如我們?cè)谧?cè)廣播接收器的時(shí)候啼染,一定要記得宴合,在Activity銷毀的時(shí)候去解注冊(cè),具體使用方式就不做過(guò)多的贅述迹鹅。

3. 類的靜態(tài)變量持有大數(shù)據(jù)量對(duì)象

因?yàn)槲覀冎镭郧ⅲ惖撵o態(tài)變量是存儲(chǔ)在方法區(qū)的,方法區(qū)空間有限而且生命周期長(zhǎng)斜棚,如果持有大數(shù)據(jù)量對(duì)象阀蒂,那么很難被gc回收,如果再次向方法區(qū)分配內(nèi)存弟蚀,會(huì)導(dǎo)致沒有足夠的空間分配蚤霞,從而導(dǎo)致OOM

4. 單例造成的內(nèi)存泄漏

這個(gè)我們?cè)谇懊嬉呀?jīng)有一個(gè)詳細(xì)的介紹,因?yàn)槲覀冊(cè)谑褂脝卫臅r(shí)候义钉,經(jīng)常會(huì)傳入context或者activity對(duì)象昧绣,因?yàn)橛猩舷挛牡拇嬖冢瑢?dǎo)致單例持有不能被銷毀捶闸;

因此在傳入context的時(shí)候夜畴,可以傳入Application的context,那么單例就不會(huì)持有activity的上下文可以正常被回收删壮;

如果不能傳入Application的context贪绘,那么可以通過(guò)弱引用包裝context,使用的時(shí)候從弱引用中取出央碟,但這樣會(huì)存在風(fēng)險(xiǎn)税灌,因?yàn)槿跻每赡茈S時(shí)被系統(tǒng)回收,如果在某個(gè)時(shí)刻必須要使用context亿虽,可能會(huì)帶來(lái)額外的問(wèn)題菱涤,因此根據(jù)不同的場(chǎng)景謹(jǐn)慎使用。

object ToastUtils {

    private var context:Context? = null

    fun setText(context: Context) {
        this.context = context
        Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
    }

}

我們看下上面的代碼经柴,ToastUtils是一個(gè)單例狸窘,我們?cè)谕膺厡懥艘粋€(gè)context:Context? 的引用墩朦,這種寫法是非常危險(xiǎn)的坯认,因?yàn)門oastUtils會(huì)持有context的引用導(dǎo)致內(nèi)存泄漏

object ToastUtils {

    private var context:Context? = null

    fun setText(context: Context) {
        this.context = context
        Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
    }

}

5. 非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例

我們先了解下什么是靜態(tài)內(nèi)部類和非靜態(tài)內(nèi)部類,首先只有內(nèi)部類才能設(shè)置為靜態(tài)類氓涣,例如

class MainActivity : AppCompatActivity() {

    private var a = 10

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
    }

    inner class InnerClass {
        fun setA(code: Int) {
            a = code
        }
    }
}

InnerClass是一個(gè)非靜態(tài)內(nèi)部類牛哺,那么在MainActivity聲明了一個(gè)變量a,其實(shí)InnerClass是能夠拿到這個(gè)變量劳吠,也就是說(shuō)引润,非靜態(tài)內(nèi)部類其實(shí)是對(duì)外部類有一個(gè)隱式持有,那么它的靜態(tài)實(shí)例對(duì)象是存儲(chǔ)在方法區(qū)痒玩,而且該對(duì)象持有MainActivity的引用淳附,導(dǎo)致退出時(shí)無(wú)法被釋放议慰。

解決方式就是:將InnerClass設(shè)置為靜態(tài)類

class InnerClass {

    fun setA(code: Int) {
        a = code //這里就無(wú)法使用外部類的對(duì)象或者方法
    }
}

大家如果對(duì)于kotlin不熟悉的話,就簡(jiǎn)單介紹一下奴曙,inner class在java中就是非靜態(tài)的內(nèi)部類别凹;而直接用class修飾,那么就相當(dāng)于Java中的 public static 靜態(tài)內(nèi)部類洽糟。

6. Handler

這個(gè)可就是老生常談了炉菲,如果使用過(guò)Handler的話都知道,它非常容易產(chǎn)生內(nèi)存泄漏坤溃,具體的原理就不說(shuō)了拍霜,感覺現(xiàn)在用Handler真的越來(lái)越少了

其實(shí)說(shuō)了這么多,真正在寫代碼的時(shí)候薪介,不能真正的避免祠饺,接下來(lái)我就使用leakcanary來(lái)檢測(cè)某個(gè)項(xiàng)目中存在的內(nèi)存泄漏問(wèn)題,并解決

3 從實(shí)際項(xiàng)目出發(fā)昭灵,根除內(nèi)存泄漏

1. 單例引發(fā)的內(nèi)存泄漏

我們從gcroot中可以看到吠裆,在TeachAidsCaptureImpl中傳入了LifeCycleOwner,LifeCycleOwner大家應(yīng)該熟悉烂完,能夠監(jiān)聽Activity或者Fragment的生命周期试疙,然后CaptureModeManager是一個(gè)單例,傳入的mode就是TeachAidsCaptureImpl抠蚣,這樣就會(huì)導(dǎo)致一個(gè)問(wèn)題祝旷,單例的生命周期很長(zhǎng),F(xiàn)ragment被銷毀的時(shí)候因?yàn)門eachAidsCaptureImpl持有了Fragment的引用嘶窄,導(dǎo)致無(wú)法銷毀

fun clear() {
    if (mode != null) {
        mode = null
    }
}

所以怀跛,在Activity或者Fragment銷毀前,將model置為空柄冲,那么內(nèi)存泄漏就會(huì)解決了吻谋,直到看到這個(gè)界面,那么我們的應(yīng)用就是安全的了

2.使用Toast引發(fā)的內(nèi)存泄漏

在我們使用Toast的時(shí)候现横,需要傳入一個(gè)上下文漓拾,我們通常會(huì)傳入Activity,那么這個(gè)上下文給誰(shuí)用的呢戒祠,在Toast中也有View骇两,如果我們自定過(guò)Toast應(yīng)該知道,那么如果Toast中的View持有了Activity的引用姜盈,那么就會(huì)導(dǎo)致內(nèi)存泄漏

Toast.makeText(this,"Toast內(nèi)存泄漏",Toast.LENGTH_SHORT).show()

那么怎樣避免呢低千?傳入Application的上下文,就不會(huì)導(dǎo)致Activity不被回收馏颂。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末示血,一起剝皮案震驚了整個(gè)濱河市棋傍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌难审,老刑警劉巖舍沙,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異剔宪,居然都是意外死亡拂铡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門葱绒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)感帅,“玉大人,你說(shuō)我怎么就攤上這事地淀∈颍” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵帮毁,是天一觀的道長(zhǎng)实苞。 經(jīng)常有香客問(wèn)我,道長(zhǎng)烈疚,這世上最難降的妖魔是什么黔牵? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮爷肝,結(jié)果婚禮上猾浦,老公的妹妹穿的比我還像新娘。我一直安慰自己灯抛,他們只是感情好金赦,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著对嚼,像睡著了一般夹抗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纵竖,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天漠烧,我揣著相機(jī)與錄音,去河邊找鬼磨确。 笑死沽甥,一個(gè)胖子當(dāng)著我的面吹牛声邦,可吹牛的內(nèi)容都是我干的乏奥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼亥曹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼邓了!你這毒婦竟也來(lái)了恨诱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤骗炉,失蹤者是張志新(化名)和其女友劉穎照宝,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體句葵,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厕鹃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乍丈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剂碴。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖轻专,靈堂內(nèi)的尸體忽然破棺而出忆矛,到底是詐尸還是另有隱情,我是刑警寧澤请垛,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布催训,位于F島的核電站,受9級(jí)特大地震影響宗收,放射性物質(zhì)發(fā)生泄漏漫拭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一混稽、第九天 我趴在偏房一處隱蔽的房頂上張望嫂侍。 院中可真熱鬧,春花似錦荚坞、人聲如沸挑宠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)各淀。三九已至,卻和暖如春诡挂,著一層夾襖步出監(jiān)牢的瞬間碎浇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工璃俗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奴璃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓城豁,卻偏偏與公主長(zhǎng)得像苟穆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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