Android?內(nèi)存優(yōu)化知識(shí)點(diǎn)梳理總結(jié)
前言:
Android 操作系統(tǒng)給每個(gè)進(jìn)程都會(huì)分配指定額度的內(nèi)存空間,App 使用內(nèi)存來(lái)進(jìn)行快速的文件訪(fǎng)問(wèn)交互。例如展示網(wǎng)絡(luò)圖片時(shí),就是通過(guò)把網(wǎng)絡(luò)圖片下載到內(nèi)存中展示,如果需要保存到本地,再?gòu)膬?nèi)存中保存到磁盤(pán)空間中。
RAM 和 ROM
手機(jī)一般有兩種存儲(chǔ)介質(zhì),一個(gè)是 RAM ,我們常說(shuō)的內(nèi)存,也稱(chēng)之為運(yùn)行內(nèi)存;另一個(gè)是 ROM ,即磁盤(pán)空間。 RAM 的訪(fǎng)問(wèn)速度一般會(huì)比 ROM 快,它是即插即用,斷電會(huì)抹除所有數(shù)據(jù),RAM 越大,可同時(shí)操作的數(shù)據(jù)就越多;ROM 是外部存儲(chǔ)空間,相當(dāng)于電腦的硬盤(pán),主要是用來(lái)存儲(chǔ)本地?cái)?shù)據(jù)的。
App 運(yùn)行時(shí),會(huì)被加載到 RAM 中,又因?yàn)?App 所在進(jìn)程會(huì)分配指定額度的空間,所以 App 的內(nèi)存空間是有限的,內(nèi)存的大小對(duì) App 性能及正常運(yùn)行都會(huì)有很大的影響。 當(dāng) App 所分配的內(nèi)存空間不足時(shí),會(huì)拋出 OOM 。所以對(duì)運(yùn)行中的 App 的內(nèi)存的優(yōu)化就顯得尤為重要。
常見(jiàn)內(nèi)存問(wèn)題
常見(jiàn)的內(nèi)存問(wèn)題包括:
- 內(nèi)存泄漏:因?yàn)?Java 對(duì)象無(wú)法被正常回收,如果長(zhǎng)期運(yùn)行程序,就會(huì)造成大量的無(wú)用對(duì)象占用內(nèi)存空間,最終導(dǎo)致 OOM。
- 內(nèi)存抖動(dòng):頻繁的創(chuàng)建對(duì)象,當(dāng)對(duì)象數(shù)據(jù)到達(dá)一定程度會(huì)造成 GC ,如果短時(shí)間內(nèi)頻繁的 GC 就會(huì)造成 App 卡頓的現(xiàn)象,這個(gè)就叫內(nèi)存抖動(dòng)。
- 內(nèi)存溢出:當(dāng) App 申請(qǐng)內(nèi)存空間時(shí),沒(méi)有足夠的內(nèi)存空間供其使用,就會(huì)導(dǎo)致內(nèi)存溢出,即 Out Of Memory。
內(nèi)存溢出
內(nèi)存溢出(Out Of Memory,簡(jiǎn)稱(chēng)OOM)是指應(yīng)用系統(tǒng)中存在無(wú)法回收的內(nèi)存或使用的內(nèi)存過(guò)多,最終使得程序運(yùn)行要用到的內(nèi)存大于能提供的最大內(nèi)存。此時(shí) App 就運(yùn)行不了,系統(tǒng)會(huì)提示內(nèi)存溢出,拋出異常。
所以避免 OOM 的辦法就是解決內(nèi)存泄漏問(wèn)題,或盡量在代碼中節(jié)約使用內(nèi)存兩種思路。
內(nèi)存泄漏
內(nèi)存泄漏在 Android 中就是在當(dāng)前App 的生命周期內(nèi)不再使用的對(duì)象被GC Roots引用,導(dǎo)致不能回收,使實(shí)際可使用內(nèi)存變小。 需要注意的是,內(nèi)存泄漏問(wèn)題的出現(xiàn),是和生命周期有關(guān)系的,從生命周期的角度考慮,就是生命周期短的對(duì)象被生命周期長(zhǎng)的 GC Roots 對(duì)象持有引用,從而導(dǎo)致生命周期短的對(duì)象在該被回收的時(shí)候,無(wú)法被正確回收,該對(duì)象長(zhǎng)期存活,但又毫無(wú)用處,白白地占用了內(nèi)存空間。當(dāng)這種對(duì)象過(guò)多時(shí),就會(huì)造成 OOM 。
常見(jiàn)內(nèi)存泄漏場(chǎng)景
無(wú)法回收無(wú)用對(duì)象的場(chǎng)景,可以統(tǒng)一理解為發(fā)生了內(nèi)存泄漏,常見(jiàn)的 case 有:
- 資源文件未關(guān)閉/回收
- 注冊(cè)對(duì)象未注銷(xiāo)
- 靜態(tài)變量持有數(shù)據(jù)對(duì)象
- 單例造成內(nèi)存泄漏
- 非靜態(tài)內(nèi)部類(lèi)的實(shí)例持有外部類(lèi)引用
- Handler
- 集合對(duì)象中的對(duì)象未釋放
- WebView 內(nèi)存泄漏
- View 的生命周期大于容器的生命周期
常見(jiàn)的諸如資源文件未關(guān)閉/為回收、注冊(cè)對(duì)象未注銷(xiāo),導(dǎo)致觀察者一致持有注冊(cè)對(duì)象的引用,從而無(wú)法正常回收注冊(cè)的對(duì)象。這里對(duì)其他幾種場(chǎng)景進(jìn)行詳細(xì)的說(shuō)明。
靜態(tài)變量或單例持有對(duì)象
在 JVM 規(guī)范中,靜態(tài)變量屬于 GC Root 其中的一種,一般情況下它的生命周期都會(huì)比較長(zhǎng),所以如果一個(gè)對(duì)象的某個(gè)屬性被靜態(tài)變量持有了引用,就會(huì)導(dǎo)致該屬性實(shí)例無(wú)法正常被回收。
以簡(jiǎn)單的示例代碼說(shuō)明:
class TestC { companion object { var leak: Any? = null } } class LeakCanaryActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { LeakCanaryPage(actions()) } staticOOM() } private fun staticOOM() { Toast.makeText(this, "static own Context", Toast.LENGTH_SHORT).show() TestC.leak = this } }
當(dāng)我們打開(kāi)這個(gè)LeakCanaryActivity
后,返回上一個(gè) Activity,此時(shí)查看 Profiler 排查內(nèi)存泄漏的內(nèi)容:
同樣的道理,單例模式一般也是全局的生命周期且唯一的對(duì)象,如果被單例持有也會(huì)導(dǎo)致一樣的問(wèn)題。
object TestB { var leak: Any? = null } // 修改 LeakCanaryActivity 的 staticOOM 方法 private fun staticOOM() { Toast.makeText(this, "static own Context", Toast.LENGTH_SHORT).show() TestB.leak = this }
非靜態(tài)內(nèi)部類(lèi)的實(shí)例生命周期比外部類(lèi)更長(zhǎng)導(dǎo)致的內(nèi)存泄漏
非靜態(tài)內(nèi)部類(lèi)一般持有對(duì)外部類(lèi)實(shí)例的引用,這個(gè)可以通過(guò)查看 class 文件發(fā)現(xiàn),內(nèi)部類(lèi)的構(gòu)造方法一般需要一個(gè)外部類(lèi)類(lèi)型的參數(shù)。所以如果一個(gè)內(nèi)部類(lèi)對(duì)象,生命周期更久的話(huà)就會(huì)造成內(nèi)存泄漏。 這里一個(gè)比較明顯的例子是多線(xiàn)程操作內(nèi)部類(lèi)對(duì)象時(shí),外部類(lèi)的生命周期已經(jīng)結(jié)束時(shí),因?yàn)閮?nèi)部類(lèi)實(shí)例持有外部類(lèi)的引用,導(dǎo)致外部類(lèi)實(shí)例無(wú)法被正常回收:
class LeakCanaryActivity : ComponentActivity() { // ... // 執(zhí)行這個(gè)方法 private fun innerClassOOM() { Toast.makeText(this, "inner leak", Toast.LENGTH_SHORT).show() val inner = InnerLeak() Thread(inner).start() finish() } // 內(nèi)部類(lèi) inner class InnerLeak: Runnable { override fun run() { Thread.sleep(15000) } } }
當(dāng)我們打開(kāi)一個(gè) Activity 后,立刻創(chuàng)建一個(gè)新的線(xiàn)程執(zhí)行內(nèi)部類(lèi),然后立刻關(guān)閉自身,此時(shí)因?yàn)?InnerLeak 仍在子線(xiàn)程中,子線(xiàn)程在 sleep ,導(dǎo)致,外部類(lèi)生命周期已經(jīng)結(jié)束(調(diào)用了 finish),內(nèi)部類(lèi)對(duì)象 inner 仍持有外部類(lèi)LeakCanaryActivity
的引用。 除了這種內(nèi)部類(lèi)的形式,也可以用匿名內(nèi)部類(lèi)的形式來(lái)寫(xiě),都會(huì)導(dǎo)致內(nèi)存泄漏。 另一方面,不光是多線(xiàn)程的場(chǎng)景,如果內(nèi)部類(lèi)對(duì)象被靜態(tài)變量持有引用也是一樣的效果,因?yàn)樗麄兌汲钟辛藘?nèi)部類(lèi)的引用,導(dǎo)致內(nèi)部類(lèi)的生命周期比外部類(lèi)的生命周期更長(zhǎng)。
Handler 導(dǎo)致的內(nèi)存泄漏
通過(guò) Handler 發(fā)送消息時(shí),消息對(duì)象 Message 本身會(huì)持有 Handler 對(duì)象:
// Handler#sendMessage(Message) 會(huì)執(zhí)行到 enqueueMessage 方法 private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { // 這里把 handler 自身保存到了 Message 的 target 屬性中了 msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
sendMessage 方法內(nèi)部調(diào)用到enqueueMessage(MessageQueue, Message, long)
時(shí),會(huì)把 Handler 對(duì)象自身賦值到 Message 的 target 上,這樣 message 就知道去找哪個(gè) Handler 執(zhí)行handleMessage(msg: Message)
方法。也是因?yàn)檫@個(gè)持有,導(dǎo)致了如果消息沒(méi)有立刻被執(zhí)行,就會(huì)一直持有 Handler 對(duì)象,此時(shí)如果關(guān)閉 Activity ,就會(huì)導(dǎo)致內(nèi)存泄漏。
原因是 Handler 以匿名內(nèi)部類(lèi)或內(nèi)部類(lèi)的形式聲明并創(chuàng)建的,會(huì)持有外部 Activity 的引用。從而導(dǎo)致持有關(guān)系是:
Message -> Handler -> Activity
實(shí)現(xiàn) Handler 內(nèi)存泄漏的代碼:
// in LeakCanaryActivity private fun handlerOOM() { val handler = object : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { if (msg.what == 12) Toast.makeText(this@LeakCanaryActivity, "handler executed", Toast.LENGTH_SHORT).show() } } Thread { handler.sendMessageDelayed(Message().apply { what = 12 }, 10000) }.start() }
操作邏輯是,在 LeakCanaryActivity 中調(diào)用這個(gè)方法后,立刻 finish LeakCanaryActivity ,然后查看內(nèi)存泄漏情況:
postDelayed 導(dǎo)致的內(nèi)存泄漏
postDelayed 實(shí)際上是把 Runnable 封裝成了一個(gè) Message 對(duì)象,傳入的 Runnable 參數(shù)被賦值給了 Message 的 callback :
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) { return sendMessageDelayed(getPostMessage(r), delayMillis); } private static Message getPostMessage(Runnable r) { Message m = Message.obtain(); m.callback = r; return m; }
而最終執(zhí)行邏輯的方法都是 sendMessageDelayed(Message, long)
,所以和 sendMessage 一樣都會(huì)導(dǎo)致內(nèi)存泄漏。與之不同的是,postDelayed
的泄漏會(huì)多一個(gè)Message#callback
因?yàn)?在調(diào)用postDelayed
時(shí),第一個(gè)是個(gè)匿名內(nèi)部類(lèi)對(duì)象,多了一個(gè)引用。
handler.postDelayed(object : Runnable { override fun run() { Log.d(TAG, "postdelay done") } }, 10000)
View 的生命周期大于 Activity 時(shí)導(dǎo)致的內(nèi)存泄漏
一個(gè)極其簡(jiǎn)單的內(nèi)存泄漏場(chǎng)景是,當(dāng)我在一個(gè) Activity 內(nèi)多次彈出 Toast 時(shí),立刻關(guān)閉當(dāng)前 Activity ,就會(huì)導(dǎo)致內(nèi)存泄漏的情況出現(xiàn):
// in LeakCanaryActivity private fun toastOOM() { Toast.makeText(this, "toast leak", Toast.LENGTH_SHORT).show() }
操作步驟:將上面的方法設(shè)置在某個(gè)點(diǎn)擊事件中,快速連續(xù)點(diǎn)擊幾次,然后立刻關(guān)閉當(dāng)前 Activity ,查看 Profiler:
集合中的對(duì)象未釋放導(dǎo)致內(nèi)存泄漏
最常見(jiàn)的場(chǎng)景是觀察者模式,觀察者模式中注冊(cè)一些觀察者對(duì)象,一般是保存到一個(gè)全局的集合中,如果觀察者對(duì)象在釋放時(shí)不及時(shí)注銷(xiāo),就會(huì)造成內(nèi)存泄漏:
object LeakCollection { val list = ArrayList<Any>() } class LeakCanaryActivity : ComponentActivity() { // ... private fun collectionOOM() { LeakCollection.list.add(this) } }
操作步驟:在 LeakCanaryActivity 內(nèi)調(diào)用collectionOOM()
,然后立刻 finish 。
最常見(jiàn)的解決辦法就是在 Activity 的 destroy 時(shí),從 list 清除自身的引用。
WebView 導(dǎo)致的內(nèi)存泄漏
網(wǎng)上都說(shuō) WebView 會(huì)導(dǎo)致內(nèi)存泄漏。通過(guò) Profiler 直接查看并沒(méi)有明顯的一個(gè) Leaks 提示。那么如何排查這個(gè)內(nèi)存泄露呢?
一個(gè)思路是參照對(duì)比實(shí)驗(yàn):
- 對(duì)照組 A :NoLeakActivity,一個(gè)空的 Activity,里面沒(méi)有任何內(nèi)容。
- 對(duì)照組 B :LeakWebViewActivity, 一個(gè)包含 WebView 的 Activity 。
在同一個(gè) Root Activity 中分別打開(kāi) A 和 B ,通過(guò)對(duì)比內(nèi)存變化,來(lái)證明 WebView 是否真的造成了內(nèi)存泄漏。
首先是打開(kāi)了 NoLeakActivity, 并沒(méi)有明顯的內(nèi)存變化。
然后返回到 LeakCannaryActivity ,內(nèi)存還是沒(méi)有變化。接著打開(kāi) LeakWebViewActivity ,發(fā)現(xiàn)內(nèi)存明顯上升,主要上升在 Native 、Others 和 Graphics 。 Graphics 可以理解,因?yàn)?loadUrl 失敗了會(huì)顯示一個(gè)失敗頁(yè)面,其中有個(gè) icon 圖片,所以主要分析的點(diǎn)是 Native 和 Others 。
然后返回到 LeakCanaryActivity, 內(nèi)存基本沒(méi)有變化。
為了證明,不是因?yàn)?NoLeakActivity 先打開(kāi),LeakWebViewActivity 后打開(kāi),所以?xún)?nèi)存中會(huì)有多余的 NoLeakActivity 相關(guān)的內(nèi)存占用,我們?cè)俅未蜷_(kāi) NoLeakActivity ,再返回,內(nèi)存仍無(wú)明顯變化。
所以,基本上可以證明,WebView 沒(méi)有隨著 Activity 的銷(xiāo)毀而被回收。
但是如何解決這種情況呢?這個(gè)問(wèn)題值得后續(xù)仔細(xì)研究一下。但目前網(wǎng)上的各種奇怪的解決方案(例如開(kāi)啟一個(gè)單獨(dú)的進(jìn)程)并不是合理的辦法。 一個(gè)說(shuō)法是,在 xml 里面是有 WebView 會(huì)出現(xiàn)內(nèi)存泄漏,但是如果通過(guò) addView 的形式去使用不會(huì)造成,以下是通過(guò) addView 的形式添加 一個(gè) WebView 對(duì)象的內(nèi)存變化。
而這是通過(guò) XML 的形式使用 WebView 的內(nèi)存變化。
兩種方法好像并沒(méi)有什么區(qū)別,但有用的一點(diǎn)是,這里的內(nèi)存變化,主要體現(xiàn)在 Native 上,證明 WebView 組件,會(huì)在 Native 層面生成一些內(nèi)容。
這個(gè)部分的分析,后續(xù)可以再深入研究。從應(yīng)用層面來(lái)看,WebView 并沒(méi)有直接觸發(fā)再 Java heap 上的內(nèi)存泄漏。而是更底層的 Native heap 中。
另外需要注意的一點(diǎn)是,通過(guò) LeakCanary 并不能精準(zhǔn)的檢測(cè)到內(nèi)存泄漏,還是得用 Profiler。
內(nèi)存抖動(dòng)
短時(shí)間內(nèi)頻繁創(chuàng)建對(duì)象,導(dǎo)致虛擬機(jī)頻繁觸發(fā)GC操作,頻繁的 GC 會(huì)導(dǎo)致畫(huà)面卡頓。
解決方案
- 盡量避免在循環(huán)體內(nèi)創(chuàng)建對(duì)象,應(yīng)該把對(duì)象創(chuàng)建移到循環(huán)體外。
- 注意自定義 View 的
onDraw()
方法會(huì)被頻繁調(diào)用,所以在這里面不應(yīng)該頻繁的創(chuàng)建對(duì)象。 - 當(dāng)需要大量使用 Bitmap 的時(shí)候,試著把它們緩存在數(shù)組中實(shí)現(xiàn)復(fù)用。
- 對(duì)于能夠復(fù)用的對(duì)象,同理可以使用對(duì)象池將它們緩存起來(lái)。
其他優(yōu)化點(diǎn)
基本上減少內(nèi)存優(yōu)化的其他思路就是復(fù)用和壓縮資源。
- 圖片資源過(guò)大,進(jìn)行縮放處理。
- 減少不必要的內(nèi)存開(kāi)銷(xiāo):一些基本數(shù)據(jù)類(lèi)型的包裝類(lèi),例如 Integer 占用 16 個(gè)字節(jié),而 int 占用 4 個(gè)字節(jié),所以盡量避免使用自動(dòng)裝箱的類(lèi)。
- 對(duì)象和資源進(jìn)行復(fù)用。
- 選擇更合適的數(shù)據(jù)結(jié)構(gòu),避免數(shù)據(jù)結(jié)構(gòu)分配過(guò)大導(dǎo)致的內(nèi)存浪費(fèi)。
- 使用int 枚舉或 String 枚舉代替枚舉類(lèi)型 ,但枚舉類(lèi)型也會(huì)有比前者更好的特性,需要酌情使用。
- 使用 LruCache 等緩存策略。
- App 內(nèi)存過(guò)低時(shí)主動(dòng)清理。
App 內(nèi)存過(guò)低時(shí)主動(dòng)清理
實(shí)現(xiàn) Application 中的 onTrimMemory/onLowMemory 方法去釋放掉圖片緩存、靜態(tài)緩存來(lái)自保。
class BaseApplication: Application() { override fun onLowMemory() { super.onLowMemory() } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) } }
到此這篇關(guān)于Android 內(nèi)存優(yōu)化知識(shí)點(diǎn)梳理總結(jié)的文章就介紹到這了,更多相關(guān)Android 內(nèi)存優(yōu)化 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android中實(shí)現(xiàn)GPS定位的簡(jiǎn)單例子
這篇文章主要介紹了Android中實(shí)現(xiàn)GPS定位的簡(jiǎn)單例子,例子邏輯清晰,但相對(duì)簡(jiǎn)單了些,需要的朋友可以參考下2014-07-07Android開(kāi)發(fā)DataBinding基礎(chǔ)使用
這篇文章主要為大家介紹了Android開(kāi)發(fā)DataBinding基礎(chǔ)使用實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06Android編程解析XML文件的方法詳解【基于XmlPullParser】
這篇文章主要介紹了Android編程解析XML文件的方法,結(jié)合實(shí)例形式分析了Android基于XmlPullParser解析xml文件的相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2017-07-07Android編程之客戶(hù)端通過(guò)socket與服務(wù)器通信的方法
這篇文章主要介紹了Android編程之客戶(hù)端通過(guò)socket與服務(wù)器通信的方法,結(jié)合實(shí)例形式分析了Android基于socket通訊的具體步驟與相關(guān)使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11ionic App 解決android端在真機(jī)上tab處于頂部的問(wèn)題
這篇文章主要介紹了ionic App 解決android端在真機(jī)上tab處于頂部的問(wèn)題的相關(guān)資料,需要的朋友可以參考下2017-06-06Android監(jiān)聽(tīng)ScrollView滑動(dòng)距離的簡(jiǎn)單處理
這篇文章主要為大家詳細(xì)介紹了Android監(jiān)聽(tīng)ScrollView滑動(dòng)距離的簡(jiǎn)單處理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02Android Handler runWithScissors 梳理流程解析
這篇文章主要為大家介紹了Android Handler runWithScissors 梳理流程解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10Android編程四大組件之Activity用法實(shí)例分析
這篇文章主要介紹了Android編程四大組件之Activity用法,實(shí)例分析了Activity的創(chuàng)建,生命周期,內(nèi)存管理及啟動(dòng)模式等,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-01-01Android 實(shí)現(xiàn)帶進(jìn)度條的WebView的實(shí)例
這篇文章主要介紹了Android 實(shí)現(xiàn)帶進(jìn)度條的WebView的實(shí)例的相關(guān)資料,這里介紹了Webview加載網(wǎng)頁(yè)的方法及帶進(jìn)度的Drawable文件view_progress_webview的實(shí)現(xiàn),需要的朋友可以參考下2017-07-07