欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android?內(nèi)存優(yōu)化知識(shí)點(diǎn)梳理總結(jié)

 更新時(shí)間:2022年06月16日 09:25:07   作者:??自動(dòng)化BUG制造器????  
這篇文章主要介紹了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)交互,長(zhǎng)時(shí)間如此便需要優(yōu)化策略,文章分享優(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)文章

最新評(píng)論