Android內(nèi)存優(yōu)化之OOM(轉(zhuǎn))

本文轉(zhuǎn)載來源 http://www.csdn.net/article/2015-09-18/2825737/1

(一)Android的內(nèi)存管理機(jī)制
Google在Android的官網(wǎng)上有這樣一篇文章,初步介紹了Android是如何管理應(yīng)用的進(jìn)程與內(nèi)存分配:http://developer.android.com/training/articles/memory.html违霞。 Android系統(tǒng)的Dalvik虛擬機(jī)扮演了常規(guī)的內(nèi)存垃圾自動(dòng)回收的角色萄窜,Android系統(tǒng)沒有為內(nèi)存提供交換區(qū),它使用 pagingmemory-mapping(mmapping)的機(jī)制來管理內(nèi)存,下面簡(jiǎn)要概述一些Android系統(tǒng)中重要的內(nèi)存管理基礎(chǔ)概念驶沼。
1)共享內(nèi)存
Android系統(tǒng)通過下面幾種方式來實(shí)現(xiàn)共享內(nèi)存:

Android應(yīng)用的進(jìn)程都是從一個(gè)叫做Zygote的進(jìn)程fork出來的爆办。Zygote進(jìn)程在系統(tǒng)啟動(dòng),并載入通用的framework的代碼與資源之后開始啟動(dòng)泛烙。為了啟動(dòng)一個(gè)新的程序進(jìn)程理卑,系統(tǒng)會(huì)fork Zygote進(jìn)程生成一個(gè)新的進(jìn)程,然后在新的進(jìn)程中加載并運(yùn)行應(yīng)用程序的代碼蔽氨。這就使得大多數(shù)的RAM pages被用來分配給framework的代碼藐唠,同時(shí)促使RAM資源能夠在應(yīng)用的所有進(jìn)程之間進(jìn)行共享霞溪。
大多數(shù)static的數(shù)據(jù)被mmapped到一個(gè)進(jìn)程中。這不僅僅讓同樣的數(shù)據(jù)能夠在進(jìn)程間進(jìn)行共享中捆,而且使得它能夠在需要的時(shí)候被paged out鸯匹。常見的static數(shù)據(jù)包括Dalvik Code、app resources泄伪、so文件等殴蓬。
大多數(shù)情況下,Android通過顯式的分配共享內(nèi)存區(qū)域(例如ashmem或gralloc)來實(shí)現(xiàn)動(dòng)態(tài)RAM區(qū)域能夠在不同進(jìn)程之間進(jìn)行共享的機(jī)制蟋滴。比如染厅,Window Surface在App與Screen Compositor之間使用共享的內(nèi)存,Cursor Buffers在Content Provider與Clients之間共享內(nèi)存津函。

2)分配與回收內(nèi)存

每一個(gè)進(jìn)程的Dalvik Heap都反映了使用內(nèi)存的占用范圍肖粮。這就是通常邏輯意義上提到的Dalvik Heap Size,它可以隨著需要進(jìn)行增長(zhǎng)尔苦,但是增長(zhǎng)行為會(huì)有一個(gè)系統(tǒng)為它設(shè)定上限涩馆。
邏輯上講的Heap Size和實(shí)際物理意義上使用的內(nèi)存大小是不對(duì)等的,Proportional Set Size(PSS)記錄了應(yīng)用程序自身占用以及與其他進(jìn)程進(jìn)行共享的內(nèi)存允坚。
Android系統(tǒng)并不會(huì)對(duì)Heap中空閑內(nèi)存區(qū)域做碎片整理魂那。系統(tǒng)僅僅會(huì)在新的內(nèi)存分配之前判斷Heap的尾端剩余空間是否足夠,如果空間不夠會(huì)觸發(fā)GC操作稠项,從而騰出更多空閑的內(nèi)存空間涯雅。在Android的高級(jí)系統(tǒng)版本里面針對(duì)Heap空間有一個(gè)Generational Heap Memory的模型,最近分配的對(duì)象會(huì)存放在Young Generation區(qū)域展运。當(dāng)這個(gè)對(duì)象在該區(qū)域停留的時(shí)間達(dá)到一定程度活逆,它會(huì)被移動(dòng)到Old Generation,最后累積一定時(shí)間再移動(dòng)到Permanent Generation區(qū)域拗胜。系統(tǒng)會(huì)根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的GC操作蔗候。例如,剛分配到Y(jié)oung Generation區(qū)域的對(duì)象通常更容易被銷毀回收挤土,同時(shí)在Young Generation區(qū)域的GC操作速度會(huì)比Old Generation區(qū)域的GC操作速度更快(如圖1所示)琴庵。


圖1 根據(jù)不同內(nèi)存數(shù)據(jù)類型執(zhí)行不同GC操作
每一個(gè)Generation的內(nèi)存區(qū)域都有固定的大小。隨著新的對(duì)象陸續(xù)被分配到此區(qū)域仰美,當(dāng)對(duì)象總的大小臨近這一級(jí)別內(nèi)存區(qū)域的閥值時(shí)迷殿,會(huì)觸發(fā)GC操作,以便騰出空間來存放其他新的對(duì)象(如圖2所示)咖杂。



圖2 對(duì)象值臨近閥值觸發(fā)GC操作
通常情況下庆寺,GC發(fā)生的時(shí)候,所有的線程都是會(huì)被暫停的诉字。執(zhí)行GC所占用的時(shí)間和它發(fā)生在哪一個(gè)Generation也有關(guān)系懦尝,Young Generation中的每次GC操作時(shí)間是最短的知纷,Old Generation其次,Permanent Generation最長(zhǎng)陵霉。執(zhí)行時(shí)間的長(zhǎng)短也和當(dāng)前Generation中的對(duì)象數(shù)量有關(guān)琅轧,遍歷樹結(jié)構(gòu)查找20000個(gè)對(duì)象比起遍歷50個(gè)對(duì)象自然是要慢很多的。
3)限制應(yīng)用的內(nèi)存

為了整個(gè)系統(tǒng)的內(nèi)存控制需要踊挠,Android系統(tǒng)為每一個(gè)應(yīng)用程序都設(shè)置一個(gè)硬性的Dalvik Heap Size最大限制閾值乍桂,這個(gè)閾值在不同的設(shè)備上會(huì)因?yàn)镽AM大小不同而各有差異。如果你的應(yīng)用占用內(nèi)存空間已經(jīng)接近這個(gè)閾值效床,此時(shí)再嘗試分配內(nèi)存的話睹酌,很容易引發(fā)OutOfMemoryError錯(cuò)誤。
ActivityManager.getMemoryClass()可以用來查詢當(dāng)前應(yīng)用的Heap Size閾值剩檀,這個(gè)方法會(huì)返回一個(gè)整數(shù)憋沿,表明應(yīng)用的Heap Size閾值是多少M(fèi)B(Megabates)。

4)應(yīng)用切換操作

Android系統(tǒng)并不會(huì)在用戶切換應(yīng)用的時(shí)候執(zhí)行交換內(nèi)存操作沪猴。Android會(huì)把那些不包含F(xiàn)oreground組件的應(yīng)用進(jìn)程放到LRU Cache中辐啄。例如,當(dāng)用戶開始啟動(dòng)一個(gè)應(yīng)用時(shí)字币,系統(tǒng)會(huì)為它創(chuàng)建一個(gè)進(jìn)程则披。但是當(dāng)用戶離開此應(yīng)用共缕,進(jìn)程不會(huì)立即被銷毀洗出,而是被放到系統(tǒng)的Cache當(dāng)中。如果用戶后來再切換回到這個(gè)應(yīng)用图谷,此進(jìn)程就能夠被馬上完整地恢復(fù)翩活,從而實(shí)現(xiàn)應(yīng)用的快速切換。
如果你的應(yīng)用中有一個(gè)被緩存的進(jìn)程便贵,這個(gè)進(jìn)程會(huì)占用一定的內(nèi)存空間菠镇,它會(huì)對(duì)系統(tǒng)的整體性能有影響。因此承璃,當(dāng)系統(tǒng)開始進(jìn)入Low Memory的狀態(tài)時(shí)利耍,它會(huì)由系統(tǒng)根據(jù)LRU的規(guī)則與應(yīng)用的優(yōu)先級(jí),內(nèi)存占用情況以及其他因素的影響綜合評(píng)估之后決定是否被殺掉盔粹。
對(duì)于那些非foreground的進(jìn)程隘梨,Android系統(tǒng)是如何判斷Kill掉哪些進(jìn)程的問題,請(qǐng)參考Processes and Threads舷嗡。

(二)OOM(Out Of Memory)
前面我們提到過使用getMemoryClass()的方法可以得到Dalvik Heap的閾值轴猎。簡(jiǎn)要地獲取某個(gè)應(yīng)用的內(nèi)存占用情況可以參考下面的示例(更多內(nèi)存查看的知識(shí),可以參考Google官方教程: Investigating Your RAM Usage
1)查看內(nèi)存使用情況
通過命令行查看內(nèi)存詳細(xì)占用情況进萄,如圖3所示捻脖。


圖3 命令行查看內(nèi)存詳細(xì)占用情況
通過Android Studio的Memory Monitor查看內(nèi)存中Dalvik Heap的實(shí)時(shí)變化锐峭,如圖4、5可婶、6所示沿癞。

圖4 Memory Monitor查看內(nèi)存中Dalvik Heap的實(shí)時(shí)變化(一)

圖5 Memory Monitor查看內(nèi)存中Dalvik Heap的實(shí)時(shí)變化(二)

圖6 Memory Monitor查看內(nèi)存中Dalvik Heap的實(shí)時(shí)變化(三)
2)發(fā)生OOM的條件
關(guān)于Native Heap、Dalvik Heap矛渴、PSS等內(nèi)存管理機(jī)制比較復(fù)雜抛寝,這里就不展開詳細(xì)描述。簡(jiǎn)單的說曙旭,通過不同的內(nèi)存分配方式(malloc/mmap/JNIEnv/etc)對(duì)不同的對(duì)象(Bitmap/etc)進(jìn)行操作盗舰,會(huì)因?yàn)锳ndroid系統(tǒng)版本的差異而產(chǎn)生不同的行為,對(duì)Native Heap與Dalvik Heap以及OOM的判斷條件都會(huì)有所影響桂躏。在2.x的系統(tǒng)上钻趋,我們常常可以看到Heap Size的total值剂习,明顯超過了通過getMemoryClass()獲取到的閾值而不會(huì)發(fā)生OOM的情況蛮位。那么,針對(duì)2.x與4.x的Android系統(tǒng)鳞绕,到底如何判斷會(huì)發(fā)生OOM呢失仁?

Android 2.x系統(tǒng)GC LOG中的dalvik allocated + external allocated + 新分配的大小 >= getMemoryClass()值的時(shí)候就會(huì)發(fā)生OOM。 例如们何,假設(shè)有這么一段Dalvik輸出的GC LOG:GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms萄焦,那么32586+8989+(新分配23975)=65550>64M時(shí),就會(huì)發(fā)生OOM冤竹。
Android 4.x的系統(tǒng)廢除了external的計(jì)數(shù)器拂封,類似Bitmap的分配改到Dalvik的Java Heap中申請(qǐng)。只要allocated + 新分配的內(nèi)存 >= getMemoryClass()的時(shí)候就會(huì)發(fā)生OOM鹦蠕,如圖7所示(注:雖然圖示演示的是ART運(yùn)行環(huán)境冒签,但是統(tǒng)計(jì)規(guī)則還是和Dalvik保持一致)。

Paste_Image.png

圖7

(三)如何避免OOM總結(jié)
前面介紹了一些基礎(chǔ)的內(nèi)存管理機(jī)制以及OOM的基礎(chǔ)知識(shí)钟病,那么在實(shí)踐操作當(dāng)中萧恕,有哪些指導(dǎo)性的規(guī)則可以參考呢?歸納下來肠阱,可以從四個(gè)方面著手票唆,首先是減小對(duì)象的內(nèi)存占用,其次是內(nèi)存對(duì)象的重復(fù)利用辖所,然后是避免對(duì)象的內(nèi)存泄露惰说,最后是內(nèi)存使用策略優(yōu)化。
減小對(duì)象的內(nèi)存占用
避免OOM的第一步就是要盡量減少新分配出來的對(duì)象占用內(nèi)存的大小缘回,盡量使用更加輕量的對(duì)象吆视。
1)使用更加輕量的數(shù)據(jù)結(jié)構(gòu)
例如典挑,我們可以考慮使用ArrayMap/SparseArray而不是HashMap等傳統(tǒng)數(shù)據(jù)結(jié)構(gòu)。圖8演示了HashMap的簡(jiǎn)要工作原理啦吧,相比起Android專門為移動(dòng)操作系統(tǒng)編寫的ArrayMap容器您觉,在大多數(shù)情況下,都顯示效率低下授滓,更占內(nèi)存琳水。通常的HashMap的實(shí)現(xiàn)方式更加消耗內(nèi)存,因?yàn)樗枰粋€(gè)額外的實(shí)例對(duì)象來記錄Mapping操作般堆。另外在孝,SparseArray更加高效,在于他們避免了對(duì)key與value的自動(dòng)裝箱(autoboxing)淮摔,并且避免了裝箱后的解箱私沮。


圖8 HashMap簡(jiǎn)要工作原理
關(guān)于更多ArrayMap/SparseArray的討論,請(qǐng)參考《 Android性能優(yōu)化典范(三)》的前三個(gè)段落和橙。
2)避免在Android里面使用Enum
Android官方培訓(xùn)課程提到過“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”仔燕,具體原理請(qǐng)參考《Android性能優(yōu)化典范(三)》,所以請(qǐng)避免在Android里面使用到枚舉魔招。
3)減小Bitmap對(duì)象的內(nèi)存占用
Bitmap是一個(gè)極容易消耗內(nèi)存的大胖子晰搀,減小創(chuàng)建出來的Bitmap的內(nèi)存占用可謂是重中之重,通常來說有以下2個(gè)措施:

inSampleSize:縮放比例办斑,在把圖片載入內(nèi)存之前外恕,我們需要先計(jì)算出一個(gè)合適的縮放比例,避免不必要的大圖載入俄周。
decode format:解碼格式吁讨,選擇ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差異峦朗。

4)使用更小的圖片
在涉及給到資源圖片時(shí),我們需要特別留意這張圖片是否存在可以壓縮的空間排龄,是否可以使用更小的圖片波势。盡量使用更小的圖片不僅可以減少內(nèi)存的使用,還能避免出現(xiàn)大量的InflationException橄维。假設(shè)有一張很大的圖片被XML文件直接引用尺铣,很有可能在初始化視圖時(shí)會(huì)因?yàn)閮?nèi)存不足而發(fā)生InflationException,這個(gè)問題的根本原因其實(shí)是發(fā)生了OOM争舞。
內(nèi)存對(duì)象的重復(fù)利用
大多數(shù)對(duì)象的復(fù)用凛忿,最終實(shí)施的方案都是利用對(duì)象池技術(shù),要么是在編寫代碼時(shí)顯式地在程序里創(chuàng)建對(duì)象池竞川,然后處理好復(fù)用的實(shí)現(xiàn)邏輯店溢。要么就是利用系統(tǒng)框架既有的某些復(fù)用特性叁熔,減少對(duì)象的重復(fù)創(chuàng)建,從而降低內(nèi)存的分配與回收(如圖9所示)床牧。



圖9 對(duì)象池技術(shù)
在Android上面最常用的一個(gè)緩存算法是LRU(Least Recently Use)荣回,簡(jiǎn)要操作原理如圖10所示。



圖10 LRU簡(jiǎn)要操作原理
1)復(fù)用系統(tǒng)自帶的資源
Android系統(tǒng)本身內(nèi)置了很多的資源戈咳,比如字符串心软、顏色、圖片著蛙、動(dòng)畫删铃、樣式以及簡(jiǎn)單布局等,這些資源都可以在應(yīng)用程序中直接引用踏堡。這樣做不僅能減少應(yīng)用程序的自身負(fù)重泳姐,減小APK的大小,還可以在一定程度上減少內(nèi)存的開銷暂吉,復(fù)用性更好胖秒。但是也有必要留意Android系統(tǒng)的版本差異性,對(duì)那些不同系統(tǒng)版本上表現(xiàn)存在很大差異慕的、不符合需求的情況阎肝,還是需要應(yīng)用程序自身內(nèi)置進(jìn)去。
2)注意在ListView/GridView等出現(xiàn)大量重復(fù)子組件的視圖里對(duì)ConvertView的復(fù)用肮街,如圖11所示风题。

圖11
3)Bitmap對(duì)象的復(fù)用
在ListView與GridView等顯示大量圖片的控件里,需要使用LRU的機(jī)制來緩存處理好的Bitmap嫉父,如圖12所示沛硅。



圖12

利用inBitmap的高級(jí)特性提高Android系統(tǒng)在Bitmap分配與釋放執(zhí)行效率(注:3.0以及4.4以后存在一些使用限制上的差異)。使用inBitmap屬性可以告知Bitmap解碼器去嘗試使用已經(jīng)存在的內(nèi)存區(qū)域绕辖,新解碼的Bitmap會(huì)嘗試去使用之前那張Bitmap在Heap中所占據(jù)的pixel data內(nèi)存區(qū)域摇肌,而不是去問內(nèi)存重新申請(qǐng)一塊區(qū)域來存放Bitmap。利用這種特性仪际,即使是上千張的圖片围小,也只會(huì)僅僅只需要占用屏幕所能夠顯示的圖片數(shù)量的內(nèi)存大小,如圖13所示树碱。


圖13 利用inBitmap的高級(jí)特性提高Android在Bitmap分配與釋放執(zhí)行效率
使用inBitmap需要注意幾個(gè)限制條件:

在SDK 11 -> 18之間肯适,重用的Bitmap大小必須是一致的。例如給inBitmap賦值的圖片大小為100-100成榜,那么新申請(qǐng)的Bitmap必須也為100-100才能夠被重用怀读。從SDK 19開始佑女,新申請(qǐng)的Bitmap大小必須小于或者等于已經(jīng)賦值過的Bitmap大小涛浙。
新申請(qǐng)的Bitmap與舊的Bitmap必須有相同的解碼格式。例如大家都是8888的樱溉,如果前面的Bitmap是8888,那么就不能支持4444與565格式的Bitmap了额港。我們可以創(chuàng)建一個(gè)包含多種典型可重用Bitmap的對(duì)象池饺窿,這樣后續(xù)的Bitmap創(chuàng)建都能夠找到合適的“模板”去進(jìn)行重用,如圖14所示移斩。


圖14
另外肚医,在2.x的系統(tǒng)上,盡管Bitmap是分配在Native層向瓷,但還是無法避免被計(jì)算到OOM的引用計(jì)數(shù)器里肠套。這里提示一下,不少應(yīng)用會(huì)通過反射vBitmapFactory.Options里面的inNativeAlloc來達(dá)到擴(kuò)大使用內(nèi)存的目的猖任,但是如果大家都這么做你稚,對(duì)系統(tǒng)整體會(huì)造成一定的負(fù)面影響,建議謹(jǐn)慎采納朱躺。
4)避免在onDraw方法里面執(zhí)行對(duì)象的創(chuàng)建
類似onDraw等頻繁調(diào)用的方法刁赖,一定需要注意避免在這里做創(chuàng)建對(duì)象的操作,因?yàn)樗麜?huì)迅速增加內(nèi)存的使用长搀,而且很容易引起頻繁的gc宇弛,甚至是內(nèi)存抖動(dòng)。
5)StringBuilder
在有些時(shí)候源请,代碼中會(huì)需要使用到大量的字符串拼接的操作枪芒,這種時(shí)候有必要考慮使用StringBuilder來替代頻繁的“+”。

避免對(duì)象的內(nèi)存泄露
內(nèi)存對(duì)象的泄漏谁尸,會(huì)導(dǎo)致一些不再使用的對(duì)象無法及時(shí)釋放舅踪,這樣一方面占用了寶貴的內(nèi)存空間,很容易導(dǎo)致后續(xù)需要分配內(nèi)存的時(shí)候良蛮,空閑空間不足而出現(xiàn)OOM抽碌。顯然,這還使得每級(jí)Generation的內(nèi)存區(qū)域可用空間變小背镇,GC就會(huì)更容易被觸發(fā)咬展,容易出現(xiàn)內(nèi)存抖動(dòng),從而引起性能問題(如圖15所示)瞒斩。


圖15
最新的LeakCanary開源控件,可以很好的幫助我們發(fā)現(xiàn)內(nèi)存泄露的情況涮总,更多關(guān)于LeakCanary的介紹胸囱,請(qǐng)看 這里中文使用說明)。另外也可以使用傳統(tǒng)的MAT工具查找內(nèi)存泄露瀑梗,請(qǐng)參考 這里便捷的中文資料)烹笔。
1)注意Activity的泄漏
通常來說裳扯,Activity的泄漏是內(nèi)存泄漏里面最嚴(yán)重的問題,它占用的內(nèi)存多谤职,影響面廣饰豺,我們需要特別注意以下兩種情況導(dǎo)致的Activity泄漏:

內(nèi)部類引用導(dǎo)致Activity的泄漏

最典型的場(chǎng)景是Handler導(dǎo)致的Activity泄漏,如果Handler中有延遲的任務(wù)或者是等待執(zhí)行的任務(wù)隊(duì)列過長(zhǎng)允蜈,都有可能因?yàn)镠andler繼續(xù)執(zhí)行而導(dǎo)致Activity發(fā)生泄漏冤吨。此時(shí)的引用關(guān)系鏈?zhǔn)荓ooper -> MessageQueue -> Message -> Handler -> Activity。為了解決這個(gè)問題饶套,可以在UI退出之前漩蟆,執(zhí)行remove Handler消息隊(duì)列中的消息與runnable對(duì)象〖寺或者是使用Static + WeakReference的方式來達(dá)到斷開Handler與Activity之間存在引用關(guān)系的目的怠李。

Activity Context被傳遞到其他實(shí)例中,這可能導(dǎo)致自身被引用而發(fā)生泄漏蛤克。

內(nèi)部類引起的泄漏不僅僅會(huì)發(fā)生在Activity上捺癞,其他任何內(nèi)部類出現(xiàn)的地方,都需要特別留意构挤!我們可以考慮盡量使用static類型的內(nèi)部類髓介,同時(shí)使用WeakReference的機(jī)制來避免因?yàn)榛ハ嘁枚霈F(xiàn)的泄露。
2)考慮使用Application Context而不是Activity Context
對(duì)于大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context)儿倒,我們都可以考慮使用Application Context而不是Activity的Context版保,這樣可以避免不經(jīng)意的Activity泄露。
3)注意臨時(shí)Bitmap對(duì)象的及時(shí)回收
雖然在大多數(shù)情況下夫否,我們會(huì)對(duì)Bitmap增加緩存機(jī)制彻犁,但是在某些時(shí)候,部分Bitmap是需要及時(shí)回收的凰慈。例如臨時(shí)創(chuàng)建的某個(gè)相對(duì)比較大的bitmap對(duì)象汞幢,在經(jīng)過變換得到新的bitmap對(duì)象之后,應(yīng)該盡快回收原始的bitmap微谓,這樣能夠更快釋放原始bitmap所占用的空間森篷。
需要特別留意的是Bitmap類里面提供的createBitmap()方法,如圖16所示:


圖16 createBitmap()方法
這個(gè)函數(shù)返回的bitmap有可能和source bitmap是同一個(gè)豺型,在回收的時(shí)候仲智,需要特別檢查source bitmap與return bitmap的引用是否相同,只有在不等的情況下姻氨,才能夠執(zhí)行source bitmap的recycle方法钓辆。
4)注意監(jiān)聽器的注銷
在Android程序里面存在很多需要register與unregister的監(jiān)聽器,我們需要確保在合適的時(shí)候及時(shí)unregister那些監(jiān)聽器。自己手動(dòng)add的listener前联,需要記得及時(shí)remove這個(gè)listener功戚。
5)注意緩存容器中的對(duì)象泄漏
有時(shí)候,我們?yōu)榱颂岣邔?duì)象的復(fù)用性把某些對(duì)象放到緩存容器中似嗤,可是如果這些對(duì)象沒有及時(shí)從容器中清除啸臀,也是有可能導(dǎo)致內(nèi)存泄漏的。例如烁落,針對(duì)2.3的系統(tǒng)乘粒,如果把drawable添加到緩存容器顽馋,因?yàn)閐rawable與View的強(qiáng)應(yīng)用,很容易導(dǎo)致activity發(fā)生泄漏寸谜。而從4.0開始,就不存在這個(gè)問題他爸。解決這個(gè)問題果善,需要對(duì)2.3系統(tǒng)上的緩存drawable做特殊封裝,處理引用解綁的問題讨跟,避免泄漏的情況鄙煤。
6)注意WebView的泄漏
Android中的WebView存在很大的兼容性問題晾匠,不僅僅是Android系統(tǒng)版本的不同對(duì)WebView產(chǎn)生很大的差異,另外不同的廠商出貨的ROM里面WebView也存在著很大的差異梯刚。更嚴(yán)重的是標(biāo)準(zhǔn)的WebView存在內(nèi)存泄露的問題凉馆,請(qǐng)看 這里。所以通常根治這個(gè)問題的辦法是為WebView開啟另外一個(gè)進(jìn)程亡资,通過AIDL與主進(jìn)程進(jìn)行通信澜共,WebView所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時(shí)機(jī)進(jìn)行銷毀,從而達(dá)到內(nèi)存的完整釋放锥腻。
7)注意Cursor對(duì)象是否及時(shí)關(guān)閉
在程序中我們經(jīng)常會(huì)進(jìn)行查詢數(shù)據(jù)庫的操作嗦董,但時(shí)常會(huì)存在不小心使用Cursor之后沒有及時(shí)關(guān)閉的情況。這些Cursor的泄露瘦黑,反復(fù)多次出現(xiàn)的話會(huì)對(duì)內(nèi)存管理產(chǎn)生很大的負(fù)面影響展懈,我們需要謹(jǐn)記對(duì)Cursor對(duì)象的及時(shí)關(guān)閉销睁。
內(nèi)存使用策略優(yōu)化
1)謹(jǐn)慎使用large heap
正如前面提到的供璧,Android設(shè)備根據(jù)硬件與軟件的設(shè)置差異而存在不同大小的內(nèi)存空間存崖,他們?yōu)閼?yīng)用程序設(shè)置了不同大小的Heap限制閾值。你可以通過調(diào)用getMemoryClass()來獲取應(yīng)用的可用Heap大小睡毒。在一些特殊的情景下来惧,你可以通過在manifest的application標(biāo)簽下添加largeHeap=true的屬性來為應(yīng)用聲明一個(gè)更大的heap空間。然后演顾,你可以通過getLargeMemoryClass()來獲取到這個(gè)更大的heap size閾值供搀。然而,聲明得到更大Heap閾值的本意是為了一小部分會(huì)消耗大量RAM的應(yīng)用(例如一個(gè)大圖片的編輯應(yīng)用)钠至。不要輕易的因?yàn)槟阈枰褂酶嗟膬?nèi)存而去請(qǐng)求一個(gè)大的Heap Size。只有當(dāng)你清楚的知道哪里會(huì)使用大量的內(nèi)存并且知道為什么這些內(nèi)存必須被保留時(shí)才去使用large heap屿脐。因此請(qǐng)謹(jǐn)慎使用large heap屬性的诵。使用額外的內(nèi)存空間會(huì)影響系統(tǒng)整體的用戶體驗(yàn)西疤,并且會(huì)使得每次gc的運(yùn)行時(shí)間更長(zhǎng)代赁。在任務(wù)切換時(shí)芭碍,系統(tǒng)的性能會(huì)大打折扣豁跑。另外, large heap并不一定能夠獲取到更大的heap艇拍。在某些有嚴(yán)格限制的機(jī)器上卸夕,large heap的大小和通常的heap size是一樣的快集。因此即使你申請(qǐng)了large heap个初,你還是應(yīng)該通過執(zhí)行g(shù)etMemoryClass()來檢查實(shí)際獲取到的heap大小楣嘁。
2)綜合考慮設(shè)備內(nèi)存閾值與其他因素設(shè)計(jì)合適的緩存大小
例如逐虚,在設(shè)計(jì)ListView或者GridView的Bitmap LRU緩存的時(shí)候叭爱,需要考慮的點(diǎn)有:

應(yīng)用程序剩下了多少可用的內(nèi)存空間?
有多少圖片會(huì)被一次呈現(xiàn)到屏幕上买雾?有多少圖片需要事先緩存好以便快速滑動(dòng)時(shí)能夠立即顯示到屏幕?
設(shè)備的屏幕大小與密度是多少? 一個(gè)xhdpi的設(shè)備會(huì)比hdpi需要一個(gè)更大的Cache來hold住同樣數(shù)量的圖片睦尽。
不同的頁面針對(duì)Bitmap的設(shè)計(jì)的尺寸與配置是什么山害,大概會(huì)花費(fèi)多少內(nèi)存浪慌?
頁面圖片被訪問的頻率权纤?是否存在其中的一部分比其他的圖片具有更高的訪問頻繁汹想?如果是古掏,也許你想要保存那些最常訪問的到內(nèi)存中丧枪,或者為不同組別的位圖(按訪問頻率分組)設(shè)置多個(gè)LruCache容器拧烦。

3)onLowMemory()與onTrimMemory()
Android用戶可以隨意在不同的應(yīng)用之間進(jìn)行快速切換屎篱。為了讓background的應(yīng)用能夠迅速的切換到forground,每一個(gè)background的應(yīng)用都會(huì)占用一定的內(nèi)存践付。Android系統(tǒng)會(huì)根據(jù)當(dāng)前的系統(tǒng)的內(nèi)存使用情況永高,決定回收部分background的應(yīng)用內(nèi)存命爬。如果background的應(yīng)用從暫停狀態(tài)直接被恢復(fù)到forground,能夠獲得較快的恢復(fù)體驗(yàn)嗜价,如果background應(yīng)用是從Kill的狀態(tài)進(jìn)行恢復(fù)家淤,相比之下就顯得稍微有點(diǎn)慢絮重,如圖17所示青伤。



圖17 從Kill狀態(tài)進(jìn)行恢復(fù)體驗(yàn)更慢

onLowMemory():Android系統(tǒng)提供了一些回調(diào)來通知當(dāng)前應(yīng)用的內(nèi)存使用情況,通常來說痴施,當(dāng)所有的background應(yīng)用都被kill掉的時(shí)候,forground應(yīng)用會(huì)收到onLowMemory()的回調(diào)芬探。在這種情況下偷仿,需要盡快釋放當(dāng)前應(yīng)用的非必須的內(nèi)存資源,從而確保系統(tǒng)能夠繼續(xù)穩(wěn)定運(yùn)行羡玛。
onTrimMemory(int):Android系統(tǒng)從4.0開始還提供了onTrimMemory()的回調(diào)薄榛,當(dāng)系統(tǒng)內(nèi)存達(dá)到某些條件的時(shí)候敞恋,所有正在運(yùn)行的應(yīng)用都會(huì)收到這個(gè)回調(diào)硬猫,同時(shí)在這個(gè)回調(diào)里面會(huì)傳遞以下的參數(shù)浦徊,代表不同的內(nèi)存使用情況,收到onTrimMemory()回調(diào)的時(shí)候呢岗,需要根據(jù)傳遞的參數(shù)類型進(jìn)行判斷悉尾,合理的選擇釋放自身的一些內(nèi)存占用构眯,一方面可以提高系統(tǒng)的整體運(yùn)行流暢度惫霸,另外也可以避免自己被系統(tǒng)判斷為優(yōu)先需要?dú)⒌舻膽?yīng)用壹店。
TRIM_MEMORY_UI_HIDDEN:你的應(yīng)用程序的所有UI界面被隱藏了硅卢,即用戶點(diǎn)擊了Home鍵或者Back鍵退出應(yīng)用将塑,導(dǎo)致應(yīng)用的UI界面完全不可見弊予。這個(gè)時(shí)候應(yīng)該釋放一些不可見的時(shí)候非必須的資源

當(dāng)程序正在前臺(tái)運(yùn)行的時(shí)候,可能會(huì)接收到從onTrimMemory()中返回的下面的值之一:

TRIM_MEMORY_RUNNING_MODERATE:你的應(yīng)用正在運(yùn)行并且不會(huì)被列為可殺死的责鳍。但是設(shè)備此時(shí)正運(yùn)行于低內(nèi)存狀態(tài)下历葛,系統(tǒng)開始觸發(fā)殺死LRU Cache中的Process的機(jī)制恤溶。
TRIM_MEMORY_RUNNING_LOW:你的應(yīng)用正在運(yùn)行且沒有被列為可殺死的。但是設(shè)備正運(yùn)行于更低內(nèi)存的狀態(tài)下帐姻,你應(yīng)該釋放不用的資源用來提升系統(tǒng)性能饥瓷。
TRIM_MEMORY_RUNNING_CRITICAL:你的應(yīng)用仍在運(yùn)行呢铆,但是系統(tǒng)已經(jīng)把LRU Cache中的大多數(shù)進(jìn)程都已經(jīng)殺死棺克,因此你應(yīng)該立即釋放所有非必須的資源逆航。如果系統(tǒng)不能回收到足夠的RAM數(shù)量因俐,系統(tǒng)將會(huì)清除所有的LRU緩存中的進(jìn)程撑帖,并且開始?xì)⑺滥切┲氨徽J(rèn)為不應(yīng)該殺死的進(jìn)程胡嘿,例如那個(gè)包含了一個(gè)運(yùn)行態(tài)Service的進(jìn)程衷敌。

當(dāng)應(yīng)用進(jìn)程退到后臺(tái)正在被Cached的時(shí)候缴罗,可能會(huì)接收到從onTrimMemory()中返回的下面的值之一:

**TRIM_MEMORY_BACKGROUND: **系統(tǒng)正運(yùn)行于低內(nèi)存狀態(tài)并且你的進(jìn)程正處于LRU緩存名單中最不容易殺掉的位置。盡管你的應(yīng)用進(jìn)程并不是處于被殺掉的高危險(xiǎn)狀態(tài)舌界,系統(tǒng)可能已經(jīng)開始?xì)⒌鬖RU緩存中的其他進(jìn)程了呻拌。你應(yīng)該釋放那些容易恢復(fù)的資源柏锄,以便于你的進(jìn)程可以保留下來趾娃,這樣當(dāng)用戶回退到你的應(yīng)用的時(shí)候才能夠迅速恢復(fù)。
**TRIM_MEMORY_MODERATE: **系統(tǒng)正運(yùn)行于低內(nèi)存狀態(tài)并且你的進(jìn)程已經(jīng)已經(jīng)接近LRU名單的中部位置笤成。如果系統(tǒng)開始變得更加內(nèi)存緊張炕泳,你的進(jìn)程是有可能被殺死的浙芙。
**TRIM_MEMORY_COMPLETE: **系統(tǒng)正運(yùn)行于低內(nèi)存的狀態(tài)并且你的進(jìn)程正處于LRU名單中最容易被殺掉的位置嗡呼。你應(yīng)該釋放任何不影響你的應(yīng)用恢復(fù)狀態(tài)的資源南窗。


因?yàn)閛nTrimMemory()的回調(diào)是在API 14才被加進(jìn)來的,對(duì)于老的版本壕翩,你可以使用onLowMemory)回調(diào)來進(jìn)行兼容。onLowMemory相當(dāng)與TRIM_MEMORY_COMPLETE荐操。

請(qǐng)注意:當(dāng)系統(tǒng)開始清除LRU緩存中的進(jìn)程時(shí)托启,雖然它首先按照LRU的順序來執(zhí)行操作屯耸,但是它同樣會(huì)考慮進(jìn)程的內(nèi)存使用量以及其他因素。占用越少的進(jìn)程越容易被留下來多矮。
4)資源文件需要選擇合適的文件夾進(jìn)行存放
我們知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夾下的圖片在不同的設(shè)備上會(huì)經(jīng)過scale的處理塔逃。例如我們只在hdpi的目錄下放置了一張100100的圖片伏蚊,那么根據(jù)換算關(guān)系躏吊,xxhdpi的手機(jī)去引用那張圖片就會(huì)被拉伸到200200颜阐。需要注意到在這種情況下凳怨,內(nèi)存占用是會(huì)顯著提高的。對(duì)于不希望被拉伸的圖片均蜜,需要放到assets或者nodpi的目錄下篙顺。
5)Try catch某些大內(nèi)存分配的操作
在某些情況下德玫,我們需要事先評(píng)估那些可能發(fā)生OOM的代碼宰僧,對(duì)于這些可能發(fā)生OOM的代碼琴儿,加入catch機(jī)制造成,可以考慮在catch里面嘗試一次降級(jí)的內(nèi)存分配操作谜疤。例如decode bitmap的時(shí)候履肃,catch到OOM坐桩,可以嘗試把采樣比例再增加一倍之后膘螟,再次嘗試decode荆残。
6)謹(jǐn)慎使用static對(duì)象
因?yàn)閟tatic的生命周期過長(zhǎng)内斯,和應(yīng)用的進(jìn)程保持一致俘闯,使用不當(dāng)很可能導(dǎo)致對(duì)象泄漏真朗,在Android中應(yīng)該謹(jǐn)慎使用static對(duì)象(如圖19所示)。


圖19
7)特別留意單例對(duì)象中不合理的持有
雖然單例模式簡(jiǎn)單實(shí)用蹭睡,提供了很多便利性,但是因?yàn)閱卫纳芷诤蛻?yīng)用保持一致辫呻,使用不合理很容易出現(xiàn)持有對(duì)象的泄漏放闺。
8)珍惜Services資源
如果你的應(yīng)用需要在后臺(tái)使用service怖侦,除非它被觸發(fā)并執(zhí)行一個(gè)任務(wù)搬葬,否則其他時(shí)候Service都應(yīng)該是停止?fàn)顟B(tài)。另外需要注意當(dāng)這個(gè)service完成任務(wù)之后因?yàn)橥V箂ervice失敗而引起的內(nèi)存泄漏抡锈。 當(dāng)你啟動(dòng)一個(gè)Service乔外,系統(tǒng)會(huì)傾向?yàn)榱吮A暨@個(gè)Service而一直保留Service所在的進(jìn)程撇簿。這使得進(jìn)程的運(yùn)行代價(jià)很高补疑,因?yàn)橄到y(tǒng)沒有辦法把Service所占用的RAM空間騰出來讓給其他組件莲组,另外Service還不能被Paged out锹杈。這減少了系統(tǒng)能夠存放到LRU緩存當(dāng)中的進(jìn)程數(shù)量竭望,它會(huì)影響應(yīng)用之間的切換效率咬清,甚至?xí)?dǎo)致系統(tǒng)內(nèi)存使用不穩(wěn)定,從而無法繼續(xù)保持住所有目前正在運(yùn)行的service画髓。 建議使用IntentService夺谁,它會(huì)在處理完交代給它的任務(wù)之后盡快結(jié)束自己匾鸥。更多信息岗照,請(qǐng)閱讀 Running in a Background Service攒至。
9)優(yōu)化布局層次迫吐,減少內(nèi)存消耗
越扁平化的視圖布局,占用的內(nèi)存就越少溉浙,效率越高戳稽。我們需要盡量保證布局足夠扁平化惊奇,當(dāng)使用系統(tǒng)提供的View無法實(shí)現(xiàn)足夠扁平的時(shí)候考慮使用自定義View來達(dá)到目的。
10)謹(jǐn)慎使用“抽象”編程
很多時(shí)候乓序,開發(fā)者會(huì)使用抽象類作為”好的編程實(shí)踐”坎背,因?yàn)槌橄竽軌蛱嵘a的靈活性與可維護(hù)性。然而耿戚,抽象會(huì)導(dǎo)致一個(gè)顯著的額外內(nèi)存開銷:他們需要同等量的代碼用于可執(zhí)行,那些代碼會(huì)被mapping到內(nèi)存中膜蛔,因此如果你的抽象沒有顯著的提升效率坛猪,應(yīng)該盡量避免他們。
11)使用nano protobufs序列化數(shù)據(jù)
Protocol buffers是由Google為序列化結(jié)構(gòu)數(shù)據(jù)而設(shè)計(jì)的皂股,一種語言無關(guān)墅茉,平臺(tái)無關(guān),具有良好的擴(kuò)展性呜呐。類似XML就斤,卻比XML更加輕量洋机,快速副砍,簡(jiǎn)單。如果你需要為你的數(shù)據(jù)實(shí)現(xiàn)序列化與協(xié)議化,建議使用nano protobufs匙隔。關(guān)于更多細(xì)節(jié)再膳,請(qǐng)參考 protobuf readme的”Nano version”章節(jié)。
12)謹(jǐn)慎使用依賴注入框架
使用類似Guice或者RoboGuice等框架注入代碼熙参,在某種程度上可以簡(jiǎn)化你的代碼黍匾。圖20是使用RoboGuice前后的對(duì)比圖:

圖20 使用RoboGuice前后對(duì)比圖
使用RoboGuice之后融痛,代碼是簡(jiǎn)化了不少。然而目派,那些注入框架會(huì)通過掃描你的代碼執(zhí)行許多初始化的操作,這會(huì)導(dǎo)致你的代碼需要大量的內(nèi)存空間來mapping代碼,而且mapped pages會(huì)長(zhǎng)時(shí)間的被保留在內(nèi)存中闽寡。除非真的很有必要何乎,建議謹(jǐn)慎使用這種技術(shù)各墨。
13)謹(jǐn)慎使用多進(jìn)程
使用多進(jìn)程可以把應(yīng)用中的部分組件運(yùn)行在單獨(dú)的進(jìn)程當(dāng)中,這樣可以擴(kuò)大應(yīng)用的內(nèi)存占用范圍,但是這個(gè)技術(shù)必須謹(jǐn)慎使用,絕大多數(shù)應(yīng)用都不應(yīng)該貿(mào)然使用多進(jìn)程更扁,一方面是因?yàn)槭褂枚噙M(jìn)程會(huì)使得代碼邏輯更加復(fù)雜听隐,另外如果使用不當(dāng),它可能反而會(huì)導(dǎo)致顯著增加內(nèi)存禽车。當(dāng)你的應(yīng)用需要運(yùn)行一個(gè)常駐后臺(tái)的任務(wù),而且這個(gè)任務(wù)并不輕量碗硬,可以考慮使用這個(gè)技術(shù)猎物。
一個(gè)典型的例子是創(chuàng)建一個(gè)可以長(zhǎng)時(shí)間后臺(tái)播放的Music Player搀罢。如果整個(gè)應(yīng)用都運(yùn)行在一個(gè)進(jìn)程中,當(dāng)后臺(tái)播放的時(shí)候掸犬,前臺(tái)的那些UI資源也沒有辦法得到釋放。類似這樣的應(yīng)用可以切分成2個(gè)進(jìn)程:一個(gè)用來操作UI形真,另外一個(gè)給后臺(tái)的Service。
14)使用ProGuard來剔除不需要的代碼
ProGuard能夠通過移除不需要的代碼唱遭,重命名類锈拨,域與方法等等對(duì)代碼進(jìn)行壓縮谷浅,優(yōu)化與混淆。使用ProGuard可以使得你的代碼更加緊湊站削,這樣能夠減少mapping代碼所需要的內(nèi)存空間。
15)謹(jǐn)慎使用第三方libraries
很多開源的library代碼都不是為移動(dòng)網(wǎng)絡(luò)環(huán)境而編寫的睦柴,如果運(yùn)用在移動(dòng)設(shè)備上,并不一定適合。即使是針對(duì)Android而設(shè)計(jì)的library,也需要特別謹(jǐn)慎,特別是在你不知道引入的library具體做了什么事情的時(shí)候粹湃。例如壳猜,其中一個(gè)library使用的是nano protobufs, 而另外一個(gè)使用的是micro protobufs。這樣一來,在你的應(yīng)用里面就有2種protobuf的實(shí)現(xiàn)方式疯特。這樣類似的沖突還可能發(fā)生在輸出日志徘层,加載圖片,緩存等等模塊里面。另外不要為了1個(gè)或者2個(gè)功能而導(dǎo)入整個(gè)library责蝠,如果沒有一個(gè)合適的庫與你的需求相吻合,你應(yīng)該考慮自己去實(shí)現(xiàn)医男,而不是導(dǎo)入一個(gè)大而全的解決方案透罢。
16)考慮不同的實(shí)現(xiàn)方式來優(yōu)化內(nèi)存占用
在某些情況下,設(shè)計(jì)的某個(gè)方案能夠快速實(shí)現(xiàn)需求半抱,但是這個(gè)方案卻可能在內(nèi)存占用上表現(xiàn)的效率不夠好。例如:

圖21
對(duì)于上面這樣一個(gè)時(shí)鐘表盤的實(shí)現(xiàn)创夜,最簡(jiǎn)單的就是使用很多張包含指針的表盤圖片,使用幀動(dòng)畫實(shí)現(xiàn)指針的旋轉(zhuǎn)仙逻。但是如果把指針扣出來挥下,單獨(dú)進(jìn)行旋轉(zhuǎn)繪制揍魂,顯然比載入N多張圖片占用的內(nèi)存要少很多。當(dāng)然這樣做棚瘟,代碼復(fù)雜度上會(huì)有所增加现斋,這里就需要在優(yōu)化內(nèi)存占用與實(shí)現(xiàn)簡(jiǎn)易度之間進(jìn)行權(quán)衡了。
總結(jié)

設(shè)計(jì)風(fēng)格很大程度上會(huì)影響到程序的內(nèi)存與性能偎蘸,相對(duì)來說庄蹋,如果大量使用類似Material Design的風(fēng)格,不僅安裝包可以變小迷雪,還可以減少內(nèi)存的占用限书,渲染性能與加載性能都會(huì)有一定的提升。
內(nèi)存優(yōu)化并不就是說程序占用的內(nèi)存越少就越好章咧,如果因?yàn)橄胍3指偷膬?nèi)存占用倦西,而頻繁觸發(fā)執(zhí)行g(shù)c操作,在某種程度上反而會(huì)導(dǎo)致應(yīng)用性能整體有所下降赁严,這里需要綜合考慮做一定的權(quán)衡扰柠。
Android的內(nèi)存優(yōu)化涉及的知識(shí)面還有很多:內(nèi)存管理的細(xì)節(jié),垃圾回收的工作原理疼约,如何查找內(nèi)存泄漏等等都可以展開講很多卤档。OOM是內(nèi)存優(yōu)化當(dāng)中比較突出的一點(diǎn),盡量減少OOM的概率對(duì)內(nèi)存優(yōu)化有著很大的意義程剥。

最后劝枣,很榮幸收到CSDN的邀請(qǐng)參加MDCC 2015中國移動(dòng)開發(fā)者大會(huì)。從第一屆MDCC開始织鲸,就一直關(guān)注著這個(gè)國內(nèi)移動(dòng)互聯(lián)網(wǎng)領(lǐng)域的開發(fā)者頂級(jí)盛會(huì)舔腾,不僅從大會(huì)上的各大公司高管與創(chuàng)始人的演講中感受到了移動(dòng)互聯(lián)網(wǎng)的浪潮,更是從不少技術(shù)達(dá)人的分享中學(xué)習(xí)到了非常夯實(shí)的干貨搂擦。希望這一次參與MDCC能夠在線下認(rèn)識(shí)各位技術(shù)達(dá)人稳诚,和更多的移動(dòng)開發(fā)同學(xué)交流學(xué)習(xí)。預(yù)祝MDCC 2015圓滿成功盾饮!
其他參考

Google I/O 2011: Memory management for Android Apps(需自備梯子)
Managing Your App’s Memory
Avoiding memory leaks
Android性能優(yōu)化典范 - 第3季
Android性能優(yōu)化典范 - 第2季
Android性能優(yōu)化典范
Android性能優(yōu)化之內(nèi)存篇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末采桃,一起剝皮案震驚了整個(gè)濱河市懒熙,隨后出現(xiàn)的幾起案子丘损,更是在濱河造成了極大的恐慌,老刑警劉巖工扎,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徘钥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡肢娘,警方通過查閱死者的電腦和手機(jī)呈础,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門舆驶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人而钞,你說我怎么就攤上這事沙廉。” “怎么了臼节?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵撬陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我网缝,道長(zhǎng)巨税,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任粉臊,我火速辦了婚禮草添,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扼仲。我一直安慰自己远寸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布犀盟。 她就那樣靜靜地躺著而晒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪阅畴。 梳的紋絲不亂的頭發(fā)上倡怎,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音贱枣,去河邊找鬼监署。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纽哥,可吹牛的內(nèi)容都是我干的钠乏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼春塌,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼晓避!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起只壳,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤俏拱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后吼句,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锅必,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年惕艳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搞隐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驹愚。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖劣纲,靈堂內(nèi)的尸體忽然破棺而出逢捺,到底是詐尸還是另有隱情,我是刑警寧澤癞季,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布蒸甜,位于F島的核電站,受9級(jí)特大地震影響余佛,放射性物質(zhì)發(fā)生泄漏柠新。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一辉巡、第九天 我趴在偏房一處隱蔽的房頂上張望恨憎。 院中可真熱鬧,春花似錦郊楣、人聲如沸憔恳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钥组。三九已至,卻和暖如春今瀑,著一層夾襖步出監(jiān)牢的瞬間程梦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工橘荠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屿附,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓哥童,卻偏偏與公主長(zhǎng)得像挺份,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贮懈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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