IO 優化不就是不在主線程讀寫大文件嗎,真的只有這么簡單嗎?
IO 基礎
IO流程:應用程序 發送邏輯IO命令給文件系統,文件系統發送物理IO命令給存儲設備/磁盤
文件系統
文件讀(read)過程:應用程序調用read() 方法,系統會通過中斷從用戶空間進入內核處理流程,然后經過 VFS、具體文件系統、頁緩存,如果數據沒有在頁緩存中,就需要真正向磁盤發起I/O請求 文件系統:存儲和組織數據的方式,如iOS的HFS+,APFS(Apple File System,iOS 10.3+), Android的ext4(Linux常用),F2FS(Flash-Friendly File System);
可以在 /proc/filesystems 看到系統可以識別的所有文件系統的列表
虛擬文件系統(VFS):屏蔽具體的文件系統,為應用程序的操作提供統一的接口; 頁緩存(Page Cache): 文件系統對數據的緩存,目的是提升內存命中率; Buffer Cache : 磁盤對數據的緩存,目的是合并部分文件系統的 I/O 請求、降低磁盤 I/O 的次數, 后來它也合并到 Page Cache 中的 Buffer Page 了;
通過 /proc/meminfo 文件可以查看緩存的內存占用情況
當手機內存不足時,系統會回收它們的內存,這樣整體 I/O 的性能就會有所降低。
磁盤
磁盤:系統的存儲設備,如CD, 機械硬盤, SSD 固態硬盤; 磁盤IO過程:先經過內核的通用塊層、I/O 調度層、設備驅動層,最后交給具體的硬件設備處理; 塊設備:系統中能夠隨機訪問固定大小數據塊(block)的設備,CD、硬盤、SSD都屬于塊設備; 通用塊層:主要作用是接收上層發出的磁盤請求,并最終發出 I/O 請求,讓上層不需要關心底層硬件設備的具體實現。 I/O 調度層:根據設置的調度算法對請求合并和排序
I/O 調度層 關鍵參數:
/sys/block/[disk]/queue/nr_requests // 隊列長度,一般是 128。
/sys/block/[disk]/queue/scheduler // 調度算法
塊設備驅動層: 據具體的物理設備,選擇對應的驅動程序通過操控硬件設備完成最終的 I/O 請求。如光盤的激光燒錄,閃存的電子擦寫;
Android I/O
Android 閃存(ROM)
Android前幾年的eMMC 標準,近幾年的UFS 2.0/2.1 標準,iOS和MacOS的NVMe 協議 閃存性能不僅僅由硬件決定,它跟采用的標準、文件系統的實現也有很大的關系
文件為什么會損壞?
格式錯誤或內容丟失,如SQLite大概有幾萬分之一的損壞率,SharedPreference 頻繁跨進程讀寫也會有萬分之一的損壞率; 從應用程序、文件系統和磁盤三個角度來審視:
1. 磁盤。手機上使用的閃存是電子式的存儲設備,所以在資料傳輸過程可能會發生電子遺失
等現象導致數據錯誤。不過閃存也會使用 ECC、多級編碼等多種方式增加數據的可靠性,一
般來說出現這種情況的可能性也比較小。
閃存壽命也可能會導致數據錯誤,由于閃存的內部結構和特征,導致它寫過的地址必須擦除才
能再次寫入,而每個塊擦除又有次數限制,次數限制是根據采用的存儲顆粒,從十萬次到幾千
都有(SLC>MLC>TLC)
2. 文件系統。雖說內核崩潰或者系統突然斷電都有可能導致文件系統損壞,文件系統把數據
寫入到 Page Cache 中,然后等待合適的時機才會真正的寫入磁盤.不過文件系統也做了很
多的保護措施。例如 system 分區保證只讀不可寫,增加異常檢查和恢復機制,ext4 的
fsck、f2fs 的 fsck.f2fs 和 checkpoint 機制等。
3. 應用程序。大部分的 I/O 方法都不是原子操作,文件的跨進程或者多線程寫入、使用一
個已經關閉的文件描述符 fd 來操作文件,它們都有可能導致數據被覆蓋或者刪除。事實上,
大部分的文件損壞都是因為應用程序代碼設計考慮不當導致的,并不是文件系統或者磁盤的問題。
I/O 有時候為什么會突然很慢?
內存不足:內存不足的時候,系統會回收 Page Cache 和 Buffer Cache 的內存,大部分的寫操作會直接落盤,導致性能低下; 寫入放大:閃存重復寫入需要先進行擦除,擦除操作的基本單元是 block 塊,一個 page 頁的寫入操作將會引起整個塊數據的遷移,這就是典型的寫入放大現象,低端機或者使用比較久的設備,由于磁盤碎片多、剩余空間少,非常容易出現寫入放大的現象。 配置不夠:低端機的 CPU 和閃存的性能相對也較差,在高負載的情況下容易出現瓶頸。
I/O 的性能評估
整個IO流程:應用程序–>系統調用–>虛擬文件系統–>文件系統–>塊設備接口–>驅動程序–>磁盤 I/O 性能指標: 吞吐量 和 IOPS 磁盤吞吐量:每秒磁盤I/O的流量,即磁盤寫入加上讀出的數據的大小。 存儲IOPS:磁盤IOPS是指一秒內磁盤進行多少次I/O讀寫; I/O 測量: 使用 proc 跟蹤 I/O 的等待時間和次數來衡量 proc/self/schedstat: se.statistics.iowait_count:IO 等待的次數se.statistics.iowait_sum: IO 等待的時間//如果是 root 的機器,我們可以開啟內核的 I/O 監控,將所有 block 讀寫 dump 到日志文件中,這樣可以通過 dmesg 命令來查看。echo 1 > /proc/sys/vm/block_dumpdmesg -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 相關的系統調用次數和耗時 strace -ttT -f -p [pid]read(53, "*****************"\.\.\., 1024) = 1024 <0.000447>read(53, "*****************"\.\.\., 1024) = 1024 <0.000084>read(53, "*****************"\.\.\., 1024) = 1024 <0.000059>
//也可以通過 strace 統計一段時間內所有系統調用的耗時概況。不過 strace 本身也會消耗不少資源,對執行時間也會產生影響。 strace -c -f -p [pid]% time seconds usecs/call calls errors syscall------ ----------- ----------- --------- --------- ----------------97.56 0.041002 21 1987 read1.44 0.000605 55 11 write
使用 vmstat //其中 Memory 中的 buff 和 cache,I/O 中的 bi 和 bo,System 中的 cs,以及 CPU 中的 sy 和 wa,這些字段的數值都與 I/O 行為有關。//我們可以配合dd 命令來配合測試,觀察 vmstat 的輸出數據變化。不過需要注意的是 Android 里面的 dd 命令似乎并不支持 conv 和 flag 參數//清除Buffer和Cache內存緩存echo 3 > /proc/sys/vm/drop_caches//每隔1秒輸出1組vmstat數據vmstat 1//測試寫入速度,寫入文件/data/data/test,buffer大小為4K,次數為1000次dd if=/dev/zero of=/data/data/test bs=4k count=1000
IO的三種方式
1. 標準IO
程序中平時用到 read/write 操作都屬于標準 I/O,也就是緩存 I/O(Buffered I/O) 緩存 I/O 可以很大程度減少真正讀寫磁盤的次數,從而提升性能,但延遲寫機制可能會導致數據丟失; Page Cache 中被修改的內存稱為“臟頁”,內核通過 flush 線程定期將數據寫入磁盤。
//具體寫入的條件我們可以通過 /proc/sys/vm 文件或者 sysctl -a | grep vm 命令得到// flush每隔5秒執行一次
vm.dirty_writeback_centisecs = 500
// 內存中駐留30秒以上的臟數據將由flush在下一次執行時寫入磁盤
vm.dirty_expire_centisecs = 3000
// 指示若臟頁占總物理內存10%以上,則觸發flush把臟數據寫回磁盤
vm.dirty_background_ratio = 10
// 系統所能擁有的最大臟頁緩存的總大小
vm.dirty_ratio = 20
在實際應用中,如果某些數據我們覺得非常重要,是完全不允許有丟失風險的,這個時候我們應該采用同步寫機制。 在應用程序中使用 sync、fsync、msync 等系統調用時,內核都會立刻將相應的數據寫回到磁盤。
2. 直接 I/O
很多數據庫自己已經做了數據和索引的緩存管理,對頁緩存的依賴反而沒那么強烈。它們希望可以繞開頁緩存機制,這樣可以減少一次數據拷貝,這些數據也不會污染頁緩存。 讀/寫均為同步執行,容易導致程序等待 只有在確定緩沖IO開銷非常巨大時才考慮直接IO
3. mmap
Android系統啟動加載dex時,不會把整個文件一次性讀到內存,而是采用mmap(); 通過把文件映射到進程的地址空間(用戶緩沖區與物理內存(頁緩存)共享數據,) 優點: 減少系統調用:只需一次mmap()系統調用,后續所有調用就像操作內存一樣,不會出現大量read/write系統調用 減少數據拷貝:普通read需要兩尺拷貝(磁盤to頁緩存,頁緩存to用戶緩沖區),mmap只需要將磁盤數據拷貝到頁緩存; 可靠性高:mmap把數據寫入頁緩存后,跟緩存IO的延遲寫機制一樣,可以依靠內核線程定期寫回磁盤; 適合對同一塊區域頻繁讀寫的情況,如用戶日志,數據上報 跨進程同步時mmap也是個很好的選擇,Android中的binder機制內部也是使用mmap實現 缺點: mmap在內核崩潰,突然斷電等情況下也可能引起內容丟失,也可以使用msync來強制同步寫; 虛擬內存增大:應用可用的需內內存空間有限,mmap一個大文件容易出現虛擬內存不足導致的OOM; 磁盤延遲:mmap通過缺頁中斷向磁盤發起真正的磁盤IO,所以如果當前問題在于磁盤IO的高延遲,那么mmap消除小小的系統調用開銷真是杯水車薪;之前講過的類重排技術主要就是為了減少缺頁中斷造成的磁盤IO延遲; 低端機或系統資源嚴重不足時,mmap也會出現頻繁寫入磁盤,性能快速下降;
多線程阻塞IO和NIO
多線程阻塞IO
IO操作可能很慢,所以應該盡量放到線程中; 文件讀寫收到IO性能瓶頸的影響,到達一定速度后整體性能就會收到明顯影響,過多的線程反而會導致應用整體性能的下降 合理使用多線程可以減少IO等待,太多的線程阻塞導致線程切換頻繁,增大系統上下文切換的開銷; 實際工作開發中大部分時候都是讀一些比較小的文件,使用單獨的IO線程還是專門新開一個線程,其實差別不大;
NIO
使用異步IO,將IO請求發送給系統后,繼續往下執行,將IO以事件的方式通知,減少線程切換的開銷; 缺點:應用程序的實現變得更復雜,有時異步改造并不容易 作用:最大作用不是減少讀取文件的耗時,而是最大化提升應員工整體的CPU利用率;(將線程等待磁盤IO的時間用來處理cpu的其他任務) 推薦使用Square 的Okio,支持同步和異步 I/O;使用demo如下:
//Okio中有兩個關鍵的接口,Sink和Source,這兩個接口都繼承了Closeable接口;
//而Sink可以簡單的看做OutputStream,Source可以簡單的看做InputStream。
//而這兩個接口都是支持讀寫超時設置的
//1. BufferedSink中定義了一系列寫入緩存區的方法
BufferedSink ?? ?write(byte[] source) 將字符數組source 寫入
BufferedSink ?? ?write(byte[] source, int offset, int byteCount)? 將字符數組的從offset開始的byteCount個字符寫入
BufferedSink ?? ?write(ByteString byteString)? 將字符串寫入
BufferedSink ?? ?write(Source source, long byteCount) 從Source寫入byteCount個長度的
long ?? ???????????? writeAll(Source source) 將Source中的所有數據寫入
BufferedSink ?? ?writeByte(int b) 寫入一個byte整型
BufferedSink ?? ?writeDecimalLong(long v) 寫入一個十進制的長整型
BufferedSink ?? ?writeHexadecimalUnsignedLong(long v) 寫入一個十六進制無符號的長整型
BufferedSink ?? ?writeInt(int i) 寫入一個整型
BufferedSink ?? ?writeIntLe(int i)
BufferedSink ?? ?writeLong(long v) 寫入一個長整型
BufferedSink ?? ?writeLongLe(long v)
BufferedSink ?? ?writeShort(int s) 寫入一個短整型
BufferedSink ?? ?writeShortLe(int s)
BufferedSink ?? ?writeString(String string, Charset charset) 寫入一個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編碼形式寫入的節點長度?? ????
//2. BufferedSource定義的方法和BufferedSink極為相似,只不過一個是寫一個是讀
BufferedSource read(byte[] sink) 將緩沖區中讀取字符數組sink 至sink
BufferedSource read(byte[] sink, int offset, int byteCount) 將緩沖區中從offst開始讀取byteCount個字符 至sink
BufferedSource readAll(Sink sink) 讀取所有的Sink
BufferedSource readByte() 從緩沖區中讀取一個字符
BufferedSource readByteArray() 從緩沖區中讀取一個字符數組
BufferedSource readByteArray(long byteCount) 從緩沖區中讀取一個長度為byteCount的字符數組
BufferedSource readByteString() 將緩沖區全部讀取為字符串
BufferedSource readByteString(long byteCount) 將緩沖區讀取長度為byteCount的字符串
BufferedSource readDecimalLong() 讀取十進制數長度
BufferedSource readFully(Buffer sink, long byteCount) 讀取byteCount個字符至sink
BufferedSource readFully(byte[] sink) 讀取所有字符至sink
BufferedSource readHexadecimalUnsignedLong() 讀取十六進制數長度
BufferedSource readInt() 從緩沖區中讀取一個整數
BufferedSource readIntLe()
BufferedSource readLong() 從緩沖區中讀取Long 整數
BufferedSource readLongLe()
BufferedSource readShort() 從緩沖區中讀取一個短整形
BufferedSource readShortLe()
BufferedSource readString(Charset charset) 從緩沖區中讀取一個String
BufferedSource readString(long byteCount, Charset charset) 讀取一個長度為byteCount的String,并以charset形式編碼
BufferedSource readUtf8() 讀取編碼格式為Utf-8的String
BufferedSource readUtf8(long byteCount) 讀取編碼格式為Utf-8且長度為byteCount的String
BufferedSource readUtf8CodePoint() 讀取一個Utf-8編碼節點,長度在1-4之間
BufferedSource readUtf8Line() 讀取一行Utf-8 編碼的String,碰到換行時停止
BufferedSource readUtf8LineStrict()
//3. ByteString: 作為一個工具類,功能十分強大,它可以把byte轉為String,這個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();}}}/*** 或許有時候網絡請求中,我們需要使用到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) {}}}
}
小文件系統
對于文件系統來說,目錄查找的性能是非常重要的 文件讀取的時間 = 找到文件的 inode 的時間 + 根據 inode 讀取文件數據的時間,如果我們需要頻繁讀寫幾萬個小文件,查找 inode 的時間會變得非常可觀; Google 的 GFS、淘寶開源的TFS、Facebook 的 Haystack ,微信的 SFS 都是專門為海量小文件的存儲和檢索設計的文件系統;要支持 VFS 接口,這樣上層的 I/O 操作代碼并不需要改動; 大量的小文件合并為大文件后,我們還可以將能連續訪問的小文件合并存儲,將原本小文件間的隨機訪問變為了順序訪問,可以大大提高性能。同時合并存儲能夠有效減少小文件存儲時所產生的磁盤碎片問題,提高磁盤的利用率。
I/O 跟蹤
1. Java Hook
java : FileInputStream -> IoBridge.open -> Libcore.os.open -> BlockGuardOs.open -> Posix.open
/1./在Libcore.java中可以找到一個挺不錯的 Hook 點,那就是BlockGuardOs這一個靜態變量
public static Os os = new BlockGuardOs(new Posix());
// 反射獲得靜態變量
Class<?> clibcore = Class.forName("libcore.io.Libcore");
Field fos = clibcore.getDeclaredField("os");
//2.可以通過動態代理的方式,在所有 I/O 相關方法前后加入插樁代碼,統計 I/O 操作相關的信息
// 動態代理對象
Proxy.newProxyInstance(cPosix.getClassLoader(), getAllInterfaces(cPosix), this);
beforeInvoke(method, args, throwable);
result = method.invoke(mPosixOs, args);
afterInvoke(method, args, result);
缺點: 性能極差:因為使用動態代理和 Java 的大量字符串操作 無法監控 Native 代碼 兼容性差: 特別是 Android P 增加對非公開 API 限制
2. Native Hook
Profilo 使用到是 PLT Hook 方案,性能比GOT Hook要稍好一些,不過 GOT Hook 的兼容性會更好一些 最終是從 libc.so 中的這幾個函數中選定 Hook 的目標函數
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);
需要選擇一些有調用上面幾個方法的 library。微信 Matrix 中選擇的是libjavacore.so、libopenjdkjvm.so、libopenjdkjvm.so,可以覆蓋到所有的 Java 層的 I/O 調用,具體可以參考io_canary_jni.cc 不過更推薦 Profilo 中atrace.cpp的做法,它直接遍歷所有已經加載的 library,一并替換。
void hookLoadedLibs() {auto& functionHooks = getFunctionHooks();auto& seenLibs = getSeenLibs();facebook::profilo::hooks::hookLoadedLibs(functionHooks, seenLibs);
}
Matrix使用
Matrix-android 當前監控范圍包括:應用安裝包大小,幀率變化,啟動耗時,卡頓,慢方法,SQLite 操作優化,文件讀寫,內存泄漏等等。
# 1. gradle.properties 中配置要依賴的 Matrix 版本號
MATRIX_VERSION=0.6.6
//2. 在你項目根目錄下的 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 falsebaseMethodMapFile = "${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. 實現 PluginListener,接收 Matrix 處理后的數據*/
public class TestPluginListener extends DefaultPluginListener {public static final String TAG = "Matrix.TestPluginListener";public TestPluginListener(Context context) {super(context);}@Overridepublic void onReportIssue(Issue issue) {super.onReportIssue(issue);MatrixLog.e(TAG, issue.toString());//add your code to process data}
}
/*** 5. 實現動態配置接口,可修改 Matrix 內部參數, 其中參數對應的 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;}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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. 選擇程序啟動的位置對 Matrix 進行初始化,如在 Application 的繼承類中*/
private void initMatrix() {// build matrixMatrix.Builder builder = new Matrix.Builder(this);// add general pluginListenerbuilder.patchListener(new TestPluginListener(this));// dynamic configDynamicConfigImplDemo dynamicConfig = new DynamicConfigImplDemo();// init pluginIOCanaryPlugin ioCanaryPlugin = new IOCanaryPlugin(new IOConfig.Builder().dynamicConfig(dynamicConfig).build());//add to matrixbuilder.plugin(ioCanaryPlugin);//init matrixMatrix.init(builder.build());// start pluginioCanaryPlugin.start();
}//至此,Matrix就已成功集成到你的項目中,并且開始收集和分析性能相關異常數據,
//如仍有疑問,請查看 示例https://github.com/Tencent/Matrix/tree/dev/samples/sample-android/.
監控內容
文件的名字、原始大小、打開文件的堆棧、使用了什么線程, 一次操作一共使用了多長時間,使用的 Buffer 是多大, 是一次連續讀完的,還是隨機的讀取;
主線程 I/O:有時候 I/O 的寫入會突然放大,即使是幾百 KB 的數據,還是盡量不要在主線程上操作; 讀寫 Buffer 過小: 如果我們的 Buffer 太小,會導致多次無用的系統調用和內存拷貝,導致 read/write 的次數增多,從而影響了性能。 重復讀:如果頻繁地讀取某個文件,并且這個文件一直沒有被寫入更新,我們可以通過緩存來提升性能。(加一層內存 cache 是最直接有效的辦法)
public String readConfig() {if (Cache != null) {return cache; }cache = read("configFile");return cache;
}
資源泄漏: 指打開資源包括文件、Cursor 等沒有及時 close,從而引起泄露。這屬于非常低級的編碼錯誤,但卻非常普遍存在。
I/O 與啟動優化
對大文件使用 mmap 或者 NIO 方式: MappedByteBuffer就是 Java NIO 中的 mmap 封裝,對于大文件的頻繁讀寫會有比較大的優化。 安裝包不壓縮: 對啟動過程需要的文件,我們可以指定在安裝包中不壓縮,這樣也會加快啟動速度,但帶來的影響是安裝包體積增大。 Buffer 復用: 我們可以利用Okio開源庫,它內部的 ByteString 和 Buffer 通過重用等技巧,很大程度上減少 CPU 和內存的消耗。 存儲結構和算法的優化: 通過算法或者數據結構的優化,讓我們可以盡量的少 I/O 甚至完全沒有 I/O, 比如一些配置文件從啟動完全解析,改成讀取時才解析對應的項;替換掉 XML、JSON 這些格式比較冗余、性能比較較差的數據結構;
參考
Android開發高手課-I/O優化(上):開發工程師必備的I/O優化知識 磁盤I/O那些事 Linux 內核的文件 Cache 管理機制介紹 vmstat 監視內存使用情況 選eMMC、UFS還是NVMe? 手機ROM存儲傳輸協議解析 聊聊 Linux IO 采用NAND Flash設計存儲設備的挑戰在哪里? linux命令–磁盤命令dd Android開發高手課-I/O優化(中):不同I/O方式的使用場景是什么? Linux 中直接 I/O 機制的介紹 微信終端跨平臺組件 mars 系列(一) - 高性能日志模塊xlog Okio Android開發高手課-I/O優化(下):如何監控線上I/O操作? Matrix
我是今陽,如果想要進階和了解更多的干貨,歡迎關注微信公眾號 “今陽說” 接收我的最新文章
總結
以上是生活随笔 為你收集整理的Android高手笔记 - IO优化 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。