Android內(nèi)存的全面分析-讓你吃透

我希望通過(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)唯一的元素又活。

圖解:

Java內(nèi)存運(yùn)行區(qū)域說(shuō)明.png

一個(gè)程序運(yùn)行時(shí),內(nèi)存的整個(gè)過(guò)程:
內(nèi)存加載過(guò)程.jpg

注意:

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)如下:

B27D06AA-9111-42AC-8F3E-745D954FD974.png

我們一般常用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)刽锤。


頁(yè)面結(jié)構(gòu).jpg

首先對(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í)非常少:

BE88E2B1-1AA3-4FD9-924E-F57724BEA66D.png

其實(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末洒缀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子欺冀,更是在濱河造成了極大的恐慌树绩,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隐轩,死亡現(xiàn)場(chǎng)離奇詭異饺饭,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)职车,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門瘫俊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人悴灵,你說(shuō)我怎么就攤上這事扛芽。” “怎么了积瞒?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵胸哥,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我赡鲜,道長(zhǎng),這世上最難降的妖魔是什么庐船? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任银酬,我火速辦了婚禮,結(jié)果婚禮上筐钟,老公的妹妹穿的比我還像新娘揩瞪。我一直安慰自己,他們只是感情好篓冲,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布李破。 她就那樣靜靜地躺著,像睡著了一般壹将。 火紅的嫁衣襯著肌膚如雪嗤攻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天诽俯,我揣著相機(jī)與錄音妇菱,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛闯团,可吹牛的內(nèi)容都是我干的辛臊。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼房交,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彻舰!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起候味,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤刃唤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后负溪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體透揣,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年川抡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辐真。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡崖堤,死狀恐怖侍咱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情密幔,我是刑警寧澤楔脯,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站胯甩,受9級(jí)特大地震影響昧廷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜偎箫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一木柬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧淹办,春花似錦眉枕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至副硅,卻和暖如春姥宝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恐疲。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工伶授, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留断序,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓糜烹,卻偏偏與公主長(zhǎng)得像违诗,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子疮蹦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容