Android高手笔记

Android高手笔记 - IO优化

2021-08-26  本文已影响0人  今阳说

IO 基础

文件系统

可以在 /proc/filesystems 看到系统可以识别的所有文件系统的列表
通过 /proc/meminfo 文件可以查看缓存的内存占用情况

磁盘

I/O 调度层 关键参数:
/sys/block/[disk]/queue/nr_requests      // 队列长度,一般是 128。
/sys/block/[disk]/queue/scheduler        // 调度算法

Android I/O

Android 闪存(ROM)

文件为什么会损坏?
1. 磁盘。手机上使用的闪存是电子式的存储设备,所以在资料传输过程可能会发生电子遗失
等现象导致数据错误。不过闪存也会使用 ECC、多级编码等多种方式增加数据的可靠性,一
般来说出现这种情况的可能性也比较小。
闪存寿命也可能会导致数据错误,由于闪存的内部结构和特征,导致它写过的地址必须擦除才
能再次写入,而每个块擦除又有次数限制,次数限制是根据采用的存储颗粒,从十万次到几千
都有(SLC>MLC>TLC)
2. 文件系统。虽说内核崩溃或者系统突然断电都有可能导致文件系统损坏,文件系统把数据
写入到 Page Cache 中,然后等待合适的时机才会真正的写入磁盘.不过文件系统也做了很
多的保护措施。例如 system 分区保证只读不可写,增加异常检查和恢复机制,ext4 的 
fsck、f2fs 的 fsck.f2fs 和 checkpoint 机制等。
3. 应用程序。大部分的 I/O 方法都不是原子操作,文件的跨进程或者多线程写入、使用一
个已经关闭的文件描述符 fd 来操作文件,它们都有可能导致数据被覆盖或者删除。事实上,
大部分的文件损坏都是因为应用程序代码设计考虑不当导致的,并不是文件系统或者磁盘的问题。
I/O 有时候为什么会突然很慢?
  1. 内存不足:内存不足的时候,系统会回收 Page Cache 和 Buffer Cache 的内存,大部分的写操作会直接落盘,导致性能低下;
  2. 写入放大:闪存重复写入需要先进行擦除,擦除操作的基本单元是 block 块,一个 page 页的写入操作将会引起整个块数据的迁移,这就是典型的写入放大现象,低端机或者使用比较久的设备,由于磁盘碎片多、剩余空间少,非常容易出现写入放大的现象。
  3. 配置不够:低端机的 CPU 和闪存的性能相对也较差,在高负载的情况下容易出现瓶颈。

I/O 的性能评估

IO的三种方式

1. 标准IO
//具体写入的条件我们可以通过 /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
2. 直接 I/O
3. mmap

多线程阻塞IO和NIO

多线程阻塞IO
NIO
//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) {
            }
        }
    }
}

小文件系统

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);

2. Native 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);
void hookLoadedLibs() {
  auto& functionHooks = getFunctionHooks();
  auto& seenLibs = getSeenLibs();
  facebook::profilo::hooks::hookLoadedLibs(functionHooks, seenLibs);
}

Matrix使用

# 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/.

监控内容

  1. 主线程 I/O:有时候 I/O 的写入会突然放大,即使是几百 KB 的数据,还是尽量不要在主线程上操作;
  2. 读写 Buffer 过小: 如果我们的 Buffer 太小,会导致多次无用的系统调用和内存拷贝,导致 read/write 的次数增多,从而影响了性能。
  3. 重复读:如果频繁地读取某个文件,并且这个文件一直没有被写入更新,我们可以通过缓存来提升性能。(加一层内存 cache 是最直接有效的办法)
public String readConfig() {
  if (Cache != null) {
     return cache; 
  }
  cache = read("configFile");
  return cache;
}
  1. 资源泄漏: 指打开资源包括文件、Cursor 等没有及时 close,从而引起泄露。这属于非常低级的编码错误,但却非常普遍存在。

I/O 与启动优化

参考

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

上一篇 下一篇

猜你喜欢

热点阅读