具透 Plus:Android 17 的应用内存限制;谈谈 macOS 和 Linux 如何运行可执行文件

17:48

聊聊Android17的应用内存限制@克莱德:每隔一段时间,我都会整理一下自己的书桌。毕竟要在一张120×60cm的桌面上放下一块34英寸带鱼屏和一台40L机箱,留给其他常用物件的空间实在不多。前几天 ...


聊聊 Android 17 的应用内存限制

@克莱德:每隔一段时间,我都会整理一下自己的书桌。毕竟要在一张 120 × 60 cm 的桌面上放下一块 34 英寸带鱼屏和一台 40 L 机箱,留给其他常用物件的空间实在不多。前几天和@路中南聊到 Android 17 的应用内存限制时,我环视了一圈桌面,突然意识到:这不就是一个绝佳的例子吗?

先说一个老生常谈、但用户教育效果始终不太显著的常识:Android 的 RAM 本来就是拿来用的。

你买来一张电脑桌,肯定不是为了让它空空荡荡地摆在那里供人观赏。正常情况下,你会把正在使用、稍后可能还会使用的东西放在桌面上或者手边:键盘、耳机、杯子、文件,想用时伸手就能拿到。Android 使用 RAM 的思路也是如此。系统会尽量把近期用过的 App、页面、图片和数据留在内存中,方便下次迅速调用。

台湾的朋友会把这些内容叫作「快取」,我们通常叫缓存,英文都是 cache。既然空闲内存本身不会直接给用户带来价值,那么所谓的 Android「运行内存清理大师」,以及那种只看「剩余运行内存」多少来评价系统优化水平的论调,自然也就显得相当荒谬。它们就像一个特别爱管闲事的朋友,因为觉得你的桌面太乱,便一股脑把所有东西都塞进抽屉里。桌面确实空了,但你每次想用东西,都得重新翻箱倒柜。

那么,我们就来聊聊这个「抽屉」。

在书桌上,你可能正在画画、打字、看视频,也可能一边玩游戏,一边享用外卖。Android 会把这些任务分别组织成不同的进程(process」,再根据它们对用户的重要程度划分优先级。

比如,你正在吃的麦当劳大堡口福套餐,可以算作前台进程(foreground process);你暂时推到显示器下方的键盘,以及正在播放尼克老师「朋友们」系列下饭视频的显示器,可以算作可见进程(visible process)。此外,系统中还有前台服务、后台进程和已缓存进程等不同状态。

正常情况下,Android 会优先保证前台进程和可见进程的运行。至于那些暂时用不到、但稍后可能还会重新打开的东西,系统可以先把它们收进「抽屉」,也就是将其保留为已缓存进程(cached process)。这样一来,你下次再打开对应的 App 时,系统不必从头启动,只需要把它从抽屉里拿回来。

负责管理这件事的角色之一叫 LMKD,也就是 Low Memory Killer Daemon——Android 系统里的 RAM 清道夫。

假设路中南一口气点了六个汉堡,桌面实在摆不下了。你会先按照优先级,把最不重要的耳机、文件和其他杂物收进抽屉。如果连抽屉也塞满了,就只能把其中一些东西扔进桌边的废纸箱。对应到 Android 系统中,就是彻底终止一部分进程,也就是俗称的把进程「杀」掉。

在理想情况下,这套基于进程优先级和 LMKD 的内存管理机制相当高效。暂时不用的东西先放进抽屉,需要时再拿出来;只有内存确实不够时,系统才会彻底终止优先级较低的进程。

问题在于,路中南这小子就像某个国民聊天 App 一样,脑子里总有「我要一口气点完金拱门所有汉堡」这种恐怖分子才会有的想法。他还满口念叨「内存涨价、设备涨价,连 Apple 都涨价,买不起新设备」之类的疯话,非要在这张小书桌上解决整顿饭,而不是努力攒钱买张大餐桌。

当一个前台应用占用的内存越来越多,系统就只能不断把其他进程塞进抽屉,或者从抽屉里扔进废纸箱。等用户再次打开这些应用时,系统又要重新加载数据、恢复页面,甚至彻底冷启动。把东西放进抽屉、从抽屉扔进废纸箱、弯腰从纸箱里捞东西、再到抽屉里翻找刚刚收进去的耳机……次数一多,工作量自然也就上来了。

这正是 Android 17 引入应用内存上限(App memory limits)所要解决的问题。

过去,系统主要根据整体内存压力和进程优先级决定应该清理谁。Android 17 则会进一步参考设备的总内存容量,为单个应用设置一个可以占用的内存上限。换句话说,系统不仅会看桌面还剩多少空间,也会限制每个人最多能往桌上堆多少个汉堡。

当设备出现内存压力、需要 LMKD 介入时,如果某个应用已经超过了自己的内存上限,系统会优先终止这个超出红线的应用,而不是任由它继续挤占空间,导致其他无辜进程被频繁清理。这样可以减少后台应用反复退出、热启动乃至冷启动的情况,也能避免一个「前台大胖子」拖累整台设备的使用体验。

当然,为了避免那些从来不读 Android 开发规范的国内开发者突然摸不着头脑,Android 17 因为应用超出内存上限而终止进程时,还会通过 MemoryLimiter、ApplicationExitInfo 等标记,帮助开发者判断应用被杀的具体原因。这样一来,开发者至少不能再把所有问题笼统归咎于「系统后台限制太严格」。

如果再往深处看 Google 在本届 I/O 上公开的信息,还会发现它同时改进了应用运行时的内存回收机制。

简单来说,应用运行过程中产生的对象会按照存活时间划分「年龄」。那些刚刚创建、很可能很快就不再使用的年轻对象,会通过 ART 运行时环境中的 GC 垃圾回收机制,以更频繁、但也更轻量的方式进行清理;存活时间较长、状态相对稳定的对象,则会更加谨慎地处理,避免频繁 GC 给应用性能和 CPU 等系统资源带来过重负担。

不过,这项改进计划会通过 Google Play 系统更新推送给所有 Android 12(API level 31)及以上设备。严格来说,它并不完全属于 Android 17 的独占新特性。至于这一部分,我们以后有机会再请路中南出来单独聊聊啦。

macOS 和 Linux 是怎么知道一个东西应该用什么运行的?

@广陵止息:当你在 macOS 或 Linux 的终端里输入 ./test.sh 时,系统面对的其实只是一个纯文本文件。它需要先判断这个文件究竟是什么,接着才能决定应该直接交给 CPU 运行,还是交给某个解释器处理。

很多人首先想到的判断依据可能是文件名扩展名:看到 .sh 就交给 Shell,看到 .py 就交给 Python。但在 *nix 系统中,文件名扩展名并没有固定语义。一个脚本可以没有扩展名,也可以使用完全不相干的扩展名;一个二进制程序即使改名为 xxx.sh,也不会因此变成 Shell 脚本。

换句话说,macOS 和 Linux 主要根据文件内容,而不是文件名,来判断它应该如何运行。

例如,Shell 脚本的第一行通常是:

#!/bin/bash

其中,#! 叫作 shebang,后面的 /bin/bash 则是解释器路径。它是在告诉系统:这个文件并不是 CPU 可以直接执行的机器码,需要交给 /bin/bash 解释运行。

因此,执行:

./test.sh

相当于执行:

/bin/bash ./test.sh

Python 脚本也经常使用类似的写法:

#!/usr/bin/env python3

这里并没有写死 Python 解释器的具体位置,而是先运行 /usr/bin/env,再由它从当前环境的 PATH 中寻找 python3。这样一来,同一个脚本便更容易适应不同系统和开发环境。

不过,并不是所有程序都需要解释器。我们日常运行的许多软件,都是已经编译好的二进制程序,文件中直接包含 CPU 能够执行的机器码,以及内核和动态链接器需要读取的其他信息。

在 Linux 上,这类程序通常采用 ELF(Executable and Linkable Format)格式。ELF 文件开头有一组固定的特征字节,也就是所谓的「Magic number」。它的开头是 7f 45 4c 46,后三个字节对应 ASCII 字符 E、L、F。

Linux 内核读到这组 Magic number 后,就知道自己面对的是一个 ELF 文件。接下来,它还会继续读取 ELF 头部的信息,判断这个程序面向哪种 CPU 架构、应该如何装载、需要使用哪个动态链接器等。

所以,x86-64 和 ARM 程序虽然同样以 ELF 的 Magic number 开头,但它们的目标架构会记录在后续字段中,并不是靠不同的开头来区分。

依靠文件内容判断格式还有一个额外好处:它比只看扩展名可靠得多。直到今天,Windows 上仍然有人因为打开了看似是文档、实际上却是批处理脚本或可执行程序的文件而中招。文件扩展名可以轻易伪装,文件内部的格式结构却没有那么容易蒙混过关。如果内核既没有认出 shebang,也无法识别或运行文件中的二进制格式,执行自然就会失败。比如,在 CPU 架构不匹配时,Linux 通常会报出 Exec format error。

但事情并不总是到这里就结束了。随着二进制转译、虚拟化和兼容层技术逐渐成熟,现代系统已经能够把原本无法运行的程序接管过来,再交给其他组件处理。Apple 的 Rosetta 和 Linux 的 binfmt_misc,就是两种典型做法。

先看 macOS。macOS 的原生可执行程序通常使用 Mach-O 格式,而 Mach-O 还可以进一步封装成 Universal Binary,也就是俗称的 Fat Binary。一个 Universal Binary 可以同时包含多种 CPU 架构的程序切片,例如同时包含 x86-64 和 ARM64 版本。

程序启动时,系统会先检查其中有没有与当前处理器匹配的切片。有的话,就直接选择对应版本运行。例如,同一个 App 在 Intel Mac 上使用 x86-64 切片,在 Apple Silicon Mac 上则使用 ARM64 切片。如果程序只包含 Intel 版本,而当前设备使用的是 Apple Silicon,系统就需要 Rosetta 2 介入,将 x86-64 机器码翻译成 ARM64 代码。对用户来说,这个过程通常是透明的:你照常双击 App,系统负责在背后完成识别、翻译和执行。

这也解释了为什么 Rosetta 2 对 Apple Silicon 的过渡期如此重要。它让大量尚未完成原生适配的 Intel App 可以继续运行,开发者和用户不必在处理器架构迁移完成的第一天就抛弃所有旧软件。

按照 Apple 目前公布的安排,macOS 27 将是最后一个全面支持 Rosetta 2 的 macOS 版本。从 macOS 28 开始,Rosetta 只会保留有限功能,用于运行少数依赖 Intel 框架、且已经无人维护的旧游戏。也就是说,一般的 Intel Mac 应用此后将无法再依靠 Rosetta 继续运行。

Linux 的处理方式则更加通用。在 Linux 中,负责这类接管工作的机制叫作 binfmt_misc,它早在 1997 年就已经加入内核。binfmt_misc 允许系统注册额外的可执行文件格式规则。内核可以根据文件开头的特征字节判断它属于某种格式,然后把这个文件自动交给指定的解释器或兼容层。它的思路与 shebang 很相似,只不过规则不是写在每个文件里,而是统一注册到系统中。

例如,在 ARM Linux 上运行 x86-64 程序时,可以把 x86-64 ELF 文件注册给 qemu-x86_64-static。之后每当用户执行这类程序,内核都会自动将其交给 QEMU 的用户态模拟器,而不需要用户每次手动输入一长串命令。同样,在 ARM 设备上运行 x86-64 Docker 容器时,也经常会通过 binfmt_misc 将容器里的 x86-64 二进制程序交给 QEMU。容器中的程序以为自己正在正常执行,背后的架构转换则由宿主系统负责完成。

理解了这一点,我们就可以把 binfmt_misc 和 Rosetta 2 连接起来了。

会员专属文章,欢迎加入少数派会员。
优质内容
权益周边
会员社群
power+
评论区
全部评论0
成为少数派会员方可评论,立即加入。若已是少数派会员,点击登录
还没有评论,来发表第一个评论吧
全部评论
还没有评论,来发表第一个评论吧
成为少数派会员方可评论,立即加入。若已是少数派会员,点击登录