Android內(nèi)存優(yōu)化操作方法梳理總結(jié)
內(nèi)存泄露
內(nèi)存泄漏就是在當(dāng)前應(yīng)用周期內(nèi)不再使用的對(duì)象被GC Roots引用,導(dǎo)致不能回收,使實(shí)際可使用內(nèi)存變小,通俗點(diǎn)講,就是無(wú)法回收無(wú)用對(duì)象。這里總結(jié)了實(shí)際開(kāi)發(fā)中常見(jiàn)的一些內(nèi)存泄露的場(chǎng)景示例和解決方案。
非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例
該實(shí)例的生命周期和應(yīng)用一樣長(zhǎng),非靜態(tài)內(nèi)部類會(huì)自動(dòng)持有外部類的引用,這就導(dǎo)致該靜態(tài)實(shí)例一直持有外部類Activity的引用。
class MemoryActivity : AppCompatActivity() { companion object { var test: Test? = null } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_memory) test = Test() } inner class Test { } }
解決方案:將非靜態(tài)內(nèi)部類改為靜態(tài)內(nèi)部類
class MemoryActivity : AppCompatActivity() { companion object { var test: Test? = null } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_memory) test = Test() } //kotlin的靜態(tài)內(nèi)部類 class Test { } }
注冊(cè)對(duì)象未注銷或資源對(duì)象未關(guān)閉
注冊(cè)了像BraodcastReceiver,EventBus這種,沒(méi)有在頁(yè)面銷毀時(shí)注銷的話,會(huì)引發(fā)泄露問(wèn)題,所以應(yīng)該在Activity銷毀時(shí)及時(shí)注銷。
類的靜態(tài)變量引用耗費(fèi)資源過(guò)多的實(shí)例
類的靜態(tài)變量生命周期等于應(yīng)用程序的生命周期,若其引用耗資過(guò)多的實(shí)例,如Context,當(dāng)引用實(shí)例需結(jié)束生命周期時(shí),會(huì)因靜態(tài)變量的持有而無(wú)法被回收,從而出現(xiàn)內(nèi)存泄露,這種情況比較常見(jiàn)的有單例持有context。
class SingleTon private constructor(val context: Context) { companion object { private var instance: SingleTon? = null fun getInstance(context: Context) = if (instance == null) SingleTon(context) else instance!! } }
當(dāng)我們?cè)贏ctivity中使用時(shí),當(dāng)Activity銷毀,就會(huì)出現(xiàn)內(nèi)存泄露
class MemoryActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_memory) SingleTon.getInstance(this) } }
這種情況可以使用applicationContext,因?yàn)锳pplication的生命周期就等于整個(gè)應(yīng)用的生命周期
class SingleTon private constructor(context: Context) { private var context: Context init { this.context = context.applicationContext } companion object { private var instance: SingleTon? = null fun getInstance(context: Context) = if (instance == null) SingleTon(context) else instance!! } }
Handler引發(fā)的內(nèi)存泄露
class MemoryActivity : AppCompatActivity() { private val tag = javaClass.simpleName private val handler = object : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { Log.i(tag, "handleMessage:$msg") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_memory) thread(start = true) { handler.sendEmptyMessageDelayed(1, 10000) } } }
當(dāng)Activity被finish時(shí),延遲發(fā)送的消息仍會(huì)存活在UI線程的消息隊(duì)列中,直到10s后才被處理,這個(gè)消息持有handler的引用,由于非靜態(tài)內(nèi)部類或匿名類會(huì)隱式持有外部類的引用,handler隱式持有外部類也就是Activity的引用,這個(gè)引用會(huì)一直存在直到這個(gè)消息被處理,所以垃圾回收機(jī)制就沒(méi)法回收而導(dǎo)致內(nèi)存泄露。
解決方案:靜態(tài)內(nèi)部類+弱引用,靜態(tài)內(nèi)部類不會(huì)持有外部類的引用,如需handler內(nèi)調(diào)用外部類Activity的方法的話,可以讓handler持有外部類Activity的弱引用,這樣Activity就不會(huì)有泄露風(fēng)險(xiǎn)了。
class MemoryActivity : AppCompatActivity() { companion object { private const val tag = "uncle" } private lateinit var handler: Handler override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_memory) handler = MyHandler(this) thread(start = true) { handler.sendEmptyMessageDelayed(1, 10000) } } class MyHandler(activity: Activity) : Handler(Looper.getMainLooper()) { private val reference = WeakReference(activity) override fun handleMessage(msg: Message) { super.handleMessage(msg) if (reference.get() != null) { Log.i(tag, "handleMessage:$msg") } } } }
集合引發(fā)的內(nèi)存泄露
先看個(gè)例子,我們定義一個(gè)棧,裝著所有的Activity
class GlobalData { companion object { val activityStack = Stack<Activity>() } }
然后每啟動(dòng)一個(gè)Activity,就把此Activity加進(jìn)去,這個(gè)時(shí)候,如果你沒(méi)有在Activity銷毀時(shí)清掉集合中對(duì)應(yīng)的引用,就會(huì)出現(xiàn)泄露問(wèn)題。當(dāng)然,實(shí)際開(kāi)發(fā)中我們不會(huì)寫這么差的代碼,這只是簡(jiǎn)單提個(gè)醒,需要注意一下集合中的一些引用,如果會(huì)導(dǎo)致泄露的,記得及時(shí)在銷毀時(shí)清掉。
class MemoryActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_memory) GlobalData.activityStack.push(this) } }
檢測(cè)工具
排查內(nèi)存泄露,需要一些工具的支持,這里主要介紹常用的兩個(gè),LeakCanary和Android Studio Profiler。
LeakCanary
一行代碼引入
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
當(dāng)你測(cè)試包安裝時(shí),手機(jī)上就會(huì)有個(gè)伴生APP,用來(lái)記錄內(nèi)存泄露信息的。
就拿上面集合引發(fā)的泄露例子來(lái)說(shuō),LeakCanary就會(huì)彈出通知并且Leaks APP中顯示內(nèi)存泄露信息,我們以此來(lái)定位內(nèi)存泄露問(wèn)題。
Android Studio Profiler
同樣,我們拿上面集合的泄漏例子來(lái)看,首先,我們點(diǎn)擊MEMORY
然后,普通的內(nèi)存問(wèn)題選擇Capture heap dump就行了
點(diǎn)擊Record,就會(huì)抓取一段時(shí)間的內(nèi)存分配信息
Leaks就是記錄內(nèi)存泄漏的,然后我們點(diǎn)擊進(jìn)去,就可以看到具體類位置了
再點(diǎn)擊進(jìn)去具體的類,就可以看到泄漏的原因啦
內(nèi)存溢出
Android系統(tǒng)中每個(gè)應(yīng)用程序可以向系統(tǒng)申請(qǐng)一定的內(nèi)存,當(dāng)申請(qǐng)的內(nèi)存不夠用的時(shí)候,就會(huì)產(chǎn)生內(nèi)存溢出,俗稱OOM,全稱Out Of Memory,就是內(nèi)存用完了。在實(shí)際開(kāi)發(fā)中,出現(xiàn)這種現(xiàn)象通常是因?yàn)閮?nèi)存泄露太多或大圖加載問(wèn)題,內(nèi)存泄露上面已經(jīng)講了,那么,下面就主要講講圖片的優(yōu)化吧!
Bitmap優(yōu)化
(1)及時(shí)回收Bitmap內(nèi)存,這時(shí)可能有人就要問(wèn)了,Android有自己的垃圾回收機(jī)制,為什么還要我們?nèi)セ厥漳??因?yàn)樯葿itmap最終是通過(guò)JNI方法實(shí)現(xiàn)的,也就是說(shuō),Bitmap的加載包含兩部分的內(nèi)存區(qū)域,一是Java部分,一是C部分。Java部分會(huì)自動(dòng)回收,但是C部分不會(huì),所以需要調(diào)用recycle來(lái)釋放C部分的內(nèi)存。那如果不調(diào)用就一定會(huì)出現(xiàn)泄露嗎?那也不是的,Android每個(gè)應(yīng)用都在獨(dú)立的進(jìn)程,進(jìn)程被關(guān)掉的話,內(nèi)存也就都被釋放了。
if (bitmap != null && !bitmap.isRecycled) { bitmap.recycle() bitmap = null }
(2)捕獲異常,Bitmap在使用的時(shí)候,最好捕獲一下OutOfMemoryError以免crash掉,你還可以設(shè)置一個(gè)默認(rèn)的圖片。
var bitmap: Bitmap? = null try { bitmap = BitmapFactory.decodeFile(filePath) imageView.setImageBitmap(bitmap) } catch (e: OutOfMemoryError) { //捕獲異常 } if (bitmap == null) { imageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.picture)) }
(3)壓縮,對(duì)于分辨率比較高的圖片,我們應(yīng)該加載一個(gè)縮小版,這里采用的是采樣率壓縮法。
val options = BitmapFactory.Options() //設(shè)置為true可以讓解析方法禁止為bitmap分配內(nèi)存,返回null,同時(shí)能獲取到長(zhǎng)寬值,從而根據(jù)情況進(jìn)行壓縮 options.inJustDecodeBounds = true BitmapFactory.decodeResource(resources, R.drawable.large_picture, options) val imgHeight = options.outHeight val imgWidth = options.outWidth //通過(guò)改變inSampleSize的值來(lái)壓縮圖片 var inSampleSize = 1 //imgWidth為圖片的寬,viewWidth為實(shí)際控件的寬 if (imgHeight > viewHeight || imgWidth > viewWidth) { val heightRatio = round(imgHeight / viewHeight.toFloat()).toInt() val widthRatio = round(imgWidth / viewWidth.toFloat()).toInt() //選擇最小比率作為inSampleSize的值,可保證最終圖片的寬高一定大于等于目標(biāo)的寬高 inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio } options.inSampleSize = inSampleSize //計(jì)算完后inJustDecodeBounds重置為false options.inJustDecodeBounds = false val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large_picture, options) imageView.setImageBitmap(bitmap)
如果程序中的圖片是本地資源或者是自己服務(wù)器上的,那這個(gè)大小我們可以自行調(diào)整,只要注意圖片不要太大,及時(shí)回收Bitmap,就能避免OOM的發(fā)生。如果圖片來(lái)源是外界,這個(gè)時(shí)候就要特別注意了,可以采用壓縮圖片或捕獲異常,避免OOM的產(chǎn)生而導(dǎo)致程序崩潰。
內(nèi)存抖動(dòng)
頻繁地創(chuàng)建對(duì)象,會(huì)導(dǎo)致內(nèi)存抖動(dòng),最終可能會(huì)導(dǎo)致卡頓或OOM,因?yàn)榇罅颗R時(shí)對(duì)象頻繁創(chuàng)建會(huì)導(dǎo)致內(nèi)存碎片,當(dāng)需要分配內(nèi)存時(shí),雖然總體上還有剩余內(nèi)存,但由于這些內(nèi)存不連續(xù),無(wú)法整塊分配,系統(tǒng)會(huì)視為內(nèi)存不夠,故導(dǎo)致OOM。
常見(jiàn)場(chǎng)景為大循環(huán)中創(chuàng)建對(duì)象,自定義View的onDraw方法中創(chuàng)建對(duì)象,因?yàn)槠聊焕L制會(huì)頻繁調(diào)用onDraw方法。我們可以將這些操作放在循環(huán)外或onDraw方法外,避免頻繁創(chuàng)建對(duì)象。
到此這篇關(guān)于Android內(nèi)存優(yōu)化操作方法梳理總結(jié)的文章就介紹到這了,更多相關(guān)Android內(nèi)存優(yōu)化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android遞歸方式刪除某文件夾下的所有文件(.mp3文件等等)
以刪除為例,當(dāng)然,對(duì)于遍歷某文件夾下的所有文件均可用這個(gè)方法。如搜索.mp3文件等,具體實(shí)現(xiàn)如下,感興趣的朋友可以參考下哈2013-06-06Android使用Jetpack Compose開(kāi)發(fā)零基礎(chǔ)起步教程
Jetpack Compose是用于構(gòu)建原生Android UI的現(xiàn)代工具包。Jetpack Compose使用更少的代碼,強(qiáng)大的工具和直觀的Kotlin API,簡(jiǎn)化并加速了Android上的UI開(kāi)發(fā)2023-04-04詳解Android開(kāi)發(fā)之MP4文件轉(zhuǎn)GIF文件
這篇文章介紹的是將錄下來(lái)的視頻選取一小段轉(zhuǎn)為 GIF 文件,不僅時(shí)間段可以手動(dòng)選取,而且還需要支持截取視頻的局部區(qū)域轉(zhuǎn)為 GIF,網(wǎng)上調(diào)研了一下技術(shù)方案,覺(jué)得還是有必要把實(shí)現(xiàn)過(guò)程拿出來(lái)分享下,有需要的可以直接拿過(guò)去用。下面來(lái)一起看看。2016-08-08Android編程動(dòng)態(tài)按鈕實(shí)現(xiàn)方法
這篇文章主要介紹了Android編程動(dòng)態(tài)按鈕實(shí)現(xiàn)方法,分享了onTouch方法及xml調(diào)用兩種實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-10-10Android使用CardView作為RecyclerView的Item并實(shí)現(xiàn)拖拽和左滑刪除
這篇文章主要介紹了Android使用CardView作為RecyclerView的Item并實(shí)現(xiàn)拖拽和左滑刪除,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11android 上傳aar到私有maven服務(wù)器的示例
這篇文章主要介紹了android 上傳aar到私有maven服務(wù)器,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11Android自定義view實(shí)現(xiàn)雪花特效實(shí)例代碼
實(shí)現(xiàn)雪花的效果其實(shí)也可以通過(guò)自定義View的方式來(lái)實(shí)現(xiàn)的,而且操作上也相對(duì)簡(jiǎn)單一些,下面這篇文章主要給大家介紹了關(guān)于Android自定義view實(shí)現(xiàn)雪花特效的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12Android 邊播邊緩存的實(shí)現(xiàn)(MP4 未加密m3u8)
這篇文章主要介紹了Android 邊播邊緩存的實(shí)現(xiàn)(MP4 未加密m3u8),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11