- IO 優(yōu)化不就是不在主線程讀寫大文件嗎,真的只有這么簡(jiǎn)單嗎?
IO 基礎(chǔ)
- IO流程:應(yīng)用程序 發(fā)送邏輯IO命令給文件系統(tǒng),文件系統(tǒng)發(fā)送物理IO命令給存儲(chǔ)設(shè)備/磁盤
文件系統(tǒng)
- 文件讀(read)過程:應(yīng)用程序調(diào)用read() 方法,系統(tǒng)會(huì)通過中斷從用戶空間進(jìn)入內(nèi)核處理流程,然后經(jīng)過 VFS、具體文件系統(tǒng)、頁緩存,如果數(shù)據(jù)沒有在頁緩存中,就需要真正向磁盤發(fā)起I/O請(qǐng)求
- 文件系統(tǒng):存儲(chǔ)和組織數(shù)據(jù)的方式,如iOS的HFS+,APFS(Apple File System,iOS 10.3+), Android的ext4(Linux常用),F(xiàn)2FS(Flash-Friendly File System);
可以在 /proc/filesystems 看到系統(tǒng)可以識(shí)別的所有文件系統(tǒng)的列表
- 虛擬文件系統(tǒng)(VFS):屏蔽具體的文件系統(tǒng),為應(yīng)用程序的操作提供統(tǒng)一的接口;
- 頁緩存(Page Cache): 文件系統(tǒng)對(duì)數(shù)據(jù)的緩存,目的是提升內(nèi)存命中率;
- Buffer Cache : 磁盤對(duì)數(shù)據(jù)的緩存,目的是合并部分文件系統(tǒng)的 I/O 請(qǐng)求、降低磁盤 I/O 的次數(shù), 后來它也合并到 Page Cache 中的 Buffer Page 了;
通過 /proc/meminfo 文件可以查看緩存的內(nèi)存占用情況
- 當(dāng)手機(jī)內(nèi)存不足時(shí),系統(tǒng)會(huì)回收它們的內(nèi)存,這樣整體 I/O 的性能就會(huì)有所降低。
磁盤
- 磁盤:系統(tǒng)的存儲(chǔ)設(shè)備,如CD, 機(jī)械硬盤, SSD 固態(tài)硬盤;
- 磁盤IO過程:先經(jīng)過內(nèi)核的通用塊層、I/O 調(diào)度層、設(shè)備驅(qū)動(dòng)層,最后交給具體的硬件設(shè)備處理;
- 塊設(shè)備:系統(tǒng)中能夠隨機(jī)訪問固定大小數(shù)據(jù)塊(block)的設(shè)備,CD、硬盤、SSD都屬于塊設(shè)備;
- 通用塊層:主要作用是接收上層發(fā)出的磁盤請(qǐng)求,并最終發(fā)出 I/O 請(qǐng)求,讓上層不需要關(guān)心底層硬件設(shè)備的具體實(shí)現(xiàn)。
- I/O 調(diào)度層:根據(jù)設(shè)置的調(diào)度算法對(duì)請(qǐng)求合并和排序
I/O 調(diào)度層 關(guān)鍵參數(shù):
/sys/block/[disk]/queue/nr_requests // 隊(duì)列長(zhǎng)度,一般是 128。
/sys/block/[disk]/queue/scheduler // 調(diào)度算法
- 塊設(shè)備驅(qū)動(dòng)層: 據(jù)具體的物理設(shè)備,選擇對(duì)應(yīng)的驅(qū)動(dòng)程序通過操控硬件設(shè)備完成最終的 I/O 請(qǐng)求。如光盤的激光燒錄,閃存的電子擦寫;
Android I/O
Android 閃存(ROM)
- Android前幾年的eMMC 標(biāo)準(zhǔn),近幾年的UFS 2.0/2.1 標(biāo)準(zhǔn),iOS和MacOS的NVMe 協(xié)議
- 閃存性能不僅僅由硬件決定,它跟采用的標(biāo)準(zhǔn)、文件系統(tǒng)的實(shí)現(xiàn)也有很大的關(guān)系
文件為什么會(huì)損壞?
- 格式錯(cuò)誤或內(nèi)容丟失,如SQLite大概有幾萬分之一的損壞率,SharedPreference 頻繁跨進(jìn)程讀寫也會(huì)有萬分之一的損壞率;
- 從應(yīng)用程序、文件系統(tǒng)和磁盤三個(gè)角度來審視:
1. 磁盤。手機(jī)上使用的閃存是電子式的存儲(chǔ)設(shè)備,所以在資料傳輸過程可能會(huì)發(fā)生電子遺失
等現(xiàn)象導(dǎo)致數(shù)據(jù)錯(cuò)誤。不過閃存也會(huì)使用 ECC、多級(jí)編碼等多種方式增加數(shù)據(jù)的可靠性,一
般來說出現(xiàn)這種情況的可能性也比較小。
閃存壽命也可能會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)誤,由于閃存的內(nèi)部結(jié)構(gòu)和特征,導(dǎo)致它寫過的地址必須擦除才
能再次寫入,而每個(gè)塊擦除又有次數(shù)限制,次數(shù)限制是根據(jù)采用的存儲(chǔ)顆粒,從十萬次到幾千
都有(SLC>MLC>TLC)
2. 文件系統(tǒng)。雖說內(nèi)核崩潰或者系統(tǒng)突然斷電都有可能導(dǎo)致文件系統(tǒng)損壞,文件系統(tǒng)把數(shù)據(jù)
寫入到 Page Cache 中,然后等待合適的時(shí)機(jī)才會(huì)真正的寫入磁盤.不過文件系統(tǒng)也做了很
多的保護(hù)措施。例如 system 分區(qū)保證只讀不可寫,增加異常檢查和恢復(fù)機(jī)制,ext4 的
fsck、f2fs 的 fsck.f2fs 和 checkpoint 機(jī)制等。
3. 應(yīng)用程序。大部分的 I/O 方法都不是原子操作,文件的跨進(jìn)程或者多線程寫入、使用一
個(gè)已經(jīng)關(guān)閉的文件描述符 fd 來操作文件,它們都有可能導(dǎo)致數(shù)據(jù)被覆蓋或者刪除。事實(shí)上,
大部分的文件損壞都是因?yàn)閼?yīng)用程序代碼設(shè)計(jì)考慮不當(dāng)導(dǎo)致的,并不是文件系統(tǒng)或者磁盤的問題。
I/O 有時(shí)候?yàn)槭裁磿?huì)突然很慢?
- 內(nèi)存不足:內(nèi)存不足的時(shí)候,系統(tǒng)會(huì)回收 Page Cache 和 Buffer Cache 的內(nèi)存,大部分的寫操作會(huì)直接落盤,導(dǎo)致性能低下;
- 寫入放大:閃存重復(fù)寫入需要先進(jìn)行擦除,擦除操作的基本單元是 block 塊,一個(gè) page 頁的寫入操作將會(huì)引起整個(gè)塊數(shù)據(jù)的遷移,這就是典型的寫入放大現(xiàn)象,低端機(jī)或者使用比較久的設(shè)備,由于磁盤碎片多、剩余空間少,非常容易出現(xiàn)寫入放大的現(xiàn)象。
- 配置不夠:低端機(jī)的 CPU 和閃存的性能相對(duì)也較差,在高負(fù)載的情況下容易出現(xiàn)瓶頸。
I/O 的性能評(píng)估
- 整個(gè)IO流程:應(yīng)用程序-->系統(tǒng)調(diào)用-->虛擬文件系統(tǒng)-->文件系統(tǒng)-->塊設(shè)備接口-->驅(qū)動(dòng)程序-->磁盤
- I/O 性能指標(biāo): 吞吐量 和 IOPS
- 磁盤吞吐量:每秒磁盤I/O的流量,即磁盤寫入加上讀出的數(shù)據(jù)的大小。
- 存儲(chǔ)IOPS:磁盤IOPS是指一秒內(nèi)磁盤進(jìn)行多少次I/O讀寫;
- I/O 測(cè)量:
- 使用 proc 跟蹤 I/O 的等待時(shí)間和次數(shù)來衡量
proc/self/schedstat:
se.statistics.iowait_count:IO 等待的次數(shù)
se.statistics.iowait_sum: IO 等待的時(shí)間
//如果是 root 的機(jī)器,我們可以開啟內(nèi)核的 I/O 監(jiān)控,將所有 block 讀寫 dump 到日志文件中,這樣可以通過 dmesg 命令來查看。
echo 1 > /proc/sys/vm/block_dump
dmesg -c grep pid
.sample.io.test(7540): READ block 29262592 on dm-1 (256 sectors)
.sample.io.test(7540): READ block 29262848 on dm-1 (256 sectors)
- 使用 strace 跟蹤 I/O 相關(guān)的系統(tǒng)調(diào)用次數(shù)和耗時(shí)
strace -ttT -f -p [pid]
read(53, "*****************"\.\.\., 1024) = 1024 <0.000447>
read(53, "*****************"\.\.\., 1024) = 1024 <0.000084>
read(53, "*****************"\.\.\., 1024) = 1024 <0.000059>
//也可以通過 strace 統(tǒng)計(jì)一段時(shí)間內(nèi)所有系統(tǒng)調(diào)用的耗時(shí)概況。不過 strace 本身也會(huì)消耗不少資源,對(duì)執(zhí)行時(shí)間也會(huì)產(chǎn)生影響。
strace -c -f -p [pid]
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
97.56 0.041002 21 1987 read
1.44 0.000605 55 11 write
- 使用 vmstat
//其中 Memory 中的 buff 和 cache,I/O 中的 bi 和 bo,System 中的 cs,以及 CPU 中的 sy 和 wa,這些字段的數(shù)值都與 I/O 行為有關(guān)。
//我們可以配合dd 命令來配合測(cè)試,觀察 vmstat 的輸出數(shù)據(jù)變化。不過需要注意的是 Android 里面的 dd 命令似乎并不支持 conv 和 flag 參數(shù)
//清除Buffer和Cache內(nèi)存緩存
echo 3 > /proc/sys/vm/drop_caches
//每隔1秒輸出1組vmstat數(shù)據(jù)
vmstat 1
//測(cè)試寫入速度,寫入文件/data/data/test,buffer大小為4K,次數(shù)為1000次
dd if=/dev/zero of=/data/data/test bs=4k count=1000
IO的三種方式
1. 標(biāo)準(zhǔn)IO
- 程序中平時(shí)用到 read/write 操作都屬于標(biāo)準(zhǔn) I/O,也就是緩存 I/O(Buffered I/O)
- 緩存 I/O 可以很大程度減少真正讀寫磁盤的次數(shù),從而提升性能,但延遲寫機(jī)制可能會(huì)導(dǎo)致數(shù)據(jù)丟失;
- Page Cache 中被修改的內(nèi)存稱為“臟頁”,內(nèi)核通過 flush 線程定期將數(shù)據(jù)寫入磁盤。
//具體寫入的條件我們可以通過 /proc/sys/vm 文件或者 sysctl -a | grep vm 命令得到
// flush每隔5秒執(zhí)行一次
vm.dirty_writeback_centisecs = 500
// 內(nèi)存中駐留30秒以上的臟數(shù)據(jù)將由flush在下一次執(zhí)行時(shí)寫入磁盤
vm.dirty_expire_centisecs = 3000
// 指示若臟頁占總物理內(nèi)存10%以上,則觸發(fā)flush把臟數(shù)據(jù)寫回磁盤
vm.dirty_background_ratio = 10
// 系統(tǒng)所能擁有的最大臟頁緩存的總大小
vm.dirty_ratio = 20
- 在實(shí)際應(yīng)用中,如果某些數(shù)據(jù)我們覺得非常重要,是完全不允許有丟失風(fēng)險(xiǎn)的,這個(gè)時(shí)候我們應(yīng)該采用同步寫機(jī)制。
- 在應(yīng)用程序中使用 sync、fsync、msync 等系統(tǒng)調(diào)用時(shí),內(nèi)核都會(huì)立刻將相應(yīng)的數(shù)據(jù)寫回到磁盤。
2. 直接 I/O
- 很多數(shù)據(jù)庫自己已經(jīng)做了數(shù)據(jù)和索引的緩存管理,對(duì)頁緩存的依賴反而沒那么強(qiáng)烈。它們希望可以繞開頁緩存機(jī)制,這樣可以減少一次數(shù)據(jù)拷貝,這些數(shù)據(jù)也不會(huì)污染頁緩存。
- 讀/寫均為同步執(zhí)行,容易導(dǎo)致程序等待
- 只有在確定緩沖IO開銷非常巨大時(shí)才考慮直接IO
3. mmap
- Android系統(tǒng)啟動(dòng)加載dex時(shí),不會(huì)把整個(gè)文件一次性讀到內(nèi)存,而是采用mmap();
- 通過把文件映射到進(jìn)程的地址空間(用戶緩沖區(qū)與物理內(nèi)存(頁緩存)共享數(shù)據(jù),)
- 優(yōu)點(diǎn):
- 減少系統(tǒng)調(diào)用:只需一次mmap()系統(tǒng)調(diào)用,后續(xù)所有調(diào)用就像操作內(nèi)存一樣,不會(huì)出現(xiàn)大量read/write系統(tǒng)調(diào)用
- 減少數(shù)據(jù)拷貝:普通read需要兩尺拷貝(磁盤to頁緩存,頁緩存to用戶緩沖區(qū)),mmap只需要將磁盤數(shù)據(jù)拷貝到頁緩存;
- 可靠性高:mmap把數(shù)據(jù)寫入頁緩存后,跟緩存IO的延遲寫機(jī)制一樣,可以依靠?jī)?nèi)核線程定期寫回磁盤;
- 適合對(duì)同一塊區(qū)域頻繁讀寫的情況,如用戶日志,數(shù)據(jù)上報(bào)
- 跨進(jìn)程同步時(shí)mmap也是個(gè)很好的選擇,Android中的binder機(jī)制內(nèi)部也是使用mmap實(shí)現(xiàn)
- 缺點(diǎn):
- mmap在內(nèi)核崩潰,突然斷電等情況下也可能引起內(nèi)容丟失,也可以使用msync來強(qiáng)制同步寫;
- 虛擬內(nèi)存增大:應(yīng)用可用的需內(nèi)內(nèi)存空間有限,mmap一個(gè)大文件容易出現(xiàn)虛擬內(nèi)存不足導(dǎo)致的OOM;
- 磁盤延遲:mmap通過缺頁中斷向磁盤發(fā)起真正的磁盤IO,所以如果當(dāng)前問題在于磁盤IO的高延遲,那么mmap消除小小的系統(tǒng)調(diào)用開銷真是杯水車薪;之前講過的類重排技術(shù)主要就是為了減少缺頁中斷造成的磁盤IO延遲;
- 低端機(jī)或系統(tǒng)資源嚴(yán)重不足時(shí),mmap也會(huì)出現(xiàn)頻繁寫入磁盤,性能快速下降;
多線程阻塞IO和NIO
多線程阻塞IO
- IO操作可能很慢,所以應(yīng)該盡量放到線程中;
- 文件讀寫收到IO性能瓶頸的影響,到達(dá)一定速度后整體性能就會(huì)收到明顯影響,過多的線程反而會(huì)導(dǎo)致應(yīng)用整體性能的下降
- 合理使用多線程可以減少IO等待,太多的線程阻塞導(dǎo)致線程切換頻繁,增大系統(tǒng)上下文切換的開銷;
- 實(shí)際工作開發(fā)中大部分時(shí)候都是讀一些比較小的文件,使用單獨(dú)的IO線程還是專門新開一個(gè)線程,其實(shí)差別不大;
NIO
- 使用異步IO,將IO請(qǐng)求發(fā)送給系統(tǒng)后,繼續(xù)往下執(zhí)行,將IO以事件的方式通知,減少線程切換的開銷;
- 缺點(diǎn):應(yīng)用程序的實(shí)現(xiàn)變得更復(fù)雜,有時(shí)異步改造并不容易
- 作用:最大作用不是減少讀取文件的耗時(shí),而是最大化提升應(yīng)員工整體的CPU利用率;(將線程等待磁盤IO的時(shí)間用來處理cpu的其他任務(wù))
- 推薦使用Square 的Okio,支持同步和異步 I/O;使用demo如下:
//Okio中有兩個(gè)關(guān)鍵的接口,Sink和Source,這兩個(gè)接口都繼承了Closeable接口;
//而Sink可以簡(jiǎn)單的看做OutputStream,Source可以簡(jiǎn)單的看做InputStream。
//而這兩個(gè)接口都是支持讀寫超時(shí)設(shè)置的
//1. BufferedSink中定義了一系列寫入緩存區(qū)的方法
BufferedSink write(byte[] source) 將字符數(shù)組source 寫入
BufferedSink write(byte[] source, int offset, int byteCount) 將字符數(shù)組的從offset開始的byteCount個(gè)字符寫入
BufferedSink write(ByteString byteString) 將字符串寫入
BufferedSink write(Source source, long byteCount) 從Source寫入byteCount個(gè)長(zhǎng)度的
long writeAll(Source source) 將Source中的所有數(shù)據(jù)寫入
BufferedSink writeByte(int b) 寫入一個(gè)byte整型
BufferedSink writeDecimalLong(long v) 寫入一個(gè)十進(jìn)制的長(zhǎng)整型
BufferedSink writeHexadecimalUnsignedLong(long v) 寫入一個(gè)十六進(jìn)制無符號(hào)的長(zhǎng)整型
BufferedSink writeInt(int i) 寫入一個(gè)整型
BufferedSink writeIntLe(int i)
BufferedSink writeLong(long v) 寫入一個(gè)長(zhǎng)整型
BufferedSink writeLongLe(long v)
BufferedSink writeShort(int s) 寫入一個(gè)短整型
BufferedSink writeShortLe(int s)
BufferedSink writeString(String string, Charset charset) 寫入一個(gè)String,并以charset格式編碼
BufferedSink writeString(String string, int beginIndex, int endIndex, Charset charset) 將String中從beginIndex到endIndex寫入,并以charset格式編碼
BufferedSink writeUtf8(String string) 將String 以Utf - 8編碼形式寫入
BufferedSink writeUtf8(String string, int beginIndex, int endIndex) 將String中從beginIndex到endIndex寫入,并以Utf - 8格式編碼
BufferedSink writeUtf8CodePoint(int codePoint) 以Utf - 8編碼形式寫入的節(jié)點(diǎn)長(zhǎng)度
//2. BufferedSource定義的方法和BufferedSink極為相似,只不過一個(gè)是寫一個(gè)是讀
BufferedSource read(byte[] sink) 將緩沖區(qū)中讀取字符數(shù)組sink 至sink
BufferedSource read(byte[] sink, int offset, int byteCount) 將緩沖區(qū)中從offst開始讀取byteCount個(gè)字符 至sink
BufferedSource readAll(Sink sink) 讀取所有的Sink
BufferedSource readByte() 從緩沖區(qū)中讀取一個(gè)字符
BufferedSource readByteArray() 從緩沖區(qū)中讀取一個(gè)字符數(shù)組
BufferedSource readByteArray(long byteCount) 從緩沖區(qū)中讀取一個(gè)長(zhǎng)度為byteCount的字符數(shù)組
BufferedSource readByteString() 將緩沖區(qū)全部讀取為字符串
BufferedSource readByteString(long byteCount) 將緩沖區(qū)讀取長(zhǎng)度為byteCount的字符串
BufferedSource readDecimalLong() 讀取十進(jìn)制數(shù)長(zhǎng)度
BufferedSource readFully(Buffer sink, long byteCount) 讀取byteCount個(gè)字符至sink
BufferedSource readFully(byte[] sink) 讀取所有字符至sink
BufferedSource readHexadecimalUnsignedLong() 讀取十六進(jìn)制數(shù)長(zhǎng)度
BufferedSource readInt() 從緩沖區(qū)中讀取一個(gè)整數(shù)
BufferedSource readIntLe()
BufferedSource readLong() 從緩沖區(qū)中讀取Long 整數(shù)
BufferedSource readLongLe()
BufferedSource readShort() 從緩沖區(qū)中讀取一個(gè)短整形
BufferedSource readShortLe()
BufferedSource readString(Charset charset) 從緩沖區(qū)中讀取一個(gè)String
BufferedSource readString(long byteCount, Charset charset) 讀取一個(gè)長(zhǎng)度為byteCount的String,并以charset形式編碼
BufferedSource readUtf8() 讀取編碼格式為Utf-8的String
BufferedSource readUtf8(long byteCount) 讀取編碼格式為Utf-8且長(zhǎng)度為byteCount的String
BufferedSource readUtf8CodePoint() 讀取一個(gè)Utf-8編碼節(jié)點(diǎn),長(zhǎng)度在1-4之間
BufferedSource readUtf8Line() 讀取一行Utf-8 編碼的String,碰到換行時(shí)停止
BufferedSource readUtf8LineStrict()
//3. ByteString: 作為一個(gè)工具類,功能十分強(qiáng)大,它可以把byte轉(zhuǎn)為String,這個(gè)String可以是utf8的值,也可以是base64后的值,也可以是md5的值,也可以是sha256的值
String base64()
String base64Url()
String utf8()
ByteString sha1()
ByteString sha256()
static ByteString decodeBase64(String base64)
static ByteString decodeHex(String hex)
static ByteString encodeUtf8(String s)
//4. 讀寫使用
/**
* @Author: LiuJinYang
* @CreateDate: 2020/12/23
*/
public class OkioDemo {
public static void main(String[] args) {
testWrite();
testRead();
testGzip();
}
private static void testWrite() {
String fileName = "tea.txt";
boolean isCreate;
Sink sink;
BufferedSink bufferedSink = null;
String path = Environment.getExternalStorageDirectory().getPath();
try {
File file = new File(path, fileName);
if (!file.exists()) {
isCreate = file.createNewFile();
} else {
isCreate = true;
}
if (isCreate) {
sink = Okio.sink(file);
bufferedSink = Okio.buffer(sink);
bufferedSink.writeInt(90002);
bufferedSink.writeString("asdfasdf", Charset.forName("GBK"));
bufferedSink.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != bufferedSink) {
bufferedSink.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void testRead() {
String fileName = "tea.txt";
Source source;
BufferedSource bufferedSource = null;
try {
String path = Environment.getExternalStorageDirectory().getPath();
File file = new File(path, fileName);
source = Okio.source(file);
bufferedSource = Okio.buffer(source);
String read = bufferedSource.readString(Charset.forName("GBK"));
LjyLogUtil.d(read);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != bufferedSource) {
bufferedSource.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 或許有時(shí)候網(wǎng)絡(luò)請(qǐng)求中,我們需要使用到Gzip的功能
*/
private static void testGzip() {
Sink sink;
BufferedSink bufferedSink = null;
GzipSink gzipSink;
try {
File dest = new File("resources/gzip.txt");
sink = Okio.sink(dest);
gzipSink = new GzipSink(sink);
bufferedSink = Okio.buffer(gzipSink);
bufferedSink.writeUtf8("android vs ios");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeQuietly(bufferedSink);
}
Source source;
BufferedSource bufferedSource = null;
GzipSource gzipSource;
try {
File file = new File("resources/gzip.txt");
source = Okio.source(file);
gzipSource = new GzipSource(source);
bufferedSource = Okio.buffer(gzipSource);
String content = bufferedSource.readUtf8();
System.out.println(content);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeQuietly(bufferedSource);
}
}
public static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
}
小文件系統(tǒng)
- 對(duì)于文件系統(tǒng)來說,目錄查找的性能是非常重要的
- 文件讀取的時(shí)間 = 找到文件的 inode 的時(shí)間 + 根據(jù) inode 讀取文件數(shù)據(jù)的時(shí)間,如果我們需要頻繁讀寫幾萬個(gè)小文件,查找 inode 的時(shí)間會(huì)變得非常可觀;
- Google 的 GFS、淘寶開源的TFS、Facebook 的 Haystack ,微信的 SFS 都是專門為海量小文件的存儲(chǔ)和檢索設(shè)計(jì)的文件系統(tǒng);要支持 VFS 接口,這樣上層的 I/O 操作代碼并不需要改動(dòng);
- 大量的小文件合并為大文件后,我們還可以將能連續(xù)訪問的小文件合并存儲(chǔ),將原本小文件間的隨機(jī)訪問變?yōu)榱隧樞蛟L問,可以大大提高性能。同時(shí)合并存儲(chǔ)能夠有效減少小文件存儲(chǔ)時(shí)所產(chǎn)生的磁盤碎片問題,提高磁盤的利用率。
I/O 跟蹤
1. Java Hook
- FileInputStream 的整個(gè)調(diào)用流程:
java : FileInputStream -> IoBridge.open -> Libcore.os.open -> BlockGuardOs.open -> Posix.open
/1./在Libcore.java中可以找到一個(gè)挺不錯(cuò)的 Hook 點(diǎn),那就是BlockGuardOs這一個(gè)靜態(tài)變量
public static Os os = new BlockGuardOs(new Posix());
// 反射獲得靜態(tài)變量
Class<?> clibcore = Class.forName("libcore.io.Libcore");
Field fos = clibcore.getDeclaredField("os");
//2.可以通過動(dòng)態(tài)代理的方式,在所有 I/O 相關(guān)方法前后加入插樁代碼,統(tǒng)計(jì) I/O 操作相關(guān)的信息
// 動(dòng)態(tài)代理對(duì)象
Proxy.newProxyInstance(cPosix.getClassLoader(), getAllInterfaces(cPosix), this);
beforeInvoke(method, args, throwable);
result = method.invoke(mPosixOs, args);
afterInvoke(method, args, result);
- 缺點(diǎn):
- 性能極差:因?yàn)槭褂脛?dòng)態(tài)代理和 Java 的大量字符串操作
- 無法監(jiān)控 Native 代碼
- 兼容性差: 特別是 Android P 增加對(duì)非公開 API 限制
2. Native Hook
- Profilo 使用到是 PLT Hook 方案,性能比GOT Hook要稍好一些,不過 GOT Hook 的兼容性會(huì)更好一些
- 最終是從 libc.so 中的這幾個(gè)函數(shù)中選定 Hook 的目標(biāo)函數(shù)
int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t size);
ssize_t write(int fd, const void *buf, size_t size); write_cuk
int close(int fd);
- 需要選擇一些有調(diào)用上面幾個(gè)方法的 library。微信 Matrix 中選擇的是libjavacore.so、libopenjdkjvm.so、libopenjdkjvm.so,可以覆蓋到所有的 Java 層的 I/O 調(diào)用,具體可以參考io_canary_jni.cc
- 不過更推薦 Profilo 中atrace.cpp的做法,它直接遍歷所有已經(jīng)加載的 library,一并替換。
void hookLoadedLibs() {
auto& functionHooks = getFunctionHooks();
auto& seenLibs = getSeenLibs();
facebook::profilo::hooks::hookLoadedLibs(functionHooks, seenLibs);
}
Matrix使用
- Matrix-android 當(dāng)前監(jiān)控范圍包括:應(yīng)用安裝包大小,幀率變化,啟動(dòng)耗時(shí),卡頓,慢方法,SQLite 操作優(yōu)化,文件讀寫,內(nèi)存泄漏等等。
# 1. gradle.properties 中配置要依賴的 Matrix 版本號(hào)
MATRIX_VERSION=0.6.6
//2. 在你項(xiàng)目根目錄下的 build.gradle 文件添加 Matrix 依賴
classpath ("com.tencent.matrix:matrix-gradle-plugin:${MATRIX_VERSION}") { changing = true }
//3.1 添加matrix-plugin
apply plugin: 'com.tencent.matrix-plugin'
//3.2
matrix {
trace {
enable = true //if you don't want to use trace canary, set false
baseMethodMapFile = "${project.buildDir}/matrix_output/Debug.methodmap"
blackListFile = "${project.projectDir}/matrixTrace/blackMethodList.txt"
}
}
//3.3 在 app/build.gradle 文件中添加 Matrix 各模塊的依賴
implementation group: "com.tencent.matrix", name: "matrix-android-lib", version: MATRIX_VERSION, changing: true
implementation group: "com.tencent.matrix", name: "matrix-android-commons", version: MATRIX_VERSION, changing: true
implementation group: "com.tencent.matrix", name: "matrix-trace-canary", version: MATRIX_VERSION, changing: true
implementation group: "com.tencent.matrix", name: "matrix-resource-canary-android", version: MATRIX_VERSION, changing: true
implementation group: "com.tencent.matrix", name: "matrix-resource-canary-common", version: MATRIX_VERSION, changing: true
implementation group: "com.tencent.matrix", name: "matrix-io-canary", version: MATRIX_VERSION, changing: true
implementation group: "com.tencent.matrix", name: "matrix-sqlite-lint-android-sdk", version: MATRIX_VERSION, changing: true
/**
* 4. 實(shí)現(xiàn) PluginListener,接收 Matrix 處理后的數(shù)據(jù)
*/
public class TestPluginListener extends DefaultPluginListener {
public static final String TAG = "Matrix.TestPluginListener";
public TestPluginListener(Context context) {
super(context);
}
@Override
public void onReportIssue(Issue issue) {
super.onReportIssue(issue);
MatrixLog.e(TAG, issue.toString());
//add your code to process data
}
}
/**
* 5. 實(shí)現(xiàn)動(dòng)態(tài)配置接口,可修改 Matrix 內(nèi)部參數(shù), 其中參數(shù)對(duì)應(yīng)的 key 位于文件 MatrixEnum中
*/
public class DynamicConfigImplDemo implements IDynamicConfig {
private static final String TAG = "Matrix.DynamicConfigImplDemo";
public DynamicConfigImplDemo() {
}
public boolean isFPSEnable() {
return true;
}
public boolean isTraceEnable() {
return true;
}
public boolean isMatrixEnable() {
return true;
}
@Override
public String get(String key, String defStr) {
//TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
return defStr;
}
@Override
public int get(String key, int defInt) {
//TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
if (MatrixEnum.clicfg_matrix_resource_max_detect_times.name().equals(key)) {
MatrixLog.i(TAG, "key:" + key + ", before change:" + defInt + ", after change, value:" + 2);
return 2;//new value
}
if (MatrixEnum.clicfg_matrix_trace_fps_report_threshold.name().equals(key)) {
return 10000;
}
if (MatrixEnum.clicfg_matrix_trace_fps_time_slice.name().equals(key)) {
return 12000;
}
return defInt;
}
@Override
public long get(String key, long defLong) {
//TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
if (MatrixEnum.clicfg_matrix_trace_fps_report_threshold.name().equals(key)) {
return 10000L;
}
if (MatrixEnum.clicfg_matrix_resource_detect_interval_millis.name().equals(key)) {
MatrixLog.i(TAG, key + ", before change:" + defLong + ", after change, value:" + 2000);
return 2000;
}
return defLong;
}
@Override
public boolean get(String key, boolean defBool) {
//TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
return defBool;
}
@Override
public float get(String key, float defFloat) {
//TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
return defFloat;
}
}
/**
* 6. 選擇程序啟動(dòng)的位置對(duì) Matrix 進(jìn)行初始化,如在 Application 的繼承類中
*/
private void initMatrix() {
// build matrix
Matrix.Builder builder = new Matrix.Builder(this);
// add general pluginListener
builder.patchListener(new TestPluginListener(this));
// dynamic config
DynamicConfigImplDemo dynamicConfig = new DynamicConfigImplDemo();
// init plugin
IOCanaryPlugin ioCanaryPlugin = new IOCanaryPlugin(new IOConfig.Builder()
.dynamicConfig(dynamicConfig)
.build());
//add to matrix
builder.plugin(ioCanaryPlugin);
//init matrix
Matrix.init(builder.build());
// start plugin
ioCanaryPlugin.start();
}
//至此,Matrix就已成功集成到你的項(xiàng)目中,并且開始收集和分析性能相關(guān)異常數(shù)據(jù),
//如仍有疑問,請(qǐng)查看 示例https://github.com/Tencent/Matrix/tree/dev/samples/sample-android/.
監(jiān)控內(nèi)容
- 文件的名字、原始大小、打開文件的堆棧、使用了什么線程, 一次操作一共使用了多長(zhǎng)時(shí)間,使用的 Buffer 是多大, 是一次連續(xù)讀完的,還是隨機(jī)的讀取;
- 主線程 I/O:有時(shí)候 I/O 的寫入會(huì)突然放大,即使是幾百 KB 的數(shù)據(jù),還是盡量不要在主線程上操作;
- 讀寫 Buffer 過小: 如果我們的 Buffer 太小,會(huì)導(dǎo)致多次無用的系統(tǒng)調(diào)用和內(nèi)存拷貝,導(dǎo)致 read/write 的次數(shù)增多,從而影響了性能。
- 重復(fù)讀:如果頻繁地讀取某個(gè)文件,并且這個(gè)文件一直沒有被寫入更新,我們可以通過緩存來提升性能。(加一層內(nèi)存 cache 是最直接有效的辦法)
public String readConfig() {
if (Cache != null) {
return cache;
}
cache = read("configFile");
return cache;
}
- 資源泄漏: 指打開資源包括文件、Cursor 等沒有及時(shí) close,從而引起泄露。這屬于非常低級(jí)的編碼錯(cuò)誤,但卻非常普遍存在。
I/O 與啟動(dòng)優(yōu)化
- 對(duì)大文件使用 mmap 或者 NIO 方式: MappedByteBuffer就是 Java NIO 中的 mmap 封裝,對(duì)于大文件的頻繁讀寫會(huì)有比較大的優(yōu)化。
- 安裝包不壓縮: 對(duì)啟動(dòng)過程需要的文件,我們可以指定在安裝包中不壓縮,這樣也會(huì)加快啟動(dòng)速度,但帶來的影響是安裝包體積增大。
- Buffer 復(fù)用: 我們可以利用Okio開源庫,它內(nèi)部的 ByteString 和 Buffer 通過重用等技巧,很大程度上減少 CPU 和內(nèi)存的消耗。
- 存儲(chǔ)結(jié)構(gòu)和算法的優(yōu)化: 通過算法或者數(shù)據(jù)結(jié)構(gòu)的優(yōu)化,讓我們可以盡量的少 I/O 甚至完全沒有 I/O, 比如一些配置文件從啟動(dòng)完全解析,改成讀取時(shí)才解析對(duì)應(yīng)的項(xiàng);替換掉 XML、JSON 這些格式比較冗余、性能比較較差的數(shù)據(jù)結(jié)構(gòu);
參考
我是今陽,如果想要進(jìn)階和了解更多的干貨,歡迎關(guān)注微信公眾號(hào) “今陽說” 接收我的最新文章