如何「偷」Android 的內(nèi)存?
之前在做一個(gè)內(nèi)存優(yōu)化的時(shí)候,使用到了MemoryFile,由此發(fā)現(xiàn)了MemoryFile的一些特性以及一個(gè)非常trickly的使用方法,因此在這里記錄一下。
What is it
MemoryFile是android在最開始就引入的一套框架,其內(nèi)部實(shí)際上是封裝了android特有的內(nèi)存共享機(jī)制Ashmem匿名共享內(nèi)存,簡(jiǎn)單來(lái)說(shuō),Ashmem在Android內(nèi)核中是被注冊(cè)成一個(gè)特殊的字符設(shè)備,Ashmem驅(qū)動(dòng)通過(guò)在內(nèi)核的一個(gè)自定義slab緩沖區(qū)中初始化一段內(nèi)存區(qū)域,然后通過(guò)mmap把申請(qǐng)的內(nèi)存映射到用戶的進(jìn)程空間中(通過(guò)tmpfs),這樣子就可以在用戶進(jìn)程中使用這里申請(qǐng)的內(nèi)存了,另外,Ashmem的一個(gè)特性就是可以在系統(tǒng)內(nèi)存不足的時(shí)候,回收掉被標(biāo)記為”unpin”的內(nèi)存,這個(gè)后面會(huì)講到,另外,MemoryFile也可以通過(guò)Binder跨進(jìn)程調(diào)用來(lái)讓兩個(gè)進(jìn)程共享一段內(nèi)存區(qū)域。由于整個(gè)申請(qǐng)內(nèi)存的過(guò)程并不再Java層上,可以很明顯的看出使用MemoryFile申請(qǐng)的內(nèi)存實(shí)際上是并不會(huì)占用Java堆內(nèi)存的。
MemoryFile暴露出來(lái)的用戶接口可以說(shuō)跟他的名字一樣,基本上跟我們平時(shí)的文件的讀寫基本一致,也可以使用InputStream和OutputStream來(lái)對(duì)其進(jìn)行讀寫等操作:
- MemoryFile memoryFile = new MemoryFile(null, inputStream.available());
- memoryFile.allowPurging(false);
- OutputStream outputStream = memoryFile.getOutputStream();
- outputStream.write(1024);
上面可以看到allowPurging這個(gè)調(diào)用,這個(gè)就是之前說(shuō)的”pin”和”unpin”,在設(shè)置了allowPurging為false之后,這個(gè)MemoryFile對(duì)應(yīng)的Ashmem就會(huì)被標(biāo)記成”pin”,那么即使在android系統(tǒng)內(nèi)存不足的時(shí)候,也不會(huì)對(duì)這段內(nèi)存進(jìn)行回收。另外,由于Ashmem默認(rèn)都是”unpin”的,因此申請(qǐng)的內(nèi)存在某個(gè)時(shí)間點(diǎn)內(nèi)都可能會(huì)被回收掉,這個(gè)時(shí)候是不可以再讀寫了
Tricks
MemoryFile是一個(gè)非常trickly的東西,由于并不占用Java堆內(nèi)存,我們可以將一些對(duì)象用MemoryFile來(lái)保存起來(lái)避免GC,另外,這里可能android上有個(gè)BUG:
在4.4及其以上的系統(tǒng)中,如果在應(yīng)用中使用了MemoryFile,那么在dumpsys meminfo的時(shí)候,可以看到多了一項(xiàng)Ashmem的值:
可以看出來(lái)雖然MemoryFile申請(qǐng)的內(nèi)存不計(jì)入Java堆也不計(jì)入Native堆中,但是占用了Ashmem的內(nèi)存,這個(gè)實(shí)際上是算入了app當(dāng)前占用的內(nèi)存當(dāng)中
但是在4.4以下的機(jī)器中時(shí),使用MemoryFile申請(qǐng)的內(nèi)存居然是不算入app的內(nèi)存中的:

而且這里我也算過(guò),也是不算入Native Heap中的,另外,這個(gè)時(shí)候去系統(tǒng)設(shè)置里面看進(jìn)程的內(nèi)存占用,也可以看出來(lái)其實(shí)并沒有計(jì)入Ashmem的內(nèi)存的
這個(gè)應(yīng)該是android的一個(gè)BUG,但是我搜了一下并沒有搜到對(duì)應(yīng)的issue,搞不好這里也可能是一個(gè)feature
而在大名鼎鼎的Fresco當(dāng)中,他們也有用到這個(gè)bug來(lái)避免在decode bitmap的時(shí)候,將文件的字節(jié)讀到Java堆中,使用了MemoryFile,并利用了這個(gè)BUG然這部分內(nèi)存不算入app中,這里分別對(duì)應(yīng)了Fresco中的GingerbreadPurgeableDecoder(https://github.com/facebook/fresco/blob/master/imagepipeline/src/main/java/com/facebook/imagepipeline/platform/GingerbreadPurgeableDecoder.java)和KitKatPurgeableDecoder(https://github.com/facebook/fresco/blob/master/imagepipeline/src/main/java/com/facebook/imagepipeline/platform/KitKatPurgeableDecoder.java),F(xiàn)resco在decode圖片的時(shí)候會(huì)在4.4和4.4以下的系統(tǒng)中分別使用這兩個(gè)不同的decoder
從這個(gè)地方可以看出來(lái),使用MemoryFile,在4.4以下的系統(tǒng)當(dāng)中,可以幫我們的app額外”偷”一些內(nèi)存,并且可以不計(jì)入app的內(nèi)存當(dāng)中
Summary
這里主要是簡(jiǎn)單介紹了MemoryFile的基本原理和用法,并且闡述了一個(gè)MemoryFile中一個(gè)可以幫助開發(fā)者”偷”內(nèi)存的地方,這個(gè)是一個(gè)非常trickly的方法,雖然4.4以下使用這塊的內(nèi)存并不計(jì)入進(jìn)程當(dāng)中,但是并不推薦大量使用,因?yàn)楫?dāng)設(shè)置了allowPurging為false的時(shí)候,這個(gè)對(duì)應(yīng)的Ashmem內(nèi)存區(qū)域是被”pin”了,那么在android系統(tǒng)內(nèi)存不足的時(shí)候,是不能夠把這段內(nèi)存區(qū)域回收的,如果長(zhǎng)時(shí)間沒有釋放的話,這樣子相當(dāng)于無(wú)端端占用了大量手機(jī)內(nèi)存而又無(wú)法回收,那對(duì)系統(tǒng)的穩(wěn)定性肯定會(huì)造成影響
References
1. Android系統(tǒng)匿名共享內(nèi)存Ashmem(Anonymous Shared Memory)驅(qū)動(dòng)程序源代碼分析
http://blog.csdn.net/luoshengyang/article/details/6664554
2. Android Kernel Features(Ashmem)
http://elinux.org/Android_Kernel_Features#ashmem