Android 內(nèi)存管理機制

本文主要包括三大部分內(nèi)容:

內(nèi)存管理基礎(chǔ):從整個計算機領(lǐng)域簡述主要的內(nèi)存管理技術(shù)梁肿。
Linux的內(nèi)存管理機制:Android畢竟是基于Linux內(nèi)核實現(xiàn)的操作系統(tǒng),因此有必要了解一下Linux的內(nèi)存管理機制。
Android的內(nèi)存管理相關(guān)知識:Android又不同于Linux庭惜,它是一個移動操作系統(tǒng)民轴,因此其內(nèi)存管理上也有自己的特性,這一部分詳細(xì)講述Android的內(nèi)存管理相關(guān)知識赃磨,包括 內(nèi)存管理機制進程管理

內(nèi)存管理基礎(chǔ)

概述

CPU只能訪問其寄存器(Register)和內(nèi)存(Memory)洼裤, 無法直接訪問硬盤(Disk)邻辉。 存儲在硬盤上的數(shù)據(jù)必須首先傳輸?shù)絻?nèi)存中才能被CPU訪問。從訪問速度來看,對寄存器的訪問非扯髋妫快在扰,通常為1納秒; 對內(nèi)存的訪問相對較慢雷客,通常為100納秒(使用緩存加速的情況下)芒珠;而對硬盤驅(qū)動器的訪問速度最慢,通常為10毫秒搅裙。

寄存器(Register):CPU內(nèi)部的高速存儲區(qū)域

當(dāng)一個程序加載到內(nèi)存中時皱卓,它由四個內(nèi)存區(qū)域組成:

  • 堆棧(Stack):存儲由該程序的每個函數(shù)創(chuàng)建的臨時變量
  • 堆(Heap):該區(qū)域特別適用于動態(tài)內(nèi)存分配
  • 數(shù)據(jù)(Data):存儲該程序的全局變量和靜態(tài)變量
  • 代碼(Code):存儲該程序的指令

主要的內(nèi)存管理技術(shù)

  • Base and limit registers(基址寄存器和界限寄存器)
  • Virtual memory(虛擬內(nèi)存)
  • Swapping(交換)
  • Segmentation(分段)
  • Paging(分頁)
Base and limit registers(基址寄存器和界限寄存器)

必須限制進程,以便它們只能訪問屬于該特定進程的內(nèi)存位置部逮。

每個進程都有一個基址寄存器和限制寄存器:

  • 基址寄存器保存最小的有效存儲器地址
  • 限制寄存器指定范圍的大小

例如娜汁,process 2的有效內(nèi)存地址是300040到420940

那么每個來自用戶進程的內(nèi)存訪問都將首先針對這兩個寄存器進行一次檢查:

操作系統(tǒng)內(nèi)核可以訪問所有內(nèi)存位置,因為它需要管理整個內(nèi)存兄朋。

Virtual memory(虛擬內(nèi)存)

虛擬內(nèi)存(VM)是OS為內(nèi)存管理提供的基本抽象掐禁。

  • 所有程序都使用虛擬內(nèi)存地址
  • 虛擬地址會被轉(zhuǎn)換為物理地址
  • 物理地址表示數(shù)據(jù)的實際物理位置
  • 物理位置可以是內(nèi)存或磁盤

虛擬地址到物理地址的轉(zhuǎn)換由存儲器管理單元(MMU - Memory Management Unit)處理。MMU使用重定位寄存器(relocation register)颅和,其值在硬件級別上被添加到每個內(nèi)存請求中傅事。

Swapping(交換)

交換是一種可以暫時將進程從內(nèi)存交換到后備存儲,而之后又可以將其返回內(nèi)存以繼續(xù)執(zhí)行的技術(shù)峡扩。

后備存儲通常是一個硬盤驅(qū)動器蹭越,其訪問速度快,且大小足以存儲內(nèi)存映像的副本教届。

如果沒有足夠的可用內(nèi)存來同時保留內(nèi)存中的所有正在運行的進程响鹃,則某些當(dāng)前未使用CPU的進程可能會被交換到后備存儲中。

交換是一個非常緩慢的過程案训。 主要耗時部分是數(shù)據(jù)傳輸买置。例如,如果進程占用10MB內(nèi)存并且后備存儲的傳輸速率為40MB/秒萤衰,則需要0.25秒來進行數(shù)據(jù)傳輸堕义。 再加上將數(shù)據(jù)交換回內(nèi)存的時間,總傳輸時間可能是半秒脆栋,這是一個巨大的延遲,因此洒擦,有些操作系統(tǒng)已經(jīng)不再使用交換了椿争。

Segmentation(分段)

分段是一種將內(nèi)存分解為邏輯片段的技術(shù),其中每個片段代表一組相關(guān)信息熟嫩。 例如秦踪,將每個進程按照堆棧,堆,數(shù)據(jù)以及代碼分為不同的段椅邓,還有OS內(nèi)核的數(shù)據(jù)段等柠逞。

將內(nèi)存分解成較小的段會增加尋找空閑內(nèi)存的機會。

每個段都有一對寄存器:

  • 基址寄存器:包含段駐留在內(nèi)存中的起始物理地址
  • 限制寄存器:指定段的長度

段表(Segment table)存儲每個段的基址和限制寄存器信息景馁。

使用分段時板壮,虛擬內(nèi)存地址是一對:<段號,偏移量>

  • 段號(Segment Number):用作段表的索引以查找特定條目
  • 偏移量(Offset):首先與限制寄存器進行比較合住,然后與基址結(jié)合以計算物理內(nèi)存地址
Paging(分頁)

有時可用內(nèi)存被分成許多小塊绰精,其中沒有一塊足夠大以滿足下一個內(nèi)存需求,然而他們的總和卻可以透葛。這個問題被稱為碎片(Fragmentation)笨使,許多內(nèi)存分配策略都會受其影響。

分頁是一種內(nèi)存管理技術(shù)僚害,它允許進程的物理內(nèi)存不連續(xù)硫椰。它通過在稱為頁面(Page)的相同大小的塊中分配內(nèi)存來消除碎片問題,是目前比較優(yōu)秀的內(nèi)存管理技術(shù)萨蚕。

分頁將物理內(nèi)存劃分為多個大小相等的塊最爬,稱為幀(Frame)。并將進程的邏輯內(nèi)存空間也劃分為大小相等的塊门岔,稱為頁面(Page)爱致。

任何進程中的任何頁面都可以放入任何可用的幀中。

頁表(Page Table)用于查找此刻存儲特定頁面的幀寒随。

使用分頁時糠悯,虛擬內(nèi)存地址是一對:<頁碼,偏移量>

  • 頁碼(Page Number):用作頁表的索引妻往,以查找此頁面的條目
  • 偏移量(Offset):與基址相結(jié)合互艾,以定義物理內(nèi)存地址

舉一個分頁地址轉(zhuǎn)換的例子:

虛擬內(nèi)存地址為0x13325328,頁表項0x13325包含的值是0x03004讯泣,那么物理地址是什么纫普?

答案:
物理地址是0x03004328
頁碼為0x13325,偏移量為0x328
相應(yīng)的幀號是0x03004

Linux的內(nèi)存管理機制

在Linux系統(tǒng)下好渠,監(jiān)控內(nèi)存常用的命令是free昨稼、top等,下面是一個free命令的執(zhí)行結(jié)果:

要了解Linux的內(nèi)存管理拳锚,首先要明白上例中各個名詞的意義:

  • total:物理內(nèi)存的總大小假栓。
  • used:已經(jīng)使用的物理內(nèi)存多小。
  • free:空閑的物理內(nèi)存值霍掺。
  • shared:多個進程共享的內(nèi)存值匾荆。
  • buffers / cached:用于磁盤緩存的大邪柚(這部分是從物理內(nèi)存中劃出來的)。
  • 第二行Mem:代表物理內(nèi)存使用情況牙丽。
  • 第三行(-/+ buffers/cached):代表磁盤緩存使用狀態(tài)简卧。
  • 第四行:Swap表示交換空間內(nèi)存使用狀態(tài)(這部分實際上是從磁盤上虛擬出來的邏輯內(nèi)存)。

free命令輸出的內(nèi)存狀態(tài)烤芦,可以從兩個角度來看:內(nèi)核角度举娩、應(yīng)用層角度。

1.從內(nèi)核角度來查看內(nèi)存的狀態(tài):

就是內(nèi)核目前可以直接分配到拍棕,不需要額外的操作晓铆,即free命令第二行 Mem 的輸出。從上例中可見绰播,41940 + 16360492 = 16402432骄噪,也就是說Mem行的 free + used = total,注意蠢箩,這里的free并不包括buffers和cached链蕊。

2.從應(yīng)用層角度來查看內(nèi)存的狀態(tài):

也就是Linux上運行的程序可以使用的內(nèi)存大小,即free命令第三行 -/+ buffers/cache 的輸出谬泌。再來做一個計算41940+(465404+12714880)=13222224滔韵,即Mem行的free + buffers + cached = -/+ buffers/cache行的free,也就是說應(yīng)用可用的物理內(nèi)存值是Mem行的free掌实、buffers和cached三者之和陪蜻,可見-/+ buffers/cache行的free是包括buffers和cached的。

對于應(yīng)用程序來說贱鼻,buffers/cached占有的內(nèi)存是可用的宴卖,因為buffers/cached是為了提高文件讀取的性能,當(dāng)應(yīng)用程序需要用到內(nèi)存的時候邻悬,buffers/cached會很快地被回收症昏,以供應(yīng)用程序使用。

物理內(nèi)存和虛擬內(nèi)存

物理內(nèi)存就是系統(tǒng)硬件提供的內(nèi)存大小父丰,是真正的內(nèi)存肝谭。在linux下還有一個虛擬內(nèi)存的概念,虛擬內(nèi)存就是為了滿足物理內(nèi)存的不足而提出的策略蛾扇,它是利用磁盤空間虛擬出的一塊邏輯內(nèi)存攘烛,用作虛擬內(nèi)存的磁盤空間被稱為 交換空間(Swap Space)

linux的內(nèi)存管理采取的是分頁存取機制屁桑,為了保證物理內(nèi)存能得到充分的利用医寿,內(nèi)核會在適當(dāng)?shù)臅r候?qū)⑽锢韮?nèi)存中不經(jīng)常使用的數(shù)據(jù)塊自動交換到虛擬內(nèi)存中,而將經(jīng)常使用的信息保留到物理內(nèi)存蘑斧。而進行這種交換所遵循的依據(jù)是“LRU”算法(Least Recently Used,最近最少使用算法)。

Buffers和Cached有什么用

在任何系統(tǒng)中竖瘾,文件的讀寫都是一個耗時的操作沟突,當(dāng)應(yīng)用程序需要讀寫文件中的數(shù)據(jù)時,操作系統(tǒng)先分配一些內(nèi)存捕传,將數(shù)據(jù)從磁盤讀入到這些內(nèi)存中惠拭,然后應(yīng)用程序讀寫這部分內(nèi)存數(shù)據(jù),之后系統(tǒng)再將數(shù)據(jù)從內(nèi)存寫到磁盤上庸论。如果是大量文件讀寫甚至重復(fù)讀寫职辅,系統(tǒng)讀寫性能就變得非常低下,在這種情況下聂示,Linux引入了緩存機制域携。

buffers與cached都是從物理內(nèi)存中分離出來的,主要用于實現(xiàn)磁盤緩存鱼喉,用來保存系統(tǒng)曾經(jīng)打開過的文件以及文件屬性信息,這樣當(dāng)操作系統(tǒng)需要讀取某些文件時,會首先在buffers與cached內(nèi)存區(qū)查找诽偷,如果找到侈净,直接讀出傳送給應(yīng)用程序,否則编曼,才從磁盤讀取豆巨,通過這種緩存機制,大大降低了對磁盤的IO操作掐场,提高了操作系統(tǒng)的數(shù)據(jù)訪問性能往扔。而這種磁盤高速緩存則是基于兩個事實:第一,內(nèi)存訪問速度遠遠高于磁盤訪問速度刻肄;第二瓤球,數(shù)據(jù)一旦被訪問,就很有可能短期內(nèi)再次被訪問敏弃。

另外卦羡,buffers與cached緩存的內(nèi)容也是不同的。buffers是用來緩沖塊設(shè)備做的麦到,它只記錄文件系統(tǒng)的元數(shù)據(jù)(metadata)以及 tracking in-flight pages绿饵,而cached是用來給文件做緩沖。更通俗一點說:buffers主要用來存放目錄里面有什么內(nèi)容瓶颠,文件的屬性以及權(quán)限等等拟赊。而cached直接用來記憶我們打開過的文件和程序

為了驗證我們的結(jié)論是否正確粹淋,可以通過vi打開一個非常大的文件吸祟,看看cached的變化瑟慈,然后再次vi這個文件,感覺一下是不是第二次打開的速度明顯快于第一次屋匕?

接著執(zhí)行下面的命令:

find /* -name  *.conf

看看buffers的值是否變化葛碧,然后重復(fù)執(zhí)行find命令,看看兩次顯示速度有何不同过吻。

Linux內(nèi)存管理的哲學(xué)

Free memory is wasted memory.

Linux的哲學(xué)是盡可能多的使用內(nèi)存进泼,減少磁盤IO,因為內(nèi)存的速度比磁盤快得多纤虽。Linux總是在力求緩存更多的數(shù)據(jù)和信息乳绕,內(nèi)存不夠時,將一些不經(jīng)常使用的數(shù)據(jù)轉(zhuǎn)移到交換分區(qū)(Swap Space)中以釋放更多可用物理內(nèi)存逼纸,當(dāng)然洋措,如果交換分區(qū)的數(shù)據(jù)再次被讀寫時,又會被轉(zhuǎn)移到物理內(nèi)存中樊展,這種設(shè)計思路提高了系統(tǒng)的整體性能呻纹。而Windows的處理方式是,內(nèi)存和虛擬內(nèi)存一起使用专缠,不是以內(nèi)存操作為主雷酪,結(jié)果就是IO的負(fù)擔(dān)比較大,可能拖慢處理速度涝婉。

Linux和Windows在內(nèi)存管理機制上的區(qū)別

在Linux系統(tǒng)使用過程中哥力,你會發(fā)現(xiàn),無論你的電腦內(nèi)存配置多么優(yōu)越墩弯,仍然不時的發(fā)生可用內(nèi)存吃緊的現(xiàn)象吩跋,感覺內(nèi)存不夠用了,其實不然渔工。這是Linux內(nèi)存管理的優(yōu)秀特性锌钮,無論物理內(nèi)存有多大,Linux都將其充分利用引矩,將一些程序調(diào)用過的硬盤數(shù)據(jù)緩存到內(nèi)存梁丘,利用內(nèi)存讀寫的高速性提高系統(tǒng)的數(shù)據(jù)訪問性能。而Window只在需要內(nèi)存時旺韭,才為應(yīng)用分配內(nèi)存氛谜,不能充分利用大容量的內(nèi)存空間。換句話說区端,每增加一些內(nèi)存值漫,Linux都能將其利用起來,充分發(fā)揮硬件投資帶來的好處织盼,而Windows只將其作為擺設(shè)杨何。

所以說酱塔,一般我們不需要太關(guān)注Linux的內(nèi)存占用情況,而如果Swap占用率一直居高不下的話晚吞,就很有可能真的是需要擴展內(nèi)存了延旧。

Android的內(nèi)存管理機制

Android使用虛擬內(nèi)存和分頁谋国,不支持交換

垃圾收集

無論是ART還是Dalvik虛擬機槽地,都和眾多Java虛擬機一樣,屬于一種托管內(nèi)存環(huán)境(程序員不需要顯示的管理內(nèi)存的分配與回收芦瘾,交由系統(tǒng)自動管理)捌蚊。托管內(nèi)存環(huán)境會跟蹤每個內(nèi)存分配, 一旦確定程序不再使用一塊內(nèi)存近弟,它就會將其釋放回堆中缅糟,而無需程序員的任何干預(yù)。 回收托管內(nèi)存環(huán)境中未使用內(nèi)存的機制稱為垃圾回收祷愉。

垃圾收集有兩個目標(biāo):

  • 在程序中查找將來無法訪問的數(shù)據(jù)對象;
  • 回收這些對象使用的資源窗宦。

Android的垃圾收集器不帶壓縮整理功能(Compact),即不會對Heap做碎片整理二鳄。

Android的內(nèi)存堆是分代式(Generational)的赴涵,意味著它會將所有分配的對象進行分代,然后分代跟蹤這些對象订讼。 例如髓窜,最近分配的對象屬于年輕代(Young Generation)。 當(dāng)一個對象長時間保持活動狀態(tài)時欺殿,它可以被提升為年老代(Older Generation)寄纵,之后還能進一步提升為永久代(Permanent Generation)

每一代的對象可占用的內(nèi)存總量都有其專用上限脖苏。 每當(dāng)一代開始填滿時程拭,系統(tǒng)就會執(zhí)行垃圾收集事件以試圖釋放內(nèi)存。 垃圾收集的持續(xù)時間取決于它在收集哪一代的對象以及每一代中有多少活動對象棍潘。

雖然垃圾收集速度非呈研快,但它仍然會影響應(yīng)用程序的性能蜒谤。通常情況下你不需要控制代碼中何時執(zhí)行垃圾收集事件山宾。 系統(tǒng)有一組用于確定何時執(zhí)行垃圾收集的標(biāo)準(zhǔn)。 滿足條件后鳍徽,系統(tǒng)將停止執(zhí)行當(dāng)前進程并開始垃圾回收资锰。 如果在像動畫或音樂播放這樣的密集處理循環(huán)中發(fā)生垃圾收集,則會增加處理時間阶祭。 這種增加可能會導(dǎo)致你的應(yīng)用程序中的代碼執(zhí)行超過建議的16ms閾值绷杜。

為實現(xiàn)高效直秆,流暢的幀渲染,Android建議繪制一幀的時間不要超過16ms鞭盟。

此外圾结,你的代碼可能會執(zhí)行各種工作,這些工作會導(dǎo)致垃圾收集事件更頻繁地發(fā)生齿诉,或使其持續(xù)時間超過正常范圍筝野。 例如,如果在Alpha混合動畫的每個幀期間在for循環(huán)的最內(nèi)部分配多個對象粤剧,則大量的對象就會污染內(nèi)存堆歇竟。 此時,垃圾收集器會執(zhí)行多個垃圾收集事件抵恋,并可能降低應(yīng)用程序的性能焕议。

共享內(nèi)存

Android可以跨進程共享RAM頁面(Pages)。 它可以通過以下方式實現(xiàn):

  • 每個應(yīng)用程序進程都是從名為Zygote的現(xiàn)有進程分叉(fork)出來的弧关。 Zygote進程在系統(tǒng)引導(dǎo)并加載framework代碼和資源(例如Activity Themes)時啟動盅安。 要啟動新的應(yīng)用程序進程,系統(tǒng)會fork Zygote進程世囊,然后在新進程中加載并運行應(yīng)用程序的代碼别瞭。 這種方法允許在所有應(yīng)用程序進程中共享大多數(shù)的為framework代碼和資源分配的RAM頁面
  • 大多數(shù)靜態(tài)數(shù)據(jù)都被映射到一個進程中茸习。 該技術(shù)允許在進程之間共享數(shù)據(jù)畜隶,并且還允許在需要時將其Page out。這些靜態(tài)數(shù)據(jù)包括:Dalvik代碼(通過將其置于預(yù)鏈接的.odex文件中進行直接的memory-mapping)号胚,app資源(通過將資源表設(shè)計為可以mmap的結(jié)構(gòu)并通過對齊APK的zip條目) 和傳統(tǒng)的項目元素籽慢,如.so文件中的本地代碼。
  • 在許多地方猫胁,Android使用顯式分配的共享內(nèi)存區(qū)域(使用ashmem或gralloc)在進程間共享相同的動態(tài)RAM箱亿。 例如,Window surface在應(yīng)用程序和屏幕合成器之間使用共享內(nèi)存弃秆,而游標(biāo)緩沖區(qū)在Content Provider和客戶端之間使用共享內(nèi)存届惋。

分配和回收應(yīng)用的內(nèi)存

Android為每個進程分配內(nèi)存的時候,采用了彈性分配方式菠赚,也就是剛開始并不會一下分配很多內(nèi)存給每個進程脑豹,而是給每一個進程分配一個“夠用”的虛擬內(nèi)存范圍。這個范圍是根據(jù)每一個設(shè)備實際的物理內(nèi)存大小來決定的衡查,并且可以隨著應(yīng)用后續(xù)需求而增加瘩欺,但最多也只能達到系統(tǒng)為每個應(yīng)用定義的上限。

堆的邏輯大小與其使用的物理內(nèi)存總量并不完全相同。 在檢查應(yīng)用程序的堆時俱饿,Android會計算一個名為“比例集大小”(PSS)的值歌粥,該值會考慮與其他進程共享的臟頁面和干凈頁面,但其總量與共享該RAM的應(yīng)用程序數(shù)量成正比拍埠。 此PSS總量就是系統(tǒng)認(rèn)為是你的物理內(nèi)存占用量失驶。

Android會在內(nèi)存中盡量長時間的保持應(yīng)用進程,即使有些進程不再使用了枣购。這樣嬉探,當(dāng)用戶下次啟動應(yīng)用的時候,只需要恢復(fù)當(dāng)前進程就可以了坷虑,不需要重新創(chuàng)建進程甲馋,進而減少應(yīng)用的啟動時間。只有當(dāng)Android系統(tǒng)發(fā)現(xiàn)內(nèi)存不足迄损,而其他為用戶提供更緊急服務(wù)的進程又需要內(nèi)存時,Android就會決定關(guān)閉某些進程以回收內(nèi)存账磺。關(guān)于這部分內(nèi)容芹敌,稍后再細(xì)說。

限制應(yīng)用的內(nèi)存

為了維護高效的多任務(wù)環(huán)境垮抗,Android為每個應(yīng)用程序設(shè)置了堆大小的硬性限制氏捞。 該限制因設(shè)備而異,取決于設(shè)備總體可用的RAM冒版。 如果應(yīng)用程序已達到該限制并嘗試分配更多內(nèi)存液茎,則會收到 OutOfMemoryError

在某些情況下辞嗡,你可能希望查詢系統(tǒng)以準(zhǔn)確確定當(dāng)前設(shè)備上可用的堆空間大小捆等,例如,確定可以安全地保留在緩存中的數(shù)據(jù)量续室。 你可以通過調(diào)用 getMemoryClass() 來查詢系統(tǒng)中的這個數(shù)字栋烤。 此方法返回一個整數(shù),指示應(yīng)用程序堆可用的兆字節(jié)數(shù)挺狰。

切換應(yīng)用

當(dāng)用戶在應(yīng)用程序之間切換時明郭,Android會將非前臺應(yīng)用程序(即用戶不可見或并沒有運行諸如音樂播放等前臺服務(wù)的進程)緩存到一個最近最少使用緩存(LRU Cache)中。例如丰泊,當(dāng)用戶首次啟動應(yīng)用程序時薯定,會為其創(chuàng)建一個進程; 但是當(dāng)用戶離開應(yīng)用程序時,該進程不會退出瞳购。 系統(tǒng)會緩存該進程话侄。 如果用戶稍后返回應(yīng)用程序,系統(tǒng)將重新使用該進程苛败,從而使應(yīng)用程序切換更快满葛。

如果你的應(yīng)用程序具有緩存進程并且它保留了當(dāng)前不需要的內(nèi)存径簿,那么即使用戶未使用它,你的應(yīng)用程序也會影響系統(tǒng)的整體性能嘀韧。 當(dāng)系統(tǒng)內(nèi)存不足時篇亭,就會從最近最少使用的進程開始,終止LRU Cache中的進程锄贷。另外译蒂,系統(tǒng)還會綜合考慮保留了最多內(nèi)存的進程,并可能終止它們以釋放RAM谊却。

當(dāng)系統(tǒng)開始終止LRU Cache中的進程時柔昼,它主要是自下而上的。 系統(tǒng)還會考慮哪些進程占用更多內(nèi)存炎辨,因為在它被殺時會為系統(tǒng)提供更多內(nèi)存增益捕透。 因此在整個LRU列表中消耗的內(nèi)存越少,保留在列表中并且能夠快速恢復(fù)的機會就越大碴萧。

Android對Linux系統(tǒng)的內(nèi)存管理機制進行的優(yōu)化

Android對內(nèi)存的使用方式同樣是“盡最大限度的使用”乙嘀,這一點繼承了Linux的優(yōu)點。只不過有所不同的是破喻,Linux側(cè)重于盡可能多的緩存磁盤數(shù)據(jù)以降低磁盤IO進而提高系統(tǒng)的數(shù)據(jù)訪問性能虎谢,而Android側(cè)重于盡可能多的緩存進程以提高應(yīng)用啟動和切換速度。Linux系統(tǒng)在進程活動停止后就結(jié)束該進程曹质,而Android系統(tǒng)則會在內(nèi)存中盡量長時間的保持應(yīng)用進程婴噩,直到系統(tǒng)需要更多內(nèi)存為止。這些保留在內(nèi)存中的進程羽德,通常情況下不會影響系統(tǒng)整體運行速度几莽,反而會在用戶再次激活這些進程時,加快進程的啟動速度玩般,因為不用重新加載界面資源了银觅,這是Android標(biāo)榜的特性之一。所以坏为,Android現(xiàn)在不推薦顯式的“退出”應(yīng)用究驴。

那為什么內(nèi)存少的時候運行大型程序會慢呢,原因是:在內(nèi)存剩余不多時打開大型程序會觸發(fā)系統(tǒng)自身的進程調(diào)度策略匀伏,這是十分消耗系統(tǒng)資源的操作洒忧,特別是在一個程序頻繁向系統(tǒng)申請內(nèi)存的時候。這種情況下系統(tǒng)并不會關(guān)閉所有打開的進程够颠,而是選擇性關(guān)閉熙侍,頻繁的調(diào)度自然會拖慢系統(tǒng)。

Android中的進程管理

說到Android的內(nèi)存管理,就不得不提到進程管理蛉抓,因為進程管理確確切切的影響著系統(tǒng)內(nèi)存庆尘。在了解進程管理之前,我們首先了解一些基礎(chǔ)概念巷送。

當(dāng)某個應(yīng)用組件啟動且該應(yīng)用沒有運行其他任何組件時驶忌,Android 系統(tǒng)會使用單個執(zhí)行線程為應(yīng)用啟動新的 Linux 進程。默認(rèn)情況下笑跛,同一應(yīng)用的所有組件在相同的進程和線程(稱為“主”線程)中運行付魔。 如果某個應(yīng)用組件啟動且該應(yīng)用已存在進程(因為存在該應(yīng)用的其他組件),則該組件會在此進程內(nèi)啟動并使用相同的執(zhí)行線程飞蹂。 但是几苍,你也可以安排應(yīng)用中的其他組件在單獨的進程中運行,并為任何進程創(chuàng)建額外的線程陈哑。

Android應(yīng)用模型的設(shè)計思想取自Web 2.0的Mashup概念妻坝,是基于組件的應(yīng)用設(shè)計模式。在該模型下芥颈,每個應(yīng)用都由一系列的組件搭建而成惠勒,組件通過應(yīng)用的配置文件描述功能。Android系統(tǒng)依照組件的配置信息爬坑,了解各個組件的功能并進行統(tǒng)一調(diào)度。這就意味著涂臣,來自不同應(yīng)用的組件可以有機地結(jié)合在一起盾计,共同完成任務(wù),各個Android應(yīng)用赁遗,只有明確的組件邊界署辉,而不再有明確的進程邊界和應(yīng)用邊界。這種設(shè)計岩四,也令得開發(fā)者無需耗費精力去重新開發(fā)一些附屬功能哭尝,而是可以全身心地投入到核心功能的開發(fā)中。這樣不但提高了應(yīng)用開發(fā)的效率剖煌,也增強了用戶體驗(比如電子郵件中選擇圖片作為附件的功能材鹦,可以直接調(diào)用專門的圖片應(yīng)用的功能,不用自己從頭開發(fā))耕姊。

系統(tǒng)不會為每個組件實例創(chuàng)建單獨的線程桶唐。運行于同一進程的所有組件均在 UI 線程中實例化,并且對每個組件的系統(tǒng)調(diào)用均由該線程進行分派茉兰。 因此尤泽,響應(yīng)系統(tǒng)回調(diào)的方法(例如,報告用戶操作的 onKeyDown() 或生命周期回調(diào)方法)始終在進程的 UI 線程中運行(四大組件的各個生命周期回調(diào)方法都是在UI線程中觸發(fā)的)。

進程的生命周期

Android的一個不尋常的基本特征是應(yīng)用程序進程的生命周期并非是由應(yīng)用本身直接控制的坯约。相反熊咽,進程的生命周期是由系統(tǒng)決定的,系統(tǒng)會權(quán)衡每個進程對用戶的相對重要程度闹丐,以及系統(tǒng)的可用內(nèi)存總量來確定横殴。比如說相對于終止一個托管了正在與用戶交互的Activity的進程,系統(tǒng)更可能終止一個托管了屏幕上不再可見的Activity的進程妇智,否則這種后果是可怕的滥玷。因此,是否終止某個進程取決于該進程中所運行組件的狀態(tài)巍棱。Android會有限清理那些已經(jīng)不再使用的進程惑畴,以保證最小的副作用。

作為應(yīng)用開發(fā)者航徙,了解各個應(yīng)用組件(特別是Activity如贷、Service和BroadcastReceiver)如何影響應(yīng)用進程的生命周期非常重要。不正確的使用這些組件到踏,有可能導(dǎo)致系統(tǒng)在應(yīng)用執(zhí)行重要工作時終止進程杠袱。

舉個常見的例子, BroadcastReceiver 在其 onReceive() 方法中接收到Intent時啟動一個線程窝稿,然后從該函數(shù)返回楣富。而一旦返回,系統(tǒng)就認(rèn)為該 BroadcastReceiver 不再處于活動狀態(tài)伴榔,因此也就不再需要其托管進程(除非該進程中還有其他組件處于活動狀態(tài))纹蝴。這樣一來,系統(tǒng)就有可能隨時終止進程以回收內(nèi)存踪少,而這也最終會導(dǎo)致運行在進程中的線程被終止塘安。此問題的解決方案通常是從 BroadcastReceiver 中安排一個 JobService ,以便系統(tǒng)知道在該進程中仍有活動的工作援奢。

為了確定在內(nèi)存不足時終止哪些進程兼犯,Android會根據(jù)進程中正在運行的組件以及這些組件的狀態(tài),將每個進程放入“重要性層次結(jié)構(gòu)”中集漾。必要時切黔,系統(tǒng)會首先殺死重要性最低的進程,以此類推帆竹,以回收系統(tǒng)資源绕娘。這就相當(dāng)于為進程分配了優(yōu)先級的概念。

進程優(yōu)先級

Android中總共有5個進程優(yōu)先級(按重要性降序):

Foreground Process:前臺進程(正常不會被殺死)

用戶當(dāng)前操作所必需的進程栽连。有很多組件能以不同的方式使得其所在進程被判定為前臺進程险领。如果一個進程滿足以下任一條件侨舆,即視為前臺進程:

  • 托管用戶正在交互的 Activity(已調(diào)用 Activity 的 onResume() 方法)
  • 托管某個 Service,后者綁定到用戶正在交互的 Activity
  • 托管正執(zhí)行一個生命周期回調(diào)的 Service(onCreate()绢陌、onStart() 或 onDestroy())
  • 托管正執(zhí)行其 onReceive() 方法的 BroadcastReceiver

通常挨下,在任意給定時間前臺進程都為數(shù)不多。只有在內(nèi)存不足以支持它們同時繼續(xù)運行這一萬不得已的情況下脐湾,系統(tǒng)才會終止它們臭笆。 此時,設(shè)備往往已達到內(nèi)存分頁狀態(tài)秤掌,因此需要終止一些前臺進程來確保用戶界面正常響應(yīng)愁铺。

Visible Process:可見進程(正常不會被殺死)

沒有任何前臺組件、但仍會影響用戶在屏幕上所見內(nèi)容的進程闻鉴。殺死這類進程也會明顯影響用戶體驗茵乱。 如果一個進程滿足以下任一條件,即視為可見進程:

  • 托管不在前臺孟岛、但仍對用戶可見的 Activity(已調(diào)用其 onPause() 方法)瓶竭。例如,啟動了一個對話框樣式的前臺 activity 渠羞,此時在其后面仍然可以看到前一個Activity斤贰。

    運行時權(quán)限對話框就屬于此類。
    考慮一下次询,還有哪種情況會導(dǎo)致只觸發(fā)onPause而不觸發(fā)onStop?

  • 托管通過 Service.startForeground() 啟動的前臺Service荧恍。

    Service.startForeground():它要求系統(tǒng)將它視為用戶可察覺到的服務(wù),或者基本上對用戶是可見的屯吊。

  • 托管系統(tǒng)用于某個用戶可察覺的特定功能的Service块饺,比如動態(tài)壁紙、輸入法服務(wù)等等雌芽。

可見進程被視為是極其重要的進程,除非為了維持所有前臺進程同時運行而必須終止辨嗽,否則系統(tǒng)不會終止這些進程世落。如果這類進程被殺死,從用戶的角度看糟需,這意味著當(dāng)前 activity 背后的可見 activity 會被黑屏代替屉佳。

Service Process:服務(wù)進程(正常不會被殺死)

正在運行已使用 startService() 方法啟動的服務(wù)且不屬于上述兩個更高類別進程的進程。盡管服務(wù)進程與用戶所見內(nèi)容沒有直接關(guān)聯(lián)洲押,但是它們通常在執(zhí)行一些用戶關(guān)心的操作(例如武花,后臺網(wǎng)絡(luò)上傳或下載數(shù)據(jù))。因此杈帐,除非內(nèi)存不足以維持所有前臺進程和可見進程同時運行体箕,否則系統(tǒng)會讓服務(wù)進程保持運行狀態(tài)专钉。

已經(jīng)運行很久(例如30分鐘或更久)的Service,有可能被降級累铅,這樣一來它們所在的進程就可以被放入Cached LRU列表中跃须。這有助于避免一些長時間運行的Service由于內(nèi)存泄漏或其他問題而消耗過多的RAM,進而導(dǎo)致系統(tǒng)無法有效使用緩存進程的情況娃兽。

Background / Cached Process:后臺進程(可能隨時被殺死)

這類進程一般會持有一個或多個目前對用戶不可見的 Activity (已調(diào)用 Activity 的 onStop() 方法)菇民。它們不是當(dāng)前所必須的,因此當(dāng)其他更高優(yōu)先級的進程需要內(nèi)存時投储,系統(tǒng)可能隨時終止它們以回收內(nèi)存第练。但如果正確實現(xiàn)了Activity的生命周期,即便系統(tǒng)終止了進程玛荞,當(dāng)用戶再次返回應(yīng)用時也不會影響用戶體驗:關(guān)聯(lián)Activity在新的進程中被重新創(chuàng)建時可以恢復(fù)之前保存的狀態(tài)娇掏。

在一個正常運行的系統(tǒng)中,緩存進程是內(nèi)存管理中唯一涉及到的進程:一個運行良好的系統(tǒng)將始終具有多個緩存進程(為了更高效的切換應(yīng)用)冲泥,并根據(jù)需要定期終止最舊的進程驹碍。只有在非常嚴(yán)重(并且不可取)的情況下凡恍,系統(tǒng)才會到達這樣一個點志秃,此時所有的緩存進程都已被終止,并且必須開始終止服務(wù)進程嚼酝。

Android系統(tǒng)回收后臺進程的參考條件
LRU算法:自下而上開始終止浮还,先回收最老的進程。越老的進程近期內(nèi)被用戶再次使用的幾率越低闽巩。殺死的進程越老钧舌,對用戶體驗的影響就越小。
回收收益:系統(tǒng)總是傾向于殺死一個能回收更多內(nèi)存的進程涎跨,因為在它被殺時會為系統(tǒng)提供更多內(nèi)存增益洼冻,從而可以殺死更少的進程。殺死的進程越少隅很,對用戶體驗的影響就越小撞牢。換句話說,應(yīng)用進程在整個LRU列表中消耗的內(nèi)存越少叔营,保留在列表中并且能夠快速恢復(fù)的機會就越大屋彪。

這類進程會被保存在一個偽LRU列表中,系統(tǒng)會優(yōu)先殺死處于列表尾部(最老)的進程绒尊,以確保包含用戶最近查看的 Activity 的進程最后一個被終止畜挥。這個LRU列表排序的確切策略是平臺的實現(xiàn)細(xì)節(jié),但通常情況下婴谱,相對于其他類型的進程蟹但,系統(tǒng)會優(yōu)先嘗試保留更有用的進程(比如托管用戶主應(yīng)用程序的進程躯泰,或者托管用戶看到的最后一個Activity的進程,等等)矮湘。還有其他一些用于終止進程的策略:對允許的進程數(shù)量硬限制斟冕,對進程可以持續(xù)緩存的時間量的硬限制,等等缅阳。

在一個健康的系統(tǒng)中磕蛇,只有緩存進程或者空進程會被系統(tǒng)隨時終止,如果服務(wù)進程十办,或者更高優(yōu)先級的可見進程以及前臺進程也開始被系統(tǒng)終止(不包括應(yīng)用本身糟糕的內(nèi)存使用導(dǎo)致OOM)秀撇,那就說明系統(tǒng)運行已經(jīng)處于一個亞健康甚至極不健康的狀態(tài),可用內(nèi)存已經(jīng)吃緊向族。

Empty Process:空進程(可以隨時殺死)

不含任何活躍組件的進程呵燕。保留這種進程的的唯一目的是用作緩存(為了更加有效的使用內(nèi)存而不是完全釋放掉),以縮短下次啟動應(yīng)用程序所需的時間件相,因為啟動一個新的進程也是需要代價的再扭。只要有需要,Android會隨時殺死這些進程夜矗。

內(nèi)存管理中對于前臺/后臺應(yīng)用的定義泛范,與用于Service限制目的的后臺應(yīng)用定義不同。從Android 8.0開始紊撕,出于節(jié)省系統(tǒng)資源罢荡、優(yōu)化用戶體驗、提高電池續(xù)航能力的考量对扶,系統(tǒng)進行了前臺/后臺應(yīng)用的區(qū)分区赵,對于后臺service進行了一些限制。在該定義中浪南,如果滿足以下任意條件笼才,應(yīng)用將被視為處于前臺:

  • 具有可見 Activity(不管該 Activity 已啟動還是已暫停)。
  • 具有前臺 Service络凿。
  • 另一個前臺應(yīng)用已關(guān)聯(lián)到該應(yīng)用(不管是通過綁定到其中一個 Service患整,還是通過使用其中一個內(nèi)容提供程序)。 例如喷众,如果另一個應(yīng)用綁定到該應(yīng)用的 Service,那么該應(yīng)用處于前臺:
    IME
    壁紙 Service
    通知偵聽器
    語音或文本 Service
    如果以上條件均不滿足紧憾,應(yīng)用將被視為處于后臺到千。 詳見后臺Service限制

Android系統(tǒng)如何評定進程的優(yōu)先級

根據(jù)進程中當(dāng)前活動組件的重要程度,Android 會將進程評定為它可能達到的最高級別赴穗。例如憔四,如果某進程同時托管著 Service 和可見 Activity剃允,則會將此進程評定為可見進程辅斟,而不是服務(wù)進程。

此外,一個進程的級別可能會因其他進程對它的依賴而有所提高膏燕,即服務(wù)于另一進程的進程其級別永遠不會低于其所服務(wù)的進程。 例如胸嘴,如果進程 A 中的內(nèi)容提供程序為進程 B 中的客戶端提供服務(wù)券犁,或者如果進程 A 中的服務(wù)綁定到進程 B 中的組件,則進程 A 始終被視為至少與進程 B 同樣重要络断。

由于運行服務(wù)的進程其級別高于托管后臺 Activity 的進程裁替,因此,在 Activity 中啟動一個長時間運行的操作時貌笨,最好為該操作啟動服務(wù)弱判,而不是簡單地創(chuàng)建工作線程,當(dāng)操作有可能比 Activity 更加持久時尤要如此锥惋。例如昌腰,一個文件上傳的操作就可以考慮使用服務(wù)來完成,這樣一來膀跌,即使用戶退出 Activity遭商,仍可在后臺繼續(xù)執(zhí)行上傳操作。使用服務(wù)可以保證淹父,無論 Activity 發(fā)生什么情況株婴,該操作至少具備“服務(wù)進程”優(yōu)先級。 同理暑认, BroadcastReceiver 也應(yīng)使用服務(wù)困介,而不是簡單地將耗時冗長的操作放入線程中。

Home鍵退出和返回鍵退出的區(qū)別

Home鍵退出蘸际,程序保留狀態(tài)為后臺進程座哩;而返回鍵退出,程序保留狀態(tài)為空進程粮彤,空進程更容易被系統(tǒng)回收根穷。Home鍵其實主要用于進程間切換,返回鍵則是真正的退出程序导坟。

從理論上來講屿良,無論是哪種情況,在沒有任何后臺工作線程(即便應(yīng)用處于后臺惫周,工作線程仍然可以執(zhí)行)的前提下尘惧,被置于后臺的進程都只是保留他們的運行狀態(tài),并不會占用CPU資源递递,所以也不耗電喷橙。只有音樂播放軟件之類的應(yīng)用需要在后臺運行Service啥么,而Service是需要占用CPU時間的,此時才會耗電贰逾。所以說沒有帶后臺服務(wù)的應(yīng)用是不耗電也不占用CPU時間的悬荣,沒必要關(guān)閉,這種設(shè)計本身就是Android的優(yōu)勢之一疙剑,可以讓應(yīng)用下次啟動時更快氯迂。然而現(xiàn)實是,很多應(yīng)用多多少少都會有一些后臺工作線程核芽,這可能是開發(fā)人員經(jīng)驗不足導(dǎo)致(比如線程未關(guān)閉或者循環(huán)發(fā)送的Handler消息未停止)囚戚,也可能是為了需求而有意為之,導(dǎo)致整個Android應(yīng)用的生態(tài)環(huán)境并不是一片干凈轧简。

作為用戶驰坊,你需要手動管理內(nèi)存嗎?

你有“內(nèi)存使用率過高”恐慌癥嗎哮独?

無論是使用桌面操作系統(tǒng)還是移動操作系統(tǒng)拳芙,很多人都喜歡隨時關(guān)注內(nèi)存,一旦發(fā)現(xiàn)內(nèi)存使用率過高就難受皮璧,忍不住的要殺進程以釋放內(nèi)存舟扎。這種習(xí)慣很大程度上都是源自Windows系統(tǒng),當(dāng)然這在Windows下也確實沒錯悴务。然而很多人在使用Linux系統(tǒng)時仍然有這個習(xí)慣睹限,甚至到了Android系統(tǒng)下,也改不掉(尤其是Android手機剛出現(xiàn)的幾年)讯檐,Clean Master等各種清理軟件鋪天蓋地羡疗。毫不客氣的說,Windows毒害了不少人别洪!當(dāng)然叨恨,這也不能怪Windows,畢竟Windows的普及率太高了挖垛,而大部分普通用戶(甚至一些計算機相關(guān)人員)又不了解Windows和Linux在內(nèi)存管理方面的差別痒钝。

何時需要清理手機的RAM?

考慮到許多手機廠商都內(nèi)置了“清理”功能痢毒,那這個東西可能也有些道理送矩。事實上,關(guān)閉應(yīng)用程序以節(jié)省內(nèi)存的做法哪替,僅在少數(shù)情況下是值得嘗試的 —— 當(dāng)應(yīng)用崩潰或無法正常運行時益愈。比如以下情況:

  • 你的微信在啟動時需要加載很久
  • 某個應(yīng)用啟動時閃退或者運行過程中閃退
  • 系統(tǒng)響應(yīng)速度非常緩慢

這些癥狀可能非常多樣化,甚至原因不明的手機發(fā)熱也可能是由于某個崩潰的應(yīng)用造成的。

管理你的手機RAM:結(jié)論

究竟需不需要手動清空內(nèi)存蒸其?答案是:No!

清空內(nèi)存意味著你需要不斷重啟應(yīng)用库快,這需要花費時間和電量摸袁,甚至?xí)s短電池壽命。內(nèi)存占用高其實并非是一件壞事义屏,甚至是需要的靠汁。因為Android是基于Linux內(nèi)核的操作系統(tǒng),而Linux的內(nèi)存哲學(xué)是:

Free memory is wasted memory.

你只需要在手機明顯變慢時采取行動闽铐。一般來說蝶怔,系統(tǒng)的自動RAM管理才是最快最高效的,也是Android標(biāo)榜的優(yōu)勢之一兄墅。關(guān)閉應(yīng)用可能釋放一些內(nèi)存踢星,但卻對高效使用內(nèi)存毫無作用。

Leave the memory management to Android, and it will leave the fun to you.

參考資料

淺談Linux的內(nèi)存管理機制
Processes and Threads Overview
Process and Application Lifecycle
Overview of Memory Management
RAM management on Android: why you shouldn't clear memory

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末隙咸,一起剝皮案震驚了整個濱河市沐悦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌五督,老刑警劉巖藏否,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異充包,居然都是意外死亡副签,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門基矮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淆储,“玉大人,你說我怎么就攤上這事愈捅《艨迹” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵蓝谨,是天一觀的道長灌具。 經(jīng)常有香客問我,道長譬巫,這世上最難降的妖魔是什么咖楣? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮芦昔,結(jié)果婚禮上诱贿,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好珠十,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布料扰。 她就那樣靜靜地躺著,像睡著了一般焙蹭。 火紅的嫁衣襯著肌膚如雪晒杈。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天孔厉,我揣著相機與錄音拯钻,去河邊找鬼。 笑死撰豺,一個胖子當(dāng)著我的面吹牛粪般,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播污桦,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼亩歹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寡润?” 一聲冷哼從身側(cè)響起捆憎,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梭纹,沒想到半個月后躲惰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡变抽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年础拨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绍载。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡诡宗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出击儡,到底是詐尸還是另有隱情塔沃,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布阳谍,位于F島的核電站蛀柴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏矫夯。R本人自食惡果不足惜鸽疾,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望训貌。 院中可真熱鬧制肮,春花似錦冒窍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至儒飒,卻和暖如春意乓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背约素。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留笆凌,地道東北人圣猎。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像乞而,于是被迫代替她去往敵國和親送悔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

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