Android I/O 相关优化方法总结

前言

在Android中,性能优化非常重要,但是,性能优化涉及多个方面,如提升线程性能、降低内存占用、提高渲染速度、提高内存读写速度、提高磁盘读写速度、提高网络访问速度等,主要目的是降低卡顿、卡慢、数据延迟展示等体验问题。

接下来我们先回顾一下常见的优化方案:

CPU线程调度优化

从线程性能、锁、线程优先级、日志异步化等方面,还有比较高级的用法如大核绑定、线程收敛等,在之前的文章中我们也进行过相关汇总过 《Android 线程性能优化方法总结》。

内存占用优化

内存占用过高不仅容易引发OOM,还会引起卡顿,为什么这么说呢?内存占用过高容易触发GC和虚拟机堆对象移动,其次过大的内存占用会影响i/O耗时,从而影响线程的处理速度。关于内存的优化,一般需要从下面几个方面着手

  • 有损压缩:这里说的有损压缩主要涉及图片、音视频、Http/rtmp资源压缩等,这种方式是最简单、直接、高效的手段。
  • 格式转换或使用SVGA、WEBP:一些图像格式可以更好的降低内存占用,其次还能降低内存i/o
  • Bitmap复用:主要指Buffer空间的复用,其次是绘制缓冲方面的数据复用
  • 享元模式:享元是提高对象或者Buffer复用率的高效手段,比如Message消息池、ExoPlayer的各种队列。在之前的一篇文章中,也有涉及,如《StringBuilder内存碎片优化
  • Bitmap压缩:实际上Bitmap压缩和上面说的有些重复,关于这方面,我们需要做到Size+格式压缩,另外就是要避免放大图片,尽可能使用算法,如线性过滤、防锯齿、Matrix去实现图像放大和变换;另外也可以使用Hardware类型,将数据放置到GPU内存中。
  • FD进行优化:FD优化是非常必要的,在前面的文章中也有涉及,如《Android HandlerThread FD 优化》实现FD裁剪。
  • 多级缓存优化:三级缓存、LFU/LRU/FIFO算法,目前,Glide源码是通过LRU+引用计数实现LFU的,非常值得参考。
  • 线程内存: 线程有自己的私有内存,过大的占用也会引发GC等问题,因此要控制线程数量,其次对一些线程池,可以适当降低StackSize。
  • 消息调度:消息调度主要从Choreograper、同步屏障、消息排序方面进行优化,对优先级高的消息可以使用Choreograper(可产生"异步"消息)或者Handler"异步"消息。
  • 算法优化:从算法的复杂度、数据结构方面提升性能,当然还有些技巧性的,如更新数据时删除和添加逻辑。
  • 多路复用:多路复用是优化i/O的重要手段,比如ZygoteServer就是多路复用的典型逻辑,其目的是使用中央处理器以外的硬件设备去优化i/O,比如GPU、DSP、DMA芯片来减少CPU负载
  • 使用多进程:多进程可以提高app的容灾能力,其次也能获取更多的内存。

View渲染优化

  • 减少布局层级:主要目的是减少measure和layout的对性能额度损耗。
  • View延迟加载:布局主要涉及include + viewStub延迟加载。
  • 减少主动触发requestLayout几率:可以参考RecyclerView的一些抑制策略和我们之前文章中的《Android TextView性能与文本展示优化
  • 减少绘制面积
  • 避免过度绘制
  • 硬件加速,硬件绘制(lockHardwareCanvas)
  • setLayerType实现缓冲,优化alpha效果,当然,本篇优化我们也提到过 《Android Alpha动画隐形成本优化
  • 高频隐藏展示的效果,选择VISBLE/INVISBLE方式,而不是GONE/VISIBLE
  • 提前为Drawable同步View大小,这一步也是为了减少requestLayout
  • 图片、视频异步解码
  • 图片缓存
  • 使用VapPlayer/AlphaPlayer/PAG/LazyAnimationDrawable实现动效,或者直接不要用动效
  • TextueView/SurfaceView/GLSurfaceView加速渲染。
  • 延迟加载资源
  • MutableContextWrapper 预加载
  • AysncLayoutInflater 异步加载
  • 布局xml2java
  • 图片延迟加载:要尽可能避免从xml中加载大图片或者gif资源,换句话说,避免在View的构造方法中加载资源
  • RecyclerView优化:预加载ViewHolder,优化缓存、多加载一屏幕数据,必要时多加载一屏UI(这种可能降低首屏性能,慎用)等。
  • RenderNode Canvas加速,低版本不支持
  • AGLS:类似opengl的shader渲染,低版本不支持

网络优化

主要涉及以下几个方面

  • DNS 优化
  • HTTP协议优化
  • TLS 优化
  • 缓存优化
  • WebView优化
  • 连接池优化

这部分其实在之前的文章中也有相当多的涉及,如《Android OkHttp使用过程中的一些经验总结》。

电量优化

以能量守恒定律的角度去思考,显然我们要避免不必要的Task,尽可能避免CPU的空转,其次是提高工作速度等。一个简单的例子,加载资源图片、i/O、屏幕常亮、Binder通信、音视频渲染等都是耗电主要因素。

进程通信优化

减少CPU中断和内核拷贝次数,直观的来说就是减少i/o操作或者进行内存映射,在Android平台上,这部分以Binder、Localsocket、MemoryFile为主,这里不再赘述。

录音优化

  • 使用系统默认的采样率,避免重采样
  • 降低采样位深

音频输出优化

  • 异步写入
  • 使用低延迟模式
  • 启用offload 模式(需要解码器支持)

我们可以看到,上面的一些优化手段有很多I/O相关的东西,那么我们本篇还需要补充什么?

实际上,上面是大家比较熟悉的方案,我们来重点熟悉一下内存映射和直接内存。

内存I/O优化

内存映射

传统的i/O如下

在传统 I/O 上,进程间的通信经过两次拷贝,而且这只是单向传输数据,如果 B进程要返回结果给 A进程,只需要把这张图中的 A进程和B进程角色关系调换一下。也就是说,如果是带返回值的 IPC,至少需要4次拷贝。

在计算机体系中,普通应用能访问的硬件设备只有内存,当然,这里的内存也只是虚拟内存。实际上,这里的i/O是需要CPU参与的,那么,有没有更好的办法呢?

肯定是有的,Binder就是这么实现的

但是,从这张图上我们可以看到,Binder也有一次拷贝,那么还能不能再优化呢?显然是可以的,参考MemoryFile或者ShareMemory实现共享内存即可。

然而,这里有个问题是,很多api都是C层实现,不过好在是Android 5.0 之后,增加了Os类,可以方便我们使用一些方法,如mmap相关的调用。

java 复制代码
android.system.Os

但是,这种优化是不是所有场景都合适呢?

显然不是的,内存映射如果是较小的数据量或者频次较低i/O操作性价比很低,因为内存映射过程是非常耗时的,如果涉及频繁扩容,其性能只会更低。

显然,适应的条件是有要求的

  • 高频数据传输
  • 1M(经验值)以上的数据传输
  • 极低频次的扩容,这种是要提前映射更低的内存,这也就是囚徒困境,扩容多了担心浪费内存,扩容少了会提升扩容频次。

因此,一般情况下,使用内存映射一般作为Buffer使用,比如Binder、Socket + epoll、xlog等案例。

直接内存

直接内存DirectByteBuffer是非常重要的优化手段,DirectByteBuffer从堆外申请内存,当然,并不是说堆外就不会OOM,但是这有个好处是,GC对堆外内存影响较小,比如数组一般需要连续的内存空间,如果在堆中申请,那么GC时会进行内存碎片整理,这个时候数组的指针地址会变化,引发耗时操作,但是堆外内存时不可移动内存,从而能避免此类问题。

HardwareBuffer

这是一个比较特殊的Buffer,不仅仅能提高性能,还支持多进程访问,可以实现多进程渲染。

磁盘 I/O优化

内存i/o优化旨在提高内存和磁盘的读写速度,我们调用磁盘的文件往往都需要通过内存,因此,磁盘I/O绝大部分情况都需要和内存相关,内存速度优化也是必要的。

实际上,磁盘 I/O和内存I/O有一些一样的优化方法,如压缩、内存映射,但也有些特殊的方法,下面我们主要看看各种优化手段

但是,我们这里要有一个常识,磁盘i/O不一定需要CPU参与,之所以在java中进行文件I/O会耗时,主要是程序的i/O模型决定的,比如InputStream#read方法,相当于"空等待",实际上使用epoll机制将其转换为类似监听模式。 ! aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80MjM2NTUzLTE3NGI4ZDljYzYxMTllNjcucG5n (1).png

BufferedOutputStream/BufferedInputStream

缓冲I/O数据可以有效减少CPU中断,提升i/O效率。

mmap

处理Os.mmap,实际上,java的FileChannel就可以实现mmap机制,提升拷贝效率,主要手段是将内核态内存和用户态映射

零拷贝 (共享内存)

实际上,mmap也可以实现零拷贝,但是Linux中也有专门的函数,sendfile函数来实现

当然,java也对齐有封装,FileChannel中有2个非常高性能的方法,不过,也不是说使用下面的方法就能触及sendfile,其实能否触发sendfile和资源大小有关,每个版本有所区别,如果资源较小的话会走mmap机制,非常适合文件拷贝。

java 复制代码
public abstract long transferFrom(ReadableByteChannel src,
                                  long position, long count)
    throws IOException;

public abstract long transferTo(long position, long count,
                                WritableByteChannel target)
    throws IOException;

文件移动优化

文件移动很简单,但是很多人总是忘记这么做,反而选择拷贝的方式。

使用File#rename即可,因为rename方法只修改link,没有I/O操作,然而,一些版本的rename是有bug的,不能跨根目录rename,比如从/data/data移动数据文件到sd卡就会异常,Android高版本中android.app.ActivityThread.AndroidOs已经修复了此bug。

当然,使用rename时一定要判断返回值,防止rename失败。

java 复制代码
@Override
public void rename(String oldPath, String newPath) throws ErrnoException {
    try {
        super.rename(oldPath, newPath);
    } catch (ErrnoException e) {
        // On emulated volumes, we have bind mounts for /Android/data and
        // /Android/obb, which prevents move from working across those directories
        // and other directories on the filesystem. To work around that, try to
        // recover by doing a copy instead.
        // Note that we only do this for "/storage/emulated", because public volumes
        // don't have these bind mounts, neither do private volumes that are not
        // the primary storage.
        if (e.errno == OsConstants.EXDEV && oldPath.startsWith("/storage/emulated")
                && newPath.startsWith("/storage/emulated")) {
            Log.v(TAG, "Recovering failed rename " + oldPath + " to " + newPath);
            try {
                Files.move(new File(oldPath).toPath(), new File(newPath).toPath(),
                        StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e2) {
                Log.e(TAG, "Rename recovery failed ", e2);
                throw e;
            }
        } else {
            throw e;
        }
    }
}

总结

本篇涉及到的内容主要是i/o相关,当然,我们这里强调一下,关于拷贝的次数的定义,在操作系统的中,只要是cpu参与的才会被统计,如DMA芯片、DSP芯片参与的不计入。因此,看到mmap或者sendfile拷贝次数有疑问是正常的,和定义有关。

i/O优化非常重要,特别是一些音视频采集app,需要经常实现磁盘、网络、内存的i/O,本篇就到这里,希望本篇内容对你有所帮助。

相关推荐
吾即是光8 分钟前
[SWPUCTF 2021 新生赛]error
android
爱吃羊的老虎13 分钟前
【WEB开发.js】getElementById :通过元素id属性获取HTML元素
前端·javascript·html
Thomas_YXQ23 分钟前
Unity3D Lua如何支持面向对象详解
开发语言·游戏·junit·性能优化·lua·unity3d
大耳猫24 分钟前
Android 基于Camera2 API进行摄像机图像预览
android·kotlin·相机·camera
MYBOYER27 分钟前
Kotlin DSL Gradle 指南
android·开发语言·kotlin
妙哉73629 分钟前
零基础学安全--HTML
前端·安全·html
咔叽布吉36 分钟前
【前端学习笔记】AJAX、axios、fetch、跨域
前端·笔记·学习
GISer_Jing1 小时前
Vue3常见Composition API详解(适用Vue2学习进入Vue3学习)
前端·javascript·vue.js
Dragon Wu1 小时前
TailwindCss 总结
前端·css·前端框架
bpmf_fff1 小时前
十、事件类型(鼠标事件、焦点.. 、键盘.. 、文本.. 、滚动..)、事件对象、事件流(事件捕获、事件冒泡、阻止冒泡和默认行为、事件委托)
前端·javascript