- 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_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 相关的系统调用次数和耗时
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 read
1.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 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. 实现 PluginListener,接收 Matrix 处理后的数据
*/
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. 实现动态配置接口,可修改 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;
}
@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. 选择程序启动的位置对 Matrix 进行初始化,如在 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就已成功集成到你的项目中,并且开始收集和分析性能相关异常数据,
//如仍有疑问,请查看 示例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
我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章