核心内容
通过共享HandlerThread的方式,减少MessageQueue的创建,从而减少FD的创建,减少OOM的发生。
前言
我们常说,面向对象的开发语言中一切皆对象,任何对象都有他的属性和成员,因此后来拓展出了“类”这个概念,类是对象的抽象,一切都能抽象成为类,但类也是对象,在抽象的过程中先有对象才有类。作为操作系统,linux也有自己的规则——一切皆文件。
Android 作为基于unix-like的操作系统,本自带了操作系统最核心的基因——一切皆文件,这个核心思想是通过I/O的思想简化组件通信模式,一个组件必须以Input、Output或者同时兼备的模式去适配,这也统一后续的系统层通信发展路线,避免了碎片化。
FD在unix-like系统中用于描述组件(如进程)、资源(如文件)、节点(如驱动)等,在Android中,我们使用的进程、文件、MessageQueue、Socket、Binder、Pipe、共享内存、sqlite等均与其有关。
现状
FD释放
在很多情况下,对文件I/O读写必须及时close,当然为了避免被忽略,java官方开发了新的回收方式
1 | java复制代码 try (FileInputStream fis = new FileInputStream("a.txt")) { |
同样,只要是Closeable的子类,均可以如此释放,如Socket等。
但问题是,I/O操作并不一定是单一方法种处理,比如我们录音过程,如果要添加音效,势必会转移到其他线程处理,这个时候,频繁的开启和关闭i/O显然不明智。其次,阻塞可能导致杂音问题,这个时候显然这个方法是不合适的。再比如SocketServer,监听到Socket,一般也要开启新的线程处理处理请求数据。
基于上述情况的不足,官方做法是利用CloseGuard做法是对I/O对象进行监视,在对象finalize发出warning信息,严格模式也是利用这种实现。
1 | java复制代码 class Foo { |
那么,除了常见的一些监控和编程规范外,还有什么优化方式呢?
fd 共享
fd共享本质尽可能让同一个资源被多次利用,而不是一个资源打开多次,典型的做法就是Sqlite的操作放到ContentProvider中,以及Okhttp 中的连接池也起到相同的作用,再后来,Http 2.0的多路复用实现。说到这里,作为协程的老祖宗,epoll也是多路复用的实现,通过epoll可以做到监视多种fd,其实也可以做到fd共享。
fd 监控
在Android系统中,没有相应的api来获取应用的fd信息,更早期的Android是可以通过下面方式创建Shell(uid是相同的)进程去获取信息,类似CMD命令行一样,后来就不行了。
fd查询
命令方式
1 | java复制代码adb shell ls -l /proc/{pid}/fd |
app中调用的话使用如下方法
1 | java复制代码public Process exec(String[] cmdarray, String[] envp, File dir) |
在实际的线上环境中,很难通过Process去监控fd的数量变化,不过读取fd的方法还是有的,如何做到监控呢,实际上可以定时也可以在关闭特定页面时触发检测逻辑,对比前后变化的差异。
1 | java复制代码public static String[] getFds() { |
然后对FD进行识别
1 | java复制代码public static String readlink(String name) { |
fd 差异比较
差异比较是比较之前的fd列表与最新的fd列表
1 | java复制代码FDMonitor.diff(oldFdList,newFdList) |
fd 监控缺陷
但是,这里仍然是有缺陷的,因为读取的到的fd有些 :node ,很难去识别,也很难定位到准确的线程资源上去,比如MessageQueue、进程和socket的。这些fd申请之后,如何对应到对象上去显然有一些难度。
fd表示符号 | 资源类型 |
---|---|
socket:[1992] | 网络请求相关 |
anon_inode:[eventpoll] | MessageQueue epoll |
anon_inode:[eventfd] | MessageQueue相关 |
anon_inode:[timerfd] | 系统定时器 |
anon_inode:[dmabuf] | InputChannel WMS通信 |
/vendor/ | 一般是系统操作使用 |
/dev/ashmem | 内存相关 |
pipe | 管道通信相关 |
/sys/ | 一般是系统操作使用 |
/data/data/打开文件相关/data/app/ | 私有目录文件fd |
/storage/emulate/0/com.sample.abc | sdcard打开文件fd |
FD OOM
fd是OOM的元凶之一,fd触发的OOM基本上分为三类,一类是FD超过了阈值数量(如线程创建、too open many file),另一类是过多的fd资源申请,导致其他操作无法申请到内存,最后一类是fd申请的资源(如内存)超过了系统所能提供的内存。
FD 最大限制梳理
这里说一下,默认情况下,fd的数量最多为1024,当然一些高端机会超过这个数值限制,查询命令如下
1 | java复制代码adb shell ulimit -n |
当然,如果要在app中读取,上面的命令是不可以的,因此需要换种方式,那就是读取自己进程目录下的limits文件
1 | java复制代码File file = new File("/proc/" + android.os.Process.myPid() + "/limits"); |
读取到文件,并进行解析
Soft Limit=1024,Hard Limit=4096,Units=files
以上是现状了,下面我们进入本篇的主题
FD 常用优化方法
FD优化的工具其实并不多,常用的的方法有
- 及时关闭fd
- 避免短时间内申请大量的fd资源 (如mmap、socket、thread、进程),按优先级事项申请
- 控制fd数量,如线程的申请不宜过多。过多的HandlerThread并不一定能发挥cpu的优势,反而线程占用的内存比较高,更容易造成oom
- 监控fd,防止fd泄露
下面,我们实现一种特殊的方法
HandlerThread FD 优化
本篇的重点是通过共享HandlerThread实现FD数量优化。
在 Android 系统中,创建一个 Looper 通常会创建 2 个 FD,一个是eventfd,另一个是epollfd,基本都是MessageQueue创建,而线程是没有FD的。在 Android 5.x上甚至会创建三个 FD,pipe_in、pipe_out 和 epollfd。
那么,在这里,我们的优化点显然不Looper,因为很难去更改系统底层实现,除非利用bhook这样的工具。如果想要通过纯java方式实现,那么,我们只能把视线放到HandlerThread上。
我们共享HandlerThread,这样每次只创建Thread,MessageQueue就可以避免创建了。
共享HandlerThread
实现自己的ShareHandlerThread和LightHandler
我们让HandlerThread 共享一个即可,思路就是模仿epoll多路复用机制,epoll多路复用和协程一样,一个监视线程,另一个是处理线程,我们这里让同一个RealHandlerThread (真实的HandlerThread)监视自定义的ShareHandlerThread创建的所有的Handler发送过来的Message,一旦到了执行时间,就发送Message到指定的线程执行,执行时到指定的Handler。
不过要这么实现的话,Handler我们也用不了,只能使用LightHandler了,但是为了保证执行的先后顺序,我们需要引入执行队列。
为什么这样可以实现,首先Looper机制本身具备先后顺序,因此我们把MessageQueue的消息发送到执行队列,其实影响并不大。
定义ShareHandlerThread
- 必须也能进行MessageQueue类似的操作,如删除和查询消息,因为其中需要定义队列
- 需要拿到消息后转发到自己的队列
- 必须在自己的线程执行
- 需要找到自己的LightHandler执行,因此这里得用Map影射一下
- 必须可以quit或者quitySafely
- 必须Callback消息的正确执行
- 防止消息执行之前LightHandler被回收
我们这里主要核心是使用Handler.Callback去拦截消息,看过handler.dispatchMessage源码就能知道,其可以优先拦截消息,但其中Callback消息是很难拦截的,使用Handler.Callback也是无法拦截到,因为他在Handler.callback之前执行,因此我们只能在LightHandler中做特殊处理。
LightHandler映射也是很简单,我们使用name记录下来
1 | java复制代码private final Map<String, WeakReference<LightHandler>> lightHandlerMaps = new ConcurrentHashMap<String, WeakReference<LightHandler>>(); |
但LightHandler被回收是一个潜在风险,因此我们可以利用WeakHashMap监视原始的Handler,只有原始的Handler被回收LightHandler才能被回收。
1 | java复制代码private final WeakHashMap<Handler, LightHandler> recycleMonitor = new WeakHashMap<>(); |
下面是比较完整的代码
1 | java复制代码public class ShareHandlerThread implements Runnable, InternalHandler.SimpleCallback { |
InternalHandler实现
不过,在上面的代码中我们可以看到,我们还需要一个辅助HandlerThread的InternaHandler,主要负责消息传递
1 | java复制代码public class InternalHandler extends Handler { |
上面是InternalHandler的主要实现,很简单,但是难度主要在final字段修改这里,因为异步Hander的mAsynchronous字段是final修饰的且不公开的,因此还需要需要反射。
LightHandler实现
映射
这里有些难点,难点主要是如何让realHandlerThread中的消息执行时找到当前的LightHandler,为此我们这里做了个映射关系,在上面ShareHandlerThread中我们创建LightHandler给name的目的就是为了生成映射关系,但是Callback消息就有一定的难度,我们先处理一般消息。
不过,这里有个细节要注意,我们不能通过msg.getData()去获取Bundle,不然默认的new Bundle()会产生占内存4+4个数组元素的对象,因此,这里我们用peekData拿数据。
1 | java复制代码private void attachLightHandler(Message msg) { |
最终映射关系会知道ShareHandlerThread总的LightHandler
1 | java复制代码 private final Map<String, LightHandler> lightHandlerMaps = new ConcurrentHashMap<>(); |
callback 问题
由于我们可以在Handler#dispatchMessage方法中修改回调,那事实上这个callback就不成问题
这里,只需要模拟正式的Handler处理流程
1 | java复制代码public void dispatchMessage(Message msg) { |
getLooper问题
我们自定义的ShareHandlerThread显然是没有Looper的,因此我们最简单的办法就是把ShareHandlerThread对象返回出去即可。
1 | java复制代码public Object getLooper(){ |
跨进程问题
如果你对Handler熟悉的话,就知道Handler是可以借助IMessenger间接跨进程的,但LightHandler实现跨进程显然要修改Message或者在外面封装一下,考虑到Handler跨进程使用的机会并不多,因此我们暂时不做太多的处理。
LightHandler完整代码
下面是完整的实现代码,主要实现了
- Message消息标记
- 消息监控
- callback 代理
- callback 消息保护
- finalize消息回收
1 | java复制代码public class LightHandler { |
完整代码就是核心逻辑。
总结
通过上述实现,我们就能实现共享Looper的的情况下,实现了消息的发送和到指定线程的处理,从而减少了MessageQueue的创建,开头说过,也避免了FD开销。
当然,这个是个简单的逻辑,实际过程中还需要进一步打磨优化。
本文转载自: 掘金