我希望通過(guò)這篇文章能夠把Android內(nèi)存相關(guān)的基礎(chǔ)和大部分內(nèi)存相關(guān)問(wèn)題如:溢出饮六、泄漏羡洁、圖片等等產(chǎn)生的都講解清楚玷过,會(huì)從java內(nèi)存逐步講解到android內(nèi)存并結(jié)合具體場(chǎng)景分析、總結(jié)常見內(nèi)存問(wèn)題原因筑煮,并給出解決辦法辛蚊。文章有點(diǎn)長(zhǎng),文字也較多真仲,可能還有點(diǎn)啰嗦袋马,若有不正確,請(qǐng)指出秸应,我會(huì)進(jìn)行優(yōu)化改進(jìn)虑凛。
Java 內(nèi)存
引用
引用類型(reference type)指向一個(gè)對(duì)象碑宴,不是原始值,指向?qū)ο蟮淖兞渴且米兞可5趈ava里面除去基本數(shù)據(jù)類型的其它類型都是引用數(shù)據(jù)類型延柠,用類的一個(gè)類型聲明的變量被指定為引用類型,這是因?yàn)樗谝靡粋€(gè)非原始類型锣披,引用實(shí)際上是存儲(chǔ)對(duì)象的地址贞间。
值傳遞和引用傳遞:
- ? “在Java里面參數(shù)傳遞都是按值傳遞”這句話的意思是:按值傳遞是傳遞的值的拷貝,按引用傳遞其實(shí)傳遞的是引用的地址值雹仿,所以統(tǒng)稱按值傳遞增热。
- ? 在Java里面只有基本類型和按照下面這種定義方式的String是按值傳遞,其它的都是按引用傳遞盅粪。就是直接使用雙引號(hào)定義的字符串方式:String str = "Java快車";
“=”的含義
在JAVA里钓葫,“=”不能被看成是一個(gè)賦值語(yǔ)句悄蕾,它不是在把一個(gè)對(duì)象賦給另外一個(gè)對(duì)象票顾,它的執(zhí)行過(guò)程實(shí)質(zhì)上是將右邊對(duì)象的地址傳給了左邊的引用,使得左邊的引用指向了右邊的對(duì)象在初始化時(shí)帆调,“=”語(yǔ)句左邊的是引用奠骄,右邊new出來(lái)的是對(duì)象。
this指針
this 關(guān)鍵字是類內(nèi)部當(dāng)中對(duì)自己的一個(gè)引用番刊,可以返回對(duì)象的自己這個(gè)類的引用含鳞,同時(shí)還可以在一個(gè)構(gòu)造函數(shù)當(dāng)中調(diào)用另一個(gè)構(gòu)造函數(shù),Java中關(guān)鍵字this指針只能用于方法內(nèi)芹务,當(dāng)一個(gè)對(duì)象被創(chuàng)建后蝉绷,JVM就會(huì)給這個(gè)對(duì)象分配一個(gè)引用自身的指針,就是this枣抱。this只能在類中的非靜態(tài)方法(實(shí)例方法)中使用熔吗,靜態(tài)方法(類方法)和靜態(tài)代碼塊中不能出現(xiàn)this。this只和特定對(duì)象關(guān)聯(lián)佳晶,不個(gè)類關(guān)聯(lián)桅狠,所以同一個(gè)類的不同對(duì)象有不同的this。
內(nèi)存模型
每一個(gè)Java應(yīng)用都唯一對(duì)應(yīng)一個(gè)JVM實(shí)例轿秧,每一個(gè)實(shí)例唯一對(duì)應(yīng)一個(gè)堆中跌。JVM的內(nèi)存主要可分為3個(gè)區(qū):堆(heap)、棧(stack)和方法區(qū)(method)菇篡。(其他暫不考慮)
堆區(qū)(Heap)
?只存對(duì)象本身漩符,不存基本類型(局部變量)和引用對(duì)象, JVM只有一個(gè)堆區(qū)驱还,并被所有線程共享嗜暴。
棧區(qū)(Stack)
?棧中只保存基礎(chǔ)數(shù)據(jù)類型的對(duì)象和對(duì)象引用津滞。每個(gè)線程一個(gè)棧區(qū),每個(gè)棧區(qū)中的數(shù)據(jù)都是私有的灼伤,其他棧不能訪問(wèn)触徐。棧分三個(gè)部分:基本類型變量區(qū),執(zhí)行環(huán)境上下文狐赡,操作指令區(qū)撞鹉。為即時(shí)調(diào)用的方法開辟空間棧(Stack)該區(qū)域具有先進(jìn)后出的特性。當(dāng)該變量退出該作用域后颖侄,Java會(huì)自動(dòng)釋放掉為該變量所分配的內(nèi)存空間鸟雏,該內(nèi)存空間可以立即被另作他用。
方法區(qū)(method)
又叫靜態(tài)區(qū)览祖,跟堆一樣孝鹊,被所有線程共享, 方法區(qū)包含所有的class和static變量展蒂,方法區(qū)包含的都是在整個(gè)程序中永遠(yuǎn)唯一的元素又活。
圖解:
一個(gè)程序運(yùn)行時(shí),內(nèi)存的整個(gè)過(guò)程:
注意:
1锰悼、類里的基本類型的成員變量存放在哪里柳骄?
?實(shí)例變量和對(duì)象駐留在堆上,局部變量駐留在棧上 。在類中聲明的變量是成員變量箕般,也叫全局變量耐薯,放在堆中的,同樣在類中聲明的變量即可是基本類型的變量 也可是引用類型的變量丝里,當(dāng)聲明的是基本類型的變量其變量名及其只時(shí)放在堆類存中的曲初。引用類型時(shí),其聲明的變量仍然會(huì)存儲(chǔ)一個(gè)內(nèi)存地址值杯聚,該內(nèi)存地址值指向所引用的對(duì)象臼婆。但這和書上所說(shuō):堆區(qū)(Heap)-- 只存對(duì)象本身,不存基本類型和引用對(duì)象 有些區(qū)別械媒。
2目锭、方法是通過(guò)什么訪問(wèn)類中的變量的?
- 成員變量:包括實(shí)例變量和類變量纷捞,用static修飾的是類變量痢虹,不用static修飾的是實(shí)例變量,所有類的成員變量可以通過(guò)this來(lái)引用主儡。
- 類變量:靜態(tài)域奖唯,靜態(tài)字段,或叫靜態(tài)變量糜值,它屬于該類所有實(shí)例共有的屬性丰捷。而且所有的實(shí)例都可以修改這個(gè)類變量的值(這個(gè)類變量沒有被final修飾的情況)坯墨,而且訪問(wèn)類變量的時(shí)候不用實(shí)例,直接用類名.的方式就可以病往。
- 成員方法:包括實(shí)例方法和類方法捣染,用static的方法就是類方法,不用static修飾的就是實(shí)例方法停巷。實(shí)例方法必須在創(chuàng)建實(shí)例之后才可以調(diào)用耍攘。
- 類方法:和類變量一樣,可以不用實(shí)例畔勤,直接用類就可以調(diào)用類方法蕾各。
3、在類方法中可用this來(lái)調(diào)用本類的類方法:錯(cuò)誤
4庆揪、final 修飾的變量存放在哪里式曲?
堆內(nèi)的!缸榛!并且在方法區(qū)內(nèi)存中只有一份A咝摺!與所有線程共享訪問(wèn)仔掸! final 聲明一個(gè)變量只是表明這個(gè)變量的值不可改變脆贵,修飾類的時(shí)候,只是表明這個(gè)類不能被繼承
Java是如何管理內(nèi)存
? Java的內(nèi)存管理就是對(duì)象的分配和釋放問(wèn)題起暮。在Java中,程序員需要通過(guò)關(guān)鍵字new為每個(gè)對(duì)象申請(qǐng)內(nèi)存空間 (基本類型除外)会烙,所有的對(duì)象都在堆 (Heap)中分配空間负懦,對(duì)象的釋放是由GC決定和執(zhí)行的。GC它也加重了JVM的工作柏腻,這也是Java程序運(yùn)行速度較慢的原因之一纸厉。因?yàn)椋珿C為了能夠正確釋放對(duì)象五嫂,GC必須監(jiān)控每一個(gè)對(duì)象的運(yùn)行狀態(tài)颗品,包括對(duì)象的申請(qǐng)、引用沃缘、被引用躯枢、賦值等,GC都需要進(jìn)行監(jiān)控槐臀。監(jiān)視對(duì)象狀態(tài)是為了更加準(zhǔn)確地锄蹂、及時(shí)地釋放對(duì)象,而釋放對(duì)象的根本原則就是該對(duì)象不再被引用水慨。
內(nèi)存溢出和內(nèi)存泄漏
內(nèi)存溢出
內(nèi)存溢出就是你要求分配的內(nèi)存超出了系統(tǒng)能給你的得糜,系統(tǒng)不能滿足需求敬扛,于是產(chǎn)生溢出。
內(nèi)存泄漏
Java內(nèi)存泄漏是指對(duì)象已經(jīng)沒有被應(yīng)用程序使用朝抖,但是垃圾回收器沒辦法移除它們啥箭,因?yàn)檫€在被引用著。在堆上分配的內(nèi)存沒有被釋放治宣,從而失去對(duì)其控制捉蚤,這樣會(huì)造成程序能使用的內(nèi)存越來(lái)越少,導(dǎo)致系統(tǒng)運(yùn)行速度減慢炼七,嚴(yán)重情況會(huì)使程序當(dāng)?shù)簟?/p>
在Java中缆巧,內(nèi)存泄漏就是存在一些被分配的對(duì)象,這些對(duì)象有下面兩個(gè)特點(diǎn)豌拙,首先陕悬,這些對(duì)象是可達(dá)的,即在有向圖中按傅,存在通路可以與其相連捉超,被引用著;其次唯绍,這些對(duì)象是無(wú)用的拼岳,即程序以后不會(huì)再使用這些對(duì)象。如果對(duì)象滿足這兩個(gè)條件况芒,這些對(duì)象就可以判定為Java中的內(nèi)存泄漏惜纸,這些對(duì)象不會(huì)被GC所回收,然而它卻占用內(nèi)存绝骚。
那如何避免內(nèi)存泄漏和溢出
要避免內(nèi)存泄漏耐版,就需要使對(duì)象符合GC回收的條件:對(duì)象不再被引用。那如何顯示的使對(duì)象符合垃圾回收條件压汪?
空引用 :當(dāng)對(duì)象沒有對(duì)他可到達(dá)引用時(shí)粪牲,他就符合垃圾回收的條件。Object obj=null止剖;
重新為引用變量賦值:可以通過(guò)設(shè)置引用變量引用另一個(gè)對(duì)象來(lái)解除該引用變量與一個(gè)對(duì)象間的引用關(guān)系腺阳。
方法內(nèi)創(chuàng)建的對(duì)象:所創(chuàng)建的局部變量?jī)H在該方法的作用期間內(nèi)存在。一旦該方法返回穿香,在這個(gè)方法內(nèi)創(chuàng)建的對(duì)象就符合垃圾收集條件亭引。但有一種明顯的例外情況,就是方法返回對(duì)象扔水。
隔離引用:這種情況中痛侍,被回收的對(duì)象仍具有引用,這種情況稱作隔離島。若存在這兩個(gè)實(shí)例主届,他們互相引用赵哲,并且這兩個(gè)對(duì)象的所有其他引用都刪除,其他任何線程無(wú)法訪問(wèn)這兩個(gè)對(duì)象中的任意一個(gè)君丁。也可以符合垃圾回收條件枫夺。
盡量不要重寫 finalize(),所有類從 Object 類繼承這個(gè)方法绘闷。
盡量少用靜態(tài)變量 橡庞,因?yàn)殪o態(tài)變量是全局的,GC 不會(huì)回收的印蔗。
如果非要使用某個(gè)可能會(huì)造成泄漏的對(duì)象扒最,考慮:軟引用(SoftReference)、弱引用(WeakReference)华嘹、虛引用(PhantomReference)
注意
final修飾的變量會(huì)不會(huì)內(nèi)存泄漏吧趣? ?final 聲明一個(gè)變量只是表明這個(gè)變量的值不可改變,修飾類的時(shí)候耙厚,只是表明這個(gè)類不能被繼承而已强挫,使用不當(dāng)還是會(huì)泄漏。
Android內(nèi)存
Android 內(nèi)存處理一直是android開發(fā)者必須要面臨的問(wèn)題薛躬,如果持有對(duì)象的強(qiáng)引用俯渤,垃圾回收器是無(wú)法在內(nèi)存中回收這個(gè)對(duì)象。良好的內(nèi)存優(yōu)化和處理能讓app流暢的運(yùn)行型宝。但一個(gè)app內(nèi)存的占用不是越少越好八匠,頻繁的內(nèi)存gc也會(huì)增加負(fù)擔(dān),造成卡頓诡曙。找到適合具體場(chǎng)景的內(nèi)存處理方案臀叙,才是最適合的。
內(nèi)存溢出泄漏問(wèn)題
一般內(nèi)存泄漏(traditional memory leak)的原因是:由忘記釋放分配的內(nèi)存導(dǎo)致的价卤。(Cursor
忘記關(guān)閉等)。邏輯內(nèi)存泄漏(logical memory leak)的原因是:當(dāng)應(yīng)用不再需要這個(gè)對(duì)象渊涝,當(dāng)仍未釋放該對(duì)象的所有引用慎璧,在Android開發(fā)中,最容易引發(fā)的內(nèi)存泄漏問(wèn)題的是Context跨释。比如Activity的Context胸私,就包含大量的內(nèi)存引用,例如View Hierarchies和其他資源鳖谈。一旦泄漏了Context岁疼,也意味泄漏它指向的所有對(duì)象。Android機(jī)器內(nèi)存有限,太多的內(nèi)存泄漏容易導(dǎo)致OOM捷绒。Activity.onDestroy()被視為Activity生命的結(jié)束瑰排,程序上來(lái)看,它應(yīng)該被銷毀了暖侨,或者Android系統(tǒng)需要回收這些內(nèi)存(注:當(dāng)內(nèi)存不夠時(shí)椭住,Android會(huì)回收看不見的Activity)。Acticity泄漏兩種情況:
- 全局進(jìn)程(process-global)的static變量字逗,這個(gè)無(wú)視應(yīng)用的狀態(tài)京郑,持有
Activity
的強(qiáng)引用的怪物。 - 活在
Activity
生命周期之外的線程葫掉,沒有清空對(duì)Activity
的強(qiáng)引用些举。
?圖片,每一款app都離不開圖片俭厚,然而圖片才是內(nèi)存占用的大戶户魏。Bitmap的不當(dāng)使用,導(dǎo)致內(nèi)存溢出套腹。如在類似電商和新聞?lì)惖腶pp中有大量的圖片要進(jìn)行處理绪抛,圖片的處理就要用到Bitmap,Android的內(nèi)存是有限的电禀,如果不對(duì)圖片進(jìn)行良好的優(yōu)化幢码,就會(huì)導(dǎo)致內(nèi)存溢出,程序卡頓尖飞,程序崩潰症副。
問(wèn)題種類:
Static Activities/Static Views
在類中定義了靜態(tài)Activity
變量,把當(dāng)前運(yùn)行的Activity
實(shí)例賦值于這個(gè)靜態(tài)變量。在類中定義了靜態(tài)view
變量充择。
解決:使用軟引用/在onDestroy時(shí)把View=null掌桩;
Sensor Manager(傳感器管理)
通過(guò)Context.getSystemService(int name)可以獲取系統(tǒng)服務(wù)、傳感器等辕坝。這些服務(wù)工作在各自的進(jìn)程中,幫助應(yīng)用處理后臺(tái)任務(wù)荐健,處理硬件交互酱畅。如果需要使用這些服務(wù),可以注冊(cè)監(jiān)聽器江场,這會(huì)導(dǎo)致服務(wù)持有了Context的引用纺酸,如果在Activity銷毀的時(shí)候沒有注銷這些監(jiān)聽器,會(huì)導(dǎo)致內(nèi)存泄漏址否。
解決:在Activity結(jié)束時(shí)注銷監(jiān)聽器餐蔬,sensorManager.unregisterListener(this, sensor);
Inner Classes(內(nèi)部類)
Activity
中有個(gè)內(nèi)部類,這樣做可以提高可讀性和封裝性,但內(nèi)部類的優(yōu)勢(shì)之一就是可以訪問(wèn)外部類樊诺,不幸的是仗考,如果用static修飾內(nèi)部類變量,就會(huì)導(dǎo)致內(nèi)存泄漏啄骇,就是內(nèi)部類持有外部類實(shí)例的強(qiáng)引用痴鳄。
解決:不用static,要么不寫成內(nèi)部類缸夹。
Anonymous Classes(匿名類)
匿名類也維護(hù)了外部類的引用痪寻。所以內(nèi)存泄漏很容易發(fā)生。常用示例:
使用AsycTsk
當(dāng)你在Activity
中定義了匿名的AsyncTsk
虽惭。當(dāng)異步任務(wù)在后臺(tái)執(zhí)行耗時(shí)任務(wù)期間橡类,Activity
不幸被銷毀了(注:用戶退出,系統(tǒng)回收)芽唇,這個(gè)被AsyncTask
持有的Activity
實(shí)例就不會(huì)被垃圾回收器回收顾画,直到異步任務(wù)結(jié)束。Handler
定義匿名的Runnable
匆笤,用匿名類Handler
執(zhí)行研侣。Runnable
內(nèi)部類會(huì)持有外部類的隱式引用,被傳遞到Handler
的消息隊(duì)列MessageQueue
中炮捧,在Message
消息沒有被處理之前庶诡,Activity
實(shí)例不會(huì)被銷毀了,于是導(dǎo)致內(nèi)存泄漏咆课。Threads/TimerTask
通過(guò)Thread和TimerTask來(lái)展現(xiàn)內(nèi)存泄漏末誓,只要是匿名類的實(shí)例,不管是不是在工作線程书蚪,都會(huì)持有Activity
的引用喇澡,導(dǎo)致內(nèi)存泄漏。
解決:
靜態(tài)內(nèi)部類不持有外部類的引用:
private static class NimbleTask extends AsyncTask<Void, Void, Void> {...}
private static class NimbleHandler extends Handler {...}
private static class NimbleTimerTask extends TimerTask {...}如果你堅(jiān)持使用匿名類殊校,只要在生命周期結(jié)束時(shí)中斷線程就可以晴玖。
靜態(tài)內(nèi)部類非要用引用外部類,可以和軟引用結(jié)合使用:
private static class MyHandler extends Handler {
WeakReference<MainActivity> mActivity;
MyHandler(MainActivity mActivity){
this.mActivity = new WeakReference<MainActivity>(mActivity);
}
@Override
public void handleMessage(Message msg) {
//TODO
}
}
注意
不論哪一種为流,都不要忘記在生命結(jié)束時(shí)調(diào)用響應(yīng)的關(guān)閉方法或者移除窜醉、清理等,例如:在Activity onStop或者onDestroy的時(shí)候艺谆,取消掉該Handler對(duì)象的Message和Runnable,removeCallbacks(Runnable r)和removeMessages(int what)等拜英。
Image(Bitmap)
什么是bitmap静汤?Bit即比特,是目前計(jì)算機(jī)系統(tǒng)里邊數(shù)據(jù)的最小單位,8個(gè)bit即為一個(gè)Byte虫给。一個(gè)bit的值藤抡,或者是0,或者是1抹估;也就是說(shuō)一個(gè)bit能存儲(chǔ)的最多信息是2缠黍。Bitmap可以理解為通過(guò)一個(gè)bit數(shù)組來(lái)存儲(chǔ)特定數(shù)據(jù)的一種數(shù)據(jù)結(jié)構(gòu);由于bit是數(shù)據(jù)的最小單位药蜻,所以這種數(shù)據(jù)結(jié)構(gòu)往往是非常節(jié)省存儲(chǔ)空間瓷式。
Bitmap是Android系統(tǒng)中的圖像處理的最重要類之一。用它可以獲取圖像文件信息语泽,進(jìn)行圖像剪切贸典、旋轉(zhuǎn)、縮放等操作踱卵,并可以指定格式保存圖像文件廊驼。
Bitmap占用的內(nèi)存,圖片(BitMap)占用的內(nèi)存=圖片長(zhǎng)度 * 圖片寬度*單位像素占用的字節(jié)數(shù)惋砂。前兩個(gè)分別代表長(zhǎng)度與寬度(像素單位)妒挎,單位像素占用字節(jié)數(shù)其大小由BitmapFactory.Options的inPreferredConfig變量決定。
inPreferredConfig為Bitmap.Config類型西饵,是個(gè)枚舉類型酝掩,對(duì)應(yīng)如下:
我們一般常用RGB_565。具體場(chǎng)景具體選擇罗标,他們這些格式有什么區(qū)別-具體參考 庸队。
注意:一張200k的圖片到內(nèi)存中并非200k!一般遠(yuǎn)大于200k闯割,具體可以自己寫demo測(cè)試彻消。
為什么圖片會(huì)引起內(nèi)存問(wèn)題?
使用圖片不當(dāng)為什么會(huì)造成oom或者卡頓宙拉?因?yàn)榘沧肯到y(tǒng)為每個(gè)程序分配的內(nèi)存大小是有限的宾尚,當(dāng)圖片(Bitmap)加載過(guò)多、過(guò)大谢澈,超出了給定的內(nèi)存就會(huì)出現(xiàn)內(nèi)存溢出煌贴,或者內(nèi)存泄漏引起(比如:1M大小Bitmap的泄漏了,我連續(xù)創(chuàng)建1000個(gè))锥忿。
常用解決方案:
緩存圖像到內(nèi)存牛郑,或者采用軟引用緩存到內(nèi)存,而不是在每次使用的時(shí)候都從新加載到內(nèi)存敬鬓。和軟引用結(jié)合是因?yàn)閮?nèi)存不足時(shí)GC會(huì)自動(dòng)回收軟引用的對(duì)象淹朋。
調(diào)整圖像大畜细鳌(縮略圖),可以根據(jù)控件大小調(diào)整相應(yīng)的圖片大小础芍。注意:我們從網(wǎng)上下載圖片到控件中杈抢,一般緩存到內(nèi)存的是調(diào)整過(guò)的圖片大小而不是原圖,比如:glide的DiskCacheStrategy.RESULT(緩存轉(zhuǎn)換后的資源)仑性。
采用低內(nèi)存占用量的編碼方式惶楼,比如Bitmap.Config.ARGB_565比Bitmap.Config.ARGB_8888更省內(nèi)存。
及時(shí)回收?qǐng)D像诊杆,如果引用了大量Bitmap對(duì)象歼捐,而應(yīng)用又不需要同時(shí)顯示所有圖片,可以將暫時(shí)用不到的Bitmap對(duì)象及時(shí)回收掉recycle()刽辙。問(wèn)題:為什么有GC了還要手動(dòng)釋放窥岩?
自定義堆內(nèi)存分配大小,優(yōu)化Dalvik虛擬機(jī)的堆內(nèi)存分配宰缤。(這個(gè)有點(diǎn)難颂翼,看看就行了,一般用不到)慨灭,使用:
private final staticfloatTARGET_HEAP_UTILIZATION = 0.75f;
//在程序onCreate時(shí)就可以調(diào)用
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);
private final static int CWJ_HEAP_SIZE = 6*1024* 1024 ;
//設(shè)置最小heap內(nèi)存為6MB大小
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
上述的方案有些并不一定很好朦乏!
案例分析:
說(shuō)明,這里分析的案例都是基于Android原生代碼氧骤。分析較為復(fù)雜的頁(yè)面呻疹,多層嵌套。案例的頁(yè)面結(jié)構(gòu)基本如下:有兩種方式筹陵,這兩種方式基本飽含類大部分新聞/電商(原生)的主頁(yè)結(jié)構(gòu)刽锤。
首先對(duì)于上圖的結(jié)構(gòu),有幾點(diǎn)基礎(chǔ)要講解:
Viewpage:適配器有兩種FragmentPagerAdapter和FragmentStatePagerAdapter朦佩,他們都會(huì)預(yù)先加載并思,但他們的緩存方式又不同,最低緩存兩頁(yè)语稠。FragmentPagerAdapter會(huì)把每次顯示的fragment都緩存宋彼,F(xiàn)ragmentStatePagerAdapter會(huì)把看不見的fragment回收,所以用FragmentStatePagerAdapter仙畦,fragment會(huì)執(zhí)行相應(yīng)的生命周期输涕。具體參考
Recycleview:recycleview會(huì)復(fù)用View,比如你有一百個(gè)item慨畸,每個(gè)item里都有imageview莱坎,但是recycleview并不會(huì)創(chuàng)建一百個(gè)imageview,有可能當(dāng)前ImageView加載圖片1寸士,當(dāng)滑動(dòng)是有可能會(huì)加載圖片2型奥。具體多少個(gè)是根據(jù)頁(yè)面來(lái)的瞳收,請(qǐng)自行測(cè)試。
下面將給出圖片內(nèi)存處理的思路:
1.只清理Bitmap-使用HashMap
上述頁(yè)面較為復(fù)雜厢汹。而且每一個(gè)看的見的頁(yè)面(fragment)都包含大量的圖片,因?yàn)橛泻芏嗌唐沸持妫翼?yè)面基本都是列表烫葬,列表也包含列表,并且頁(yè)面非常多凡蜻,它具體是采用Fragment+Viewpage+FragmentPagerAdapter+Fragment+Recycleview的結(jié)構(gòu)搭综。有時(shí)候我們只想清理圖片,因?yàn)閳D片占用內(nèi)存最高划栓,如何處理兑巾?
解決方案:
試想下,一個(gè)view控件如果加載到內(nèi)存能用多大空間忠荞?比如我創(chuàng)建1000個(gè)imageview蒋歌,其實(shí)非常少:
其實(shí)內(nèi)存幾乎被圖片(bitmap)占領(lǐng)了,只要頁(yè)面不可見時(shí)把頁(yè)面上控件里的Bitmap清理掉就ok了委煤。這樣只有控件占用內(nèi)存堂油。那么該怎么做呢?
方案:我們需要緩存所有的imageview(第三方控件不會(huì)緩存ImageView)和bitmap碧绞,然后根據(jù)判斷imageview不可見時(shí)府框,去掉引用!把imageview上bitmap的引用去掉(ImageView.setImageBitmap(null))讥邻,這樣只有緩存對(duì)象持有Bitmap的引用迫靖,在循環(huán)調(diào)用recycle()進(jìn)行清理。
參考上面內(nèi)存優(yōu)化的常用方法:調(diào)整片大小兴使、降低圖片編碼系宜、做緩存。但這里的難點(diǎn)是:
- 我們根據(jù)什么策略緩存鲫惶?
- 什么時(shí)候釋放內(nèi)存蜈首?因?yàn)橹灰床灰娋土ⅠR釋放那緩存就沒有意義了,每次都會(huì)重新加載欠母,這很愚蠢欢策!
- 如何判斷控件ImageView不可見?
- 如何根據(jù)ImageView不可見時(shí)釋放對(duì)應(yīng)的bitmap赏淌,他們的映射關(guān)系怎么建立踩寇?
首先要知道一點(diǎn)映射關(guān)系:在recycleview中一個(gè)ImageView能對(duì)應(yīng)多個(gè)Bitmap,因?yàn)閂iew會(huì)復(fù)用六水,但當(dāng)前顯示的ImageView只能對(duì)應(yīng)一個(gè)Bitmap俺孙。一個(gè)Bitmap也可以對(duì)應(yīng)多個(gè)ImageView辣卒。一個(gè)Bitmap只能對(duì)應(yīng)一個(gè)Url,但一個(gè)url能對(duì)應(yīng)多個(gè)bitmap睛榄。要處理ImageView和Bitmap的映射關(guān)系荣茫,就需要緩存他們兩個(gè)。
建立映射關(guān)系:那我們可以根據(jù)url和控件大小進(jìn)行進(jìn)行bitmap映射的:
public String getKeyForBitmap(String url) {
final int targetBitmapWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
final int targetBitmapHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
return curUrl.length() + "_" + url.hashCode() + "_" + targetBitmapWidth + "_" + targetBitmapHeight;
}
判斷釋放條件:釋放內(nèi)存是根據(jù)達(dá)到運(yùn)行時(shí)內(nèi)存的80%:
public void checkMemory() {
Runtime runtime = Runtime.getRuntime();
//判斷運(yùn)行時(shí)內(nèi)存是否達(dá)到80%场靴,超過(guò)就釋放
if (runtime.totalMemory() * 1f / runtime.maxMemory() > 0.8) {
trimMemory();
}
}
判斷view是否可見:ImageView的不可見-本身不可見啡莉,父控件不可見,context沒有了(activity銷毀了)旨剥。isShown()方法就能判斷View是不是可見:
public boolean isAbleToRecycle() {
return getContext() == null || !isShown() || getWindowVisibility() == View.GONE;
}
具體緩存代碼:
public class MemoryCacheController {
private static MemoryCacheController instance;
private String keyForBitmap;
public static MemoryCacheController getInstance() {
if (null == instance)
instance = new MemoryCacheController();
return instance;
}
/**
* 把建立過(guò)的set都存儲(chǔ)咧欣,免得每次都去新建
*/
private LinkedList<Set> setLinkedList = new LinkedList<>();
/**
* 根據(jù)具體key緩存bitmap,key是根據(jù)大小和url計(jì)算得來(lái)
*/
private HashMap<String, Bitmap> bitmapMap = new HashMap<>(100);
/**
* 根據(jù)bitmap映射imageview,set集合用來(lái)存放所有映射過(guò)的控件
*/
private HashMap<Bitmap, Set<ImgView>> bitmap2viewSetMap = new HashMap<>(100);
/**
* 根據(jù)ImageView映射bitmap
*/
private HashMap<ImgView, Bitmap> imgViewBitmapHashMap = new HashMap<>(100);
/**
* 把ImageView和Bitmap加入緩存轨帜。
*
* @param imgView
* @param keyForBitmap 緩存的key是根據(jù)Url和控件大小合成的特殊字符串
* @param bitmap
*/
public synchronized void put(ImgView imgView, String keyForBitmap, Bitmap bitmap) {
Bitmap lastBitmap = imgViewBitmapHashMap.get(imgView);
// 映射關(guān)系是否已存在
if (lastBitmap == bitmap) {
return;
} else if (lastBitmap != null) {// bitmap更換魄咕,映射關(guān)系調(diào)整
// 得到bitmap對(duì)應(yīng)的所有imageview
Set lastBitmapViewSet = bitmap2viewSetMap.get(lastBitmap);
if (null != lastBitmapViewSet) {
//移除bitmap映射的當(dāng)前的imageview
lastBitmapViewSet.remove(imgView);
}
//建立新的映射關(guān)系
bitmapMap.put(keyForBitmap, bitmap);
imgViewBitmapHashMap.put(imgView, bitmap);
Set viewSet = bitmap2viewSetMap.get(bitmap);
if (null == viewSet) {
viewSet = obtainSet();
bitmap2viewSetMap.put(bitmap, viewSet);
}
viewSet.add(imgView);
}
}
public synchronized Bitmap get(String keyForBitmap) {
return bitmapMap.get(keyForBitmap);
}
/**
* 釋放內(nèi)存
*/
public synchronized void trimMemory() {
LinkedList<Bitmap> recyclerBitmapList = new LinkedList<>();
for (Bitmap bitmap : bitmap2viewSetMap.keySet()) {
Set<ImgView> imgViewSet = bitmap2viewSetMap.get(bitmap);
boolean needRecycle = true;
for (ImgView imgView : imgViewSet) {
if (imgView.getCurBitmap() == bitmap) {
// 判斷View是否可見
if (!imgView.isAbleToRecycle()) {
needRecycle = false;
break;
}
}
}
if (needRecycle) {
recyclerBitmapList.add(bitmap);//把bitmap添加,說(shuō)明他能釋放了
}
}
LinkedList<String> keyList = new LinkedList<>();
for (Map.Entry<String, Bitmap> entry : bitmapMap.entrySet()) {
if (recyclerBitmapList.contains(entry.getValue())) {
keyList.add(entry.getKey());
}
}
for (String url : keyList) {
bitmapMap.remove(url);
}
// 先釋放ImageView的引用蚌父,在釋放bitmap
for (Bitmap bitmap : recyclerBitmapList) {
Set set = bitmap2viewSetMap.get(bitmap);
for (ImgView imgView : bitmap2viewSetMap.get(bitmap)) {
imgView.setImageBitmap(null);
imgViewBitmapHashMap.remove(imgView);
}
set.clear();
setLinkedList.add(set);
bitmap2viewSetMap.remove(bitmap);
if (null != bitmap && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
private Set obtainSet() {
Set set = setLinkedList.poll();
if (null == set) set = new HashSet(1);
return set;
}
public void checkMemory() {
Runtime runtime = Runtime.getRuntime();
//判斷運(yùn)行時(shí)內(nèi)存是否達(dá)到80%哮兰,超過(guò)就釋放,在Activity的onLowMemory里調(diào)用checkMemory
if (runtime.totalMemory() * 1f / runtime.maxMemory() > 0.8) {
trimMemory();
}
}
}
上面這種只是一種方案和思路梢什,也只是適合當(dāng)前的場(chǎng)景下奠蹬,但問(wèn)題也很多:
- ImageView和Bitmap的映射是比較麻煩的。
- 判斷內(nèi)存什么時(shí)候釋放嗡午,80%其實(shí)也不一定很好(安卓機(jī)型重多囤躁,很難判斷)。
- 應(yīng)用占有的內(nèi)存量會(huì)不斷攀升荔睹,直到內(nèi)存不足時(shí)狸演,出現(xiàn)斷崖式的內(nèi)存回收
- GC 的時(shí)間可能會(huì)比較長(zhǎng),造成界面會(huì)有明顯的卡頓僻他。
- GC 回收的內(nèi)存宵距,沒有區(qū)分,可能回收了最近在使用的 Bitmap吨拗,造成二次加載
- 頁(yè)面一加載就會(huì)緩存满哪,很多頁(yè)面沒用了也清理不掉,造成垃圾緩存劝篷。
2.使用弱引用緩存呢哨鸭?
? 弱引用也會(huì)出現(xiàn)斷崖式回收,回收時(shí)間長(zhǎng)娇妓,沒有區(qū)分像鸡,最嚴(yán)重的,新的 Android 系統(tǒng)開始每次 GC 都會(huì)回收弱引用哈恰,這就使內(nèi)存緩存沒有用處只估。
3.強(qiáng)引用 + LRU 算法
給定一個(gè)固定圖片緩存大小志群,將所有的使用的 Bitmap 用強(qiáng)引用的方式管理起來(lái),并利用 LRU 算法蛔钙,將舊的 Bitmap 釋放锌云,新的 bitmap 增加。LruCache的核心思想很好理解夸楣,就是要維護(hù)一個(gè)緩存對(duì)象列表--LinkedHashMap宾抓,其中對(duì)象列表的排列方式是按照訪問(wèn)順序?qū)崿F(xiàn)的,即一直沒訪問(wèn)的對(duì)象豫喧,將放在隊(duì)尾,即將被淘汰幢泼。而最近訪問(wèn)的對(duì)象將放在隊(duì)頭紧显,最后被淘汰。當(dāng)棧滿的時(shí)候就從棧底回收掉最舊的那個(gè)引用缕棵,這樣孵班,圖片緩存不會(huì)無(wú)限制的增長(zhǎng),內(nèi)存量也能處在一個(gè)較理想的范圍招驴,申請(qǐng)和釋放篙程。
但這個(gè)思路也會(huì)有問(wèn)題:
雖然圖片緩存的內(nèi)存不會(huì)無(wú)限制增長(zhǎng),但會(huì)周期性的釋放和申請(qǐng)别厘。特別是對(duì)于一個(gè)長(zhǎng)列表頁(yè)面虱饿,圖片會(huì)不斷的申請(qǐng),不斷的釋放触趴。因?yàn)樽罱K的內(nèi)存釋放還是GC去處理氮发,快速滑動(dòng)時(shí),會(huì)造成大量的圖片申請(qǐng)內(nèi)存冗懦,大量的圖片釋放爽冕,系統(tǒng)的 GC 會(huì)很頻繁,就產(chǎn)生了所謂的 內(nèi)存抖動(dòng) 披蕉。內(nèi)存的抖動(dòng)同樣也會(huì)造成界面卡頓颈畸,在快速滑動(dòng)時(shí),會(huì)非常明顯没讲。但要比弱引用的方案好多了眯娱。
說(shuō)說(shuō)Glide的方式
- Glide 構(gòu)建了一個(gè) BitmapPool , Bitmap 申請(qǐng)和回收都是透過(guò) BitmapPool 來(lái)處理的食零。新加載圖片時(shí)困乒,會(huì)先從 BitmapPool 里面找有沒有相應(yīng)大小的 Bitmap ,有則直接使用贰谣,沒有才會(huì)申請(qǐng)新的 Bitmap 娜搂;回收時(shí)迁霎,則會(huì)提交給 BitmapPool , 供下次使用。 這種方式極大的減少了 Bitmap 的申請(qǐng)和回收操作百宇,使得 GC 頻度降低了很多考廉。
- Glide使用了默認(rèn)使用了LruCache技術(shù)來(lái)處理內(nèi)存緩存。
- Glide 的內(nèi)存緩存有個(gè) active 的設(shè)計(jì) 從內(nèi)存緩存中取數(shù)據(jù)時(shí)携御,不像一般的實(shí)現(xiàn)用 get昌粤,而是用 remove ,再將這個(gè)緩存數(shù)據(jù)放到一個(gè) value 為軟引用的 activeResources map 中啄刹,并計(jì)數(shù)引用數(shù)涮坐,在圖片加載完成后進(jìn)行判斷,如果引用計(jì)數(shù)為空則回收掉誓军。
- 內(nèi)存緩存更小圖片 Glide 以 url袱讹、viewwidth、viewheight昵时、屏幕的分辨率等做為聯(lián)合 key捷雕,將處理后的圖片緩存在內(nèi)存緩存中,而不是原始圖片以節(jié)省大小壹甥。
案例分析
頁(yè)面可見才加載數(shù)據(jù)不可見回收救巷,緩存的管理交給第三方。
我們針對(duì)上述 (復(fù)雜的頁(yè)面結(jié)構(gòu)) 處理:始終保持緩存兩頁(yè)(因?yàn)轫?yè)面太多)句柠,看不見就去掉引用浦译,等待回收。當(dāng)然我沒有他們的源碼俄占,但是可以分析怎么做出效果管怠。要處理的問(wèn)題:
- 頁(yè)面可見才加載數(shù)據(jù)(網(wǎng)絡(luò)請(qǐng)求)。
- 頁(yè)面不可見缸榄,清理引用渤弛,等待Gc回收。
- Bitmap的緩存交給Glide甚带,Glide會(huì)自動(dòng)判斷清理她肯,但是我們要讓不用的Bitmap有且只有Glide持有它的引用,ImageView不能持有。這樣Glide在釋放Bitmap的時(shí)才能成功鹰贵,不然Bitmap發(fā)現(xiàn)ImageView持有引用是無(wú)法釋放的晴氨。
方案:
- 對(duì)于Viewpage+pageadapter,它始終會(huì)預(yù)先加載下一頁(yè)碉输,所以會(huì)走Fragment的onCreate系列生命周期籽前。一般我們初識(shí)化相關(guān)的工作都會(huì)在onCrate里處理,所以沒等滑到那一頁(yè)就加載了數(shù)據(jù)。所以這時(shí)我們要使用懶加載——懶加載就是在頁(yè)面可見才加載數(shù)據(jù)枝哄。核心是Fragment的setUserVisibleHint()方法肄梨。
- 在也面不可見時(shí)要清理引用,就要讓Fragment走onDestroy挠锥,如果我們使用Viewpage+FragmentPageAdapter众羡,由于FragmentPageAdapter的特性會(huì)緩存所有加載過(guò)的頁(yè)面,不會(huì)銷毀Fragment蓖租,不會(huì)走onDestroy系列生命周期粱侣!所以這里我們使用Viewpage+FragmentStatePageAdapter,特別適合多頁(yè)面的情況蓖宦,F(xiàn)ragmentStatePageAdapter會(huì)在頁(yè)面不可見時(shí)回收Fragment齐婴,然后調(diào)用onDestroy生命周期。
- 當(dāng)Glide發(fā)現(xiàn)內(nèi)存不夠用稠茂,需要清理一部分緩存時(shí)尔店,這時(shí)由于我們?cè)赾lear() 里清理了相關(guān)View的引用, 而且之前RecycleView會(huì)復(fù)用View,比如ImageView上一個(gè)加載Bitmap1主慰,在滑動(dòng)時(shí)復(fù)用有可能加載Bitmap2,這時(shí)Bitmap就只有Glide引用了鲫售。所以只有Glide持有無(wú)用Bitmap的引用共螺,這時(shí)就可以放心處理,你也不用擔(dān)心OOM了情竹。
其他
當(dāng)然如果你不放心藐不,你還可以把所有的ImageView都緩存(HashMap),然后在onDestroy里調(diào)用清理秦效。一可以根據(jù)View是否可見來(lái)判斷是否要清理引用雏蛮,例如:
public class ImageViewCash {
private static ImageViewCash instance;
public static ImageViewCash getInstance() {
if (null == instance)
instance = new ImageViewCash();
return instance;
}
/**
* 根據(jù)Context做為緩存的key
*/
private HashMap<Context, Set<ImageView>> ImageViewCash = new HashMap<>(100);
public HashMap<Context, Set<ImageView>> getImageViewCash() {
return ImageViewCash;
}
public void setImageViewCash(HashMap<Context, Set<ImageView>> imageViewCash) {
ImageViewCash = imageViewCash;
}
/**
* 清理引用
*
* @param context
*/
public synchronized void trimReference(Context context) {
Set<ImageView> sets = ImageViewCash.get(context);
for (ImageView imgView : sets) {
// 判斷ImageView是否可見
if (imgView.isShown() || imgView.getContext() == null) {
imgView.setImageBitmap(null);
}
}
}
}
為什么HashMap不行?
- HashMap是無(wú)序的阱州,也就是說(shuō)挑秉,迭代HashMap所得到的元素順序并不是它們最初放置到HashMap的順序。HashMap的這一缺點(diǎn)往往會(huì)造成諸多不便苔货,因?yàn)樵谟行﹫?chǎng)景中犀概,我們確需要用到一個(gè)可以保持插入順序的Map。
- LinkedHashMap夜惭。雖然LinkedHashMap增加了時(shí)間和空間上的開銷姻灶,但是它通過(guò)維護(hù)一個(gè)額外的雙向鏈表保證了迭代順序。特別地诈茧,該迭代順序可以是插入順序产喉,也可以是訪問(wèn)順序。因此,根據(jù)鏈表中元素的順序可以將LinkedHashMap分為:保持插入順序的LinkedHashMap 和 保持訪問(wèn)順序的LinkedHashMap曾沈,其中LinkedHashMap的默認(rèn)實(shí)現(xiàn)是按插入順序排序的这嚣。
正是由于LruCache采用了LinkedHashMap,才能是內(nèi)存相對(duì)穩(wěn)定晦譬。
Finalizer
FinalReference由JVM來(lái)實(shí)例化疤苹,VM會(huì)對(duì)那些實(shí)現(xiàn)了Object中finalize()方法的類實(shí)例化一個(gè)對(duì)應(yīng)的FinalReference。注意:實(shí)現(xiàn)的finalize方法體必須非空敛腌。
Finalizer是FinalReference的子類卧土,該類被final修飾,不可再被繼承像樊,JVM實(shí)際操作的是Finalizer尤莺。當(dāng)一個(gè)類滿足實(shí)例化FinalReference的條件時(shí),JVM會(huì)調(diào)用Finalizer.register()進(jìn)行注冊(cè)生棍。(PS:后續(xù)講的Finalizer其實(shí)也是在說(shuō)FinalReference颤霎。)
JVM在類加載的時(shí)候會(huì)遍歷當(dāng)前類的所有方法,包括父類的方法涂滴,只要有一個(gè)參數(shù)為空且返回void的非空f(shuō)inalize方法就認(rèn)為這個(gè)類在創(chuàng)建對(duì)象的時(shí)候需要進(jìn)行注冊(cè)友酱。
GC回收問(wèn)題
對(duì)象因?yàn)镕inalizer的引用而變成了一個(gè)臨時(shí)的強(qiáng)引用,即使沒有其他的強(qiáng)引用柔纵,還是無(wú)法立即被回收缔杉;
對(duì)象至少經(jīng)歷兩次GC才能被回收,因?yàn)橹挥性贔inalizerThread執(zhí)行完了f對(duì)象的finalize方法的情況下才有可能被下次GC回收搁料,而有可能期間已經(jīng)經(jīng)歷過(guò)多次GC了或详,但是一直還沒執(zhí)行對(duì)象的finalize方法;
CPU資源比較稀缺的情況下FinalizerThread線程有可能因?yàn)閮?yōu)先級(jí)比較低而延遲執(zhí)行對(duì)象的finalize方法郭计;
因?yàn)閷?duì)象的finalize方法遲遲沒有執(zhí)行霸琴,有可能會(huì)導(dǎo)致大部分f對(duì)象進(jìn)入到old分代,此時(shí)容易引發(fā)old分代的GC昭伸,甚至Full GC梧乘,GC暫停時(shí)間明顯變長(zhǎng),甚至導(dǎo)致OOM勋乾;
對(duì)象的finalize方法被調(diào)用后宋下,這個(gè)對(duì)象其實(shí)還并沒有被回收,雖然可能在不久的將來(lái)會(huì)被回收辑莫。
?在我們寫代碼的時(shí)候学歧,也要加強(qiáng)Finalizer對(duì)象的理解和警覺,了解哪些系統(tǒng)類是有Finalizer對(duì)象各吨,并了解Finalizer對(duì)內(nèi)存枝笨,性能和穩(wěn)定性所帶來(lái)的影響袁铐。特別是我們自己寫類的時(shí)候,要盡量避免重寫finalize方法横浑,即使重寫了也要注意該方法的實(shí)現(xiàn)剔桨,不要有耗時(shí)操作,也盡量不要拋出異常等徙融。[具體參考]
其他內(nèi)存問(wèn)題
- webview內(nèi)存泄漏
- 個(gè)別手機(jī)輸入法內(nèi)存泄漏(華為手機(jī))
- …(Google)
內(nèi)存分析工具
參考
http://blog.qiji.tech/archives/10029
http://childe.net.cn/2017/04/01/JDK%E6%BA%90%E7%A0%81-FinalReference/
http://wiki.jikexueyuan.com/project/java-special-topic/platorm-memory.html
http://www.reibang.com/p/63aead89f3b9