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

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

 更新時間:2022年06月16日 09:25:07   作者:??自動化BUG制造器????  
這篇文章主要介紹了Android?內(nèi)存優(yōu)化知識點梳理總結(jié),Android?操作系統(tǒng)給每個進程都會分配指定額度的內(nèi)存空間,App?使用內(nèi)存來進行快速的文件訪問交互,長時間如此便需要優(yōu)化策略,文章分享優(yōu)化知識點總結(jié),需要的朋友可以參考一下

前言:

Android 操作系統(tǒng)給每個進程都會分配指定額度的內(nèi)存空間,App 使用內(nèi)存來進行快速的文件訪問交互。例如展示網(wǎng)絡(luò)圖片時,就是通過把網(wǎng)絡(luò)圖片下載到內(nèi)存中展示,如果需要保存到本地,再從內(nèi)存中保存到磁盤空間中。

RAM 和 ROM

手機一般有兩種存儲介質(zhì),一個是 RAM ,我們常說的內(nèi)存,也稱之為運行內(nèi)存;另一個是 ROM ,即磁盤空間。 RAM 的訪問速度一般會比 ROM 快,它是即插即用,斷電會抹除所有數(shù)據(jù),RAM 越大,可同時操作的數(shù)據(jù)就越多;ROM 是外部存儲空間,相當于電腦的硬盤,主要是用來存儲本地數(shù)據(jù)的。

App 運行時,會被加載到 RAM 中,又因為 App 所在進程會分配指定額度的空間,所以 App 的內(nèi)存空間是有限的,內(nèi)存的大小對 App 性能及正常運行都會有很大的影響。 當 App 所分配的內(nèi)存空間不足時,會拋出 OOM 。所以對運行中的 App 的內(nèi)存的優(yōu)化就顯得尤為重要。

常見內(nèi)存問題

常見的內(nèi)存問題包括:

  • 內(nèi)存泄漏:因為 Java 對象無法被正?;厥?,如果長期運行程序,就會造成大量的無用對象占用內(nèi)存空間,最終導(dǎo)致 OOM。
  • 內(nèi)存抖動:頻繁的創(chuàng)建對象,當對象數(shù)據(jù)到達一定程度會造成 GC ,如果短時間內(nèi)頻繁的 GC 就會造成 App 卡頓的現(xiàn)象,這個就叫內(nèi)存抖動。
  • 內(nèi)存溢出:當 App 申請內(nèi)存空間時,沒有足夠的內(nèi)存空間供其使用,就會導(dǎo)致內(nèi)存溢出,即 Out Of Memory。

內(nèi)存溢出

內(nèi)存溢出(Out Of Memory,簡稱OOM)是指應(yīng)用系統(tǒng)中存在無法回收的內(nèi)存或使用的內(nèi)存過多,最終使得程序運行要用到的內(nèi)存大于能提供的最大內(nèi)存。此時 App 就運行不了,系統(tǒng)會提示內(nèi)存溢出,拋出異常。

所以避免 OOM 的辦法就是解決內(nèi)存泄漏問題,或盡量在代碼中節(jié)約使用內(nèi)存兩種思路。

內(nèi)存泄漏

內(nèi)存泄漏在 Android 中就是在當前App 的生命周期內(nèi)不再使用的對象被GC Roots引用,導(dǎo)致不能回收,使實際可使用內(nèi)存變小。 需要注意的是,內(nèi)存泄漏問題的出現(xiàn),是和生命周期有關(guān)系的,從生命周期的角度考慮,就是生命周期短的對象被生命周期長的 GC Roots 對象持有引用,從而導(dǎo)致生命周期短的對象在該被回收的時候,無法被正確回收,該對象長期存活,但又毫無用處,白白地占用了內(nèi)存空間。當這種對象過多時,就會造成 OOM 。

常見內(nèi)存泄漏場景

無法回收無用對象的場景,可以統(tǒng)一理解為發(fā)生了內(nèi)存泄漏,常見的 case 有:

  • 資源文件未關(guān)閉/回收
  • 注冊對象未注銷
  • 靜態(tài)變量持有數(shù)據(jù)對象
  • 單例造成內(nèi)存泄漏
  • 非靜態(tài)內(nèi)部類的實例持有外部類引用
  • Handler
  • 集合對象中的對象未釋放
  • WebView 內(nèi)存泄漏
  • View 的生命周期大于容器的生命周期

常見的諸如資源文件未關(guān)閉/為回收、注冊對象未注銷,導(dǎo)致觀察者一致持有注冊對象的引用,從而無法正?;厥兆缘膶ο?。這里對其他幾種場景進行詳細的說明。

靜態(tài)變量或單例持有對象

在 JVM 規(guī)范中,靜態(tài)變量屬于 GC Root 其中的一種,一般情況下它的生命周期都會比較長,所以如果一個對象的某個屬性被靜態(tài)變量持有了引用,就會導(dǎo)致該屬性實例無法正常被回收。

以簡單的示例代碼說明:

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
	}
}

當我們打開這個LeakCanaryActivity后,返回上一個 Activity,此時查看 Profiler 排查內(nèi)存泄漏的內(nèi)容:

同樣的道理,單例模式一般也是全局的生命周期且唯一的對象,如果被單例持有也會導(dǎo)致一樣的問題。

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)部類的實例生命周期比外部類更長導(dǎo)致的內(nèi)存泄漏

非靜態(tài)內(nèi)部類一般持有對外部類實例的引用,這個可以通過查看 class 文件發(fā)現(xiàn),內(nèi)部類的構(gòu)造方法一般需要一個外部類類型的參數(shù)。所以如果一個內(nèi)部類對象,生命周期更久的話就會造成內(nèi)存泄漏。 這里一個比較明顯的例子是多線程操作內(nèi)部類對象時,外部類的生命周期已經(jīng)結(jié)束時,因為內(nèi)部類實例持有外部類的引用,導(dǎo)致外部類實例無法被正?;厥眨?/p>

class LeakCanaryActivity : ComponentActivity() {
	// ... 
	// 執(zhí)行這個方法
	private fun innerClassOOM() {
    Toast.makeText(this, "inner leak", Toast.LENGTH_SHORT).show()
    val inner = InnerLeak()
    Thread(inner).start()
	  finish()
	}
	// 內(nèi)部類
	inner class InnerLeak: Runnable {
    override fun run() {
        Thread.sleep(15000)
    }
	}
}

當我們打開一個 Activity 后,立刻創(chuàng)建一個新的線程執(zhí)行內(nèi)部類,然后立刻關(guān)閉自身,此時因為 InnerLeak 仍在子線程中,子線程在 sleep ,導(dǎo)致,外部類生命周期已經(jīng)結(jié)束(調(diào)用了 finish),內(nèi)部類對象 inner 仍持有外部類LeakCanaryActivity的引用。 除了這種內(nèi)部類的形式,也可以用匿名內(nèi)部類的形式來寫,都會導(dǎo)致內(nèi)存泄漏。 另一方面,不光是多線程的場景,如果內(nèi)部類對象被靜態(tài)變量持有引用也是一樣的效果,因為他們都持有了內(nèi)部類的引用,導(dǎo)致內(nèi)部類的生命周期比外部類的生命周期更長。

Handler 導(dǎo)致的內(nèi)存泄漏

通過 Handler 發(fā)送消息時,消息對象 Message 本身會持有 Handler 對象:

// Handler#sendMessage(Message) 會執(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)時,會把 Handler 對象自身賦值到 Message 的 target 上,這樣 message 就知道去找哪個 Handler 執(zhí)行handleMessage(msg: Message)方法。也是因為這個持有,導(dǎo)致了如果消息沒有立刻被執(zhí)行,就會一直持有 Handler 對象,此時如果關(guān)閉 Activity ,就會導(dǎo)致內(nèi)存泄漏。

原因是 Handler 以匿名內(nèi)部類或內(nèi)部類的形式聲明并創(chuàng)建的,會持有外部 Activity 的引用。從而導(dǎo)致持有關(guān)系是:

Message -> Handler -> Activity

實現(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)用這個方法后,立刻 finish LeakCanaryActivity ,然后查看內(nèi)存泄漏情況:

postDelayed 導(dǎo)致的內(nèi)存泄漏

postDelayed 實際上是把 Runnable 封裝成了一個 Message 對象,傳入的 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 一樣都會導(dǎo)致內(nèi)存泄漏。與之不同的是,postDelayed的泄漏會多一個Message#callback因為 在調(diào)用postDelayed時,第一個是個匿名內(nèi)部類對象,多了一個引用。

handler.postDelayed(object : Runnable {
    override fun run() {
        Log.d(TAG, "postdelay done")
    }
}, 10000)

View 的生命周期大于 Activity 時導(dǎo)致的內(nèi)存泄漏

一個極其簡單的內(nèi)存泄漏場景是,當我在一個 Activity 內(nèi)多次彈出 Toast 時,立刻關(guān)閉當前 Activity ,就會導(dǎo)致內(nèi)存泄漏的情況出現(xiàn):

// in LeakCanaryActivity
private fun toastOOM() {
    Toast.makeText(this, "toast leak", Toast.LENGTH_SHORT).show()
}

操作步驟:將上面的方法設(shè)置在某個點擊事件中,快速連續(xù)點擊幾次,然后立刻關(guān)閉當前 Activity ,查看 Profiler:

集合中的對象未釋放導(dǎo)致內(nèi)存泄漏

最常見的場景是觀察者模式,觀察者模式中注冊一些觀察者對象,一般是保存到一個全局的集合中,如果觀察者對象在釋放時不及時注銷,就會造成內(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 。

最常見的解決辦法就是在 Activity 的 destroy 時,從 list 清除自身的引用。

WebView 導(dǎo)致的內(nèi)存泄漏

網(wǎng)上都說 WebView 會導(dǎo)致內(nèi)存泄漏。通過 Profiler 直接查看并沒有明顯的一個 Leaks 提示。那么如何排查這個內(nèi)存泄露呢?

一個思路是參照對比實驗:

  • 對照組 A :NoLeakActivity,一個空的 Activity,里面沒有任何內(nèi)容。
  • 對照組 B :LeakWebViewActivity, 一個包含 WebView 的 Activity 。

在同一個 Root Activity 中分別打開 A 和 B ,通過對比內(nèi)存變化,來證明 WebView 是否真的造成了內(nèi)存泄漏。

首先是打開了 NoLeakActivity, 并沒有明顯的內(nèi)存變化。

然后返回到 LeakCannaryActivity ,內(nèi)存還是沒有變化。接著打開 LeakWebViewActivity ,發(fā)現(xiàn)內(nèi)存明顯上升,主要上升在 Native 、Others 和 Graphics 。 Graphics 可以理解,因為 loadUrl 失敗了會顯示一個失敗頁面,其中有個 icon 圖片,所以主要分析的點是 Native 和 Others 。

然后返回到 LeakCanaryActivity, 內(nèi)存基本沒有變化。

為了證明,不是因為 NoLeakActivity 先打開,LeakWebViewActivity 后打開,所以內(nèi)存中會有多余的 NoLeakActivity 相關(guān)的內(nèi)存占用,我們再次打開 NoLeakActivity ,再返回,內(nèi)存仍無明顯變化。

所以,基本上可以證明,WebView 沒有隨著 Activity 的銷毀而被回收。

但是如何解決這種情況呢?這個問題值得后續(xù)仔細研究一下。但目前網(wǎng)上的各種奇怪的解決方案(例如開啟一個單獨的進程)并不是合理的辦法。 一個說法是,在 xml 里面是有 WebView 會出現(xiàn)內(nèi)存泄漏,但是如果通過 addView 的形式去使用不會造成,以下是通過 addView 的形式添加 一個 WebView 對象的內(nèi)存變化。

而這是通過 XML 的形式使用 WebView 的內(nèi)存變化。

兩種方法好像并沒有什么區(qū)別,但有用的一點是,這里的內(nèi)存變化,主要體現(xiàn)在 Native 上,證明 WebView 組件,會在 Native 層面生成一些內(nèi)容。

這個部分的分析,后續(xù)可以再深入研究。從應(yīng)用層面來看,WebView 并沒有直接觸發(fā)再 Java heap 上的內(nèi)存泄漏。而是更底層的 Native heap 中。

另外需要注意的一點是,通過 LeakCanary 并不能精準的檢測到內(nèi)存泄漏,還是得用 Profiler。

內(nèi)存抖動

短時間內(nèi)頻繁創(chuàng)建對象,導(dǎo)致虛擬機頻繁觸發(fā)GC操作,頻繁的 GC 會導(dǎo)致畫面卡頓。

解決方案

  • 盡量避免在循環(huán)體內(nèi)創(chuàng)建對象,應(yīng)該把對象創(chuàng)建移到循環(huán)體外。
  • 注意自定義 View 的 onDraw() 方法會被頻繁調(diào)用,所以在這里面不應(yīng)該頻繁的創(chuàng)建對象。
  • 當需要大量使用 Bitmap 的時候,試著把它們緩存在數(shù)組中實現(xiàn)復(fù)用。
  • 對于能夠復(fù)用的對象,同理可以使用對象池將它們緩存起來。

其他優(yōu)化點

基本上減少內(nèi)存優(yōu)化的其他思路就是復(fù)用和壓縮資源。

  • 圖片資源過大,進行縮放處理。
  • 減少不必要的內(nèi)存開銷:一些基本數(shù)據(jù)類型的包裝類,例如 Integer 占用 16 個字節(jié),而 int 占用 4 個字節(jié),所以盡量避免使用自動裝箱的類。
  • 對象和資源進行復(fù)用。
  • 選擇更合適的數(shù)據(jù)結(jié)構(gòu),避免數(shù)據(jù)結(jié)構(gòu)分配過大導(dǎo)致的內(nèi)存浪費。
  • 使用int 枚舉或 String 枚舉代替枚舉類型 ,但枚舉類型也會有比前者更好的特性,需要酌情使用。
  • 使用 LruCache 等緩存策略。
  • App 內(nèi)存過低時主動清理。

App 內(nèi)存過低時主動清理

實現(xiàn) Application 中的 onTrimMemory/onLowMemory 方法去釋放掉圖片緩存、靜態(tài)緩存來自保。

class BaseApplication: Application() {
    override fun onLowMemory() {
        super.onLowMemory()
    }
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
    }
}

到此這篇關(guān)于Android 內(nèi)存優(yōu)化知識點梳理總結(jié)的文章就介紹到這了,更多相關(guān)Android 內(nèi)存優(yōu)化 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論