<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
前言:
Android 作業系統給每個程序都會分配指定額度的記憶體空間,App 使用記憶體來進行快速的檔案存取互動。例如展示網路圖片時,就是通過把網路圖片下載到記憶體中展示,如果需要儲存到本地,再從記憶體中儲存到磁碟空間中。
手機一般有兩種儲存媒介,一個是 RAM ,我們常說的記憶體,也稱之為執行記憶體;另一個是 ROM ,即磁碟空間。 RAM 的存取速度一般會比 ROM 快,它是隨插即用,斷電會抹除所有資料,RAM 越大,可同時操作的資料就越多;ROM 是外部儲存空間,相當於電腦的硬碟,主要是用來儲存本地資料的。
App 執行時,會被載入到 RAM 中,又因為 App 所在程序會分配指定額度的空間,所以 App 的記憶體空間是有限的,記憶體的大小對 App 效能及正常執行都會有很大的影響。 當 App 所分配的記憶體空間不足時,會丟擲 OOM 。所以對執行中的 App 的記憶體的優化就顯得尤為重要。
常見的記憶體問題包括:
記憶體溢位(Out Of Memory,簡稱OOM)是指應用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式執行要用到的記憶體大於能提供的最大記憶體。此時 App 就執行不了,系統會提示記憶體溢位,丟擲異常。
所以避免 OOM 的辦法就是解決記憶體漏失問題,或儘量在程式碼中節約使用記憶體兩種思路。
記憶體漏失在 Android 中就是在當前App 的生命週期內不再使用的物件被GC Roots參照,導致不能回收,使實際可使用記憶體變小。 需要注意的是,記憶體漏失問題的出現,是和生命週期有關係的,從生命週期的角度考慮,就是生命週期短的物件被生命週期長的 GC Roots 物件持有參照,從而導致生命週期短的物件在該被回收的時候,無法被正確回收,該物件長期存活,但又毫無用處,白白地佔用了記憶體空間。當這種物件過多時,就會造成 OOM 。
無法回收無用物件的場景,可以統一理解為發生了記憶體漏失,常見的 case 有:
常見的諸如資原始檔未關閉/為回收、註冊物件未登出,導致觀察者一致持有註冊物件的參照,從而無法正常回收註冊的物件。這裡對其他幾種場景進行詳細的說明。
在 JVM 規範中,靜態變數屬於 GC Root 其中的一種,一般情況下它的生命週期都會比較長,所以如果一個物件的某個屬性被靜態變數持有了參照,就會導致該屬性範例無法正常被回收。
以簡單的範例程式碼說明:
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 排查記憶體漏失的內容:
同樣的道理,單例模式一般也是全域性的生命週期且唯一的物件,如果被單例持有也會導致一樣的問題。
object TestB { var leak: Any? = null } // 修改 LeakCanaryActivity 的 staticOOM 方法 private fun staticOOM() { Toast.makeText(this, "static own Context", Toast.LENGTH_SHORT).show() TestB.leak = this }
非靜態內部類一般持有對外部類範例的參照,這個可以通過檢視 class 檔案發現,內部類的構造方法一般需要一個外部類型別的引數。所以如果一個內部類物件,生命週期更久的話就會造成記憶體漏失。 這裡一個比較明顯的例子是多執行緒操作內部類物件時,外部類的生命週期已經結束時,因為內部類範例持有外部類的參照,導致外部類範例無法被正常回收:
class LeakCanaryActivity : ComponentActivity() { // ... // 執行這個方法 private fun innerClassOOM() { Toast.makeText(this, "inner leak", Toast.LENGTH_SHORT).show() val inner = InnerLeak() Thread(inner).start() finish() } // 內部類 inner class InnerLeak: Runnable { override fun run() { Thread.sleep(15000) } } }
當我們開啟一個 Activity 後,立刻建立一個新的執行緒執行內部類,然後立刻關閉自身,此時因為 InnerLeak 仍在子執行緒中,子執行緒在 sleep ,導致,外部類生命週期已經結束(呼叫了 finish),內部類物件 inner 仍持有外部類LeakCanaryActivity
的參照。 除了這種內部類的形式,也可以用匿名內部類的形式來寫,都會導致記憶體漏失。 另一方面,不光是多執行緒的場景,如果內部類物件被靜態變數持有參照也是一樣的效果,因為他們都持有了內部類的參照,導致內部類的生命週期比外部類的生命週期更長。
通過 Handler 傳送訊息時,訊息物件 Message 本身會持有 Handler 物件:
// Handler#sendMessage(Message) 會執行到 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 方法內部呼叫到enqueueMessage(MessageQueue, Message, long)
時,會把 Handler 物件自身賦值到 Message 的 target 上,這樣 message 就知道去找哪個 Handler 執行handleMessage(msg: Message)
方法。也是因為這個持有,導致瞭如果訊息沒有立刻被執行,就會一直持有 Handler 物件,此時如果關閉 Activity ,就會導致記憶體漏失。
原因是 Handler 以匿名內部類或內部類的形式宣告並建立的,會持有外部 Activity 的參照。從而導致持有關係是:
Message -> Handler -> Activity
實現 Handler 記憶體漏失的程式碼:
// 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 中呼叫這個方法後,立刻 finish LeakCanaryActivity ,然後檢視記憶體漏失情況:
postDelayed 實際上是把 Runnable 封裝成了一個 Message 物件,傳入的 Runnable 引數被賦值給了 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; }
而最終執行邏輯的方法都是 sendMessageDelayed(Message, long)
,所以和 sendMessage 一樣都會導致記憶體漏失。與之不同的是,postDelayed
的洩漏會多一個Message#callback
因為 在呼叫postDelayed
時,第一個是個匿名內部類物件,多了一個參照。
handler.postDelayed(object : Runnable { override fun run() { Log.d(TAG, "postdelay done") } }, 10000)
一個極其簡單的記憶體漏失場景是,當我在一個 Activity 內多次彈出 Toast 時,立刻關閉當前 Activity ,就會導致記憶體漏失的情況出現:
// in LeakCanaryActivity private fun toastOOM() { Toast.makeText(this, "toast leak", Toast.LENGTH_SHORT).show() }
操作步驟:將上面的方法設定在某個點選事件中,快速連續點選幾次,然後立刻關閉當前 Activity ,檢視 Profiler:
最常見的場景是觀察者模式,觀察者模式中註冊一些觀察者物件,一般是儲存到一個全域性的集合中,如果觀察者物件在釋放時不及時登出,就會造成記憶體漏失:
object LeakCollection { val list = ArrayList<Any>() } class LeakCanaryActivity : ComponentActivity() { // ... private fun collectionOOM() { LeakCollection.list.add(this) } }
操作步驟:在 LeakCanaryActivity 內呼叫collectionOOM()
,然後立刻 finish 。
最常見的解決辦法就是在 Activity 的 destroy 時,從 list 清除自身的參照。
網上都說 WebView 會導致記憶體漏失。通過 Profiler 直接檢視並沒有明顯的一個 Leaks 提示。那麼如何排查這個記憶體洩露呢?
一個思路是參照對比實驗:
在同一個 Root Activity 中分別開啟 A 和 B ,通過對比記憶體變化,來證明 WebView 是否真的造成了記憶體漏失。
首先是開啟了 NoLeakActivity, 並沒有明顯的記憶體變化。
然後返回到 LeakCannaryActivity ,記憶體還是沒有變化。接著開啟 LeakWebViewActivity ,發現記憶體明顯上升,主要上升在 Native 、Others 和 Graphics 。 Graphics 可以理解,因為 loadUrl 失敗了會顯示一個失敗頁面,其中有個 icon 圖片,所以主要分析的點是 Native 和 Others 。
然後返回到 LeakCanaryActivity, 記憶體基本沒有變化。
為了證明,不是因為 NoLeakActivity 先開啟,LeakWebViewActivity 後開啟,所以記憶體中會有多餘的 NoLeakActivity 相關的記憶體佔用,我們再次開啟 NoLeakActivity ,再返回,記憶體仍無明顯變化。
所以,基本上可以證明,WebView 沒有隨著 Activity 的銷燬而被回收。
但是如何解決這種情況呢?這個問題值得後續仔細研究一下。但目前網上的各種奇怪的解決方案(例如開啟一個單獨的程序)並不是合理的辦法。 一個說法是,在 xml 裡面是有 WebView 會出現記憶體漏失,但是如果通過 addView 的形式去使用不會造成,以下是通過 addView 的形式新增 一個 WebView 物件的記憶體變化。
而這是通過 XML 的形式使用 WebView 的記憶體變化。
兩種方法好像並沒有什麼區別,但有用的一點是,這裡的記憶體變化,主要體現在 Native 上,證明 WebView 元件,會在 Native 層面生成一些內容。
這個部分的分析,後續可以再深入研究。從應用層面來看,WebView 並沒有直接觸發再 Java heap 上的記憶體漏失。而是更底層的 Native heap 中。
另外需要注意的一點是,通過 LeakCanary 並不能精準的檢測到記憶體漏失,還是得用 Profiler。
短時間內頻繁建立物件,導致虛擬機器器頻繁觸發GC操作,頻繁的 GC 會導致畫面卡頓。
onDraw()
方法會被頻繁呼叫,所以在這裡面不應該頻繁的建立物件。基本上減少記憶體優化的其他思路就是複用和壓縮資源。
實現 Application 中的 onTrimMemory/onLowMemory 方法去釋放掉圖片快取、靜態快取來自保。
class BaseApplication: Application() { override fun onLowMemory() { super.onLowMemory() } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) } }
到此這篇關於Android 記憶體優化知識點梳理總結的文章就介紹到這了,更多相關Android 記憶體優化 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45