ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入解析Android底层基石:mmap系统调用原理与实战

深入解析Android底层基石:mmap系统调用原理与实战 搞懂 Android 系统调用绕不开 mmap。这一篇是系列里的“Android 前篇-11”重点就是 Linux 内核里最常被问、也最容易被误解的 mmap。很多做应用开发的朋友一开始没接触过这块觉得底层的东西离自己很远但其实我们每天都在用比如 ClassLoader 加载 dex、Binder 映射共享内存、SurfaceFlinger 的图形缓冲区、甚至你手机上 App 启动时的 dex2oat 优化背后都有 mmap 的影子。理解它不只是为了面试装点门面而是真正排查 native 崩溃、内存优化、IO 性能调优时能拿出手的底牌。这篇文章我会按“设计思路 → 核心原理 → 实操验证 → 问题排查”的路线来讲尽量用 Android 真实场景打比方把参数含义、系统调用流程、常见坑都掰开揉碎。适合正在学 Android framework、想做性能优化、或者打算深入 Binder/IO 方向的开发者。只要你写过 C/C 或者看过 JNI 代码跟着实操走一遍基本就能把 mmap 这条主线串起来了。1. 为什么是 mmapAndroid 底层的内存与 IO 基石1.1 从“读文件”说起普通 read 与 mmap 的本质差异要理解 mmap 的价值先从最简单的场景开始你需要把一个文件的内容读进内存。常规做法是 read(fd, buf, len)这个过程在内核里大致是发起 read 系统调用 → 内核从磁盘把数据复制到页缓存Page Cache→ 再从页缓存复制到用户空间的 buf。一次 read至少发生两次数据拷贝磁盘/页缓存一次页缓存到用户区一次。如果你还要在用户态把数据再搬到另一块 buffer那就是三次。mmap 的思路完全不同它不主动复制文件内容而是把文件的一段区间“映射”到进程的虚拟地址空间进程直接通过指针读写这段内存。第一次访问映射区时才会发生缺页中断由内核负责把对应页从磁盘加载到页缓存然后再映射到进程页表。这个过程中用户空间看到的就是普通内存指针后续重复读写同一页时不再需要系统调用CPU 直接访问内存映射区域即可。对比下来你可以把 mmap 理解为“懒加载 内核帮你仲裁的大文件窗口”read 则是“一次给钱一次货”的搬运工。在 Android 里这个差异直接反映在启动时间上。App 启动时系统要加载 dex/oat 文件如果每个文件都用 read 全量读一遍再解析里面的类、方法、字符串启动耗时肯定压不住。用 mmap 后只有真正访问到某个类对应的页面时内核才去加载那一页未访问的部分不会白白消耗 IO 和内存。这就是为什么即便是一个几百 MB 的 APKApp 也能瞬间冷启动起来的原因之一。1.2 mmap 在 Android 系统里的典型角色在 Android 的世界里mmap 不是“偶尔用一下”它是核心基础设施。举几个最常见的角色你会发现这些都是日常开发中经常听到的名词。第一个是 Binder。Binder 驱动为了完成跨进程传输会在内核分配一段物理内存并映射到每个进程的地址空间这部分用了 mmap 的原理。进程和系统服务之间共享一块内核缓冲区binder_transaction 数据就是通过这个共享区域传递的省掉了多次拷贝。整个过程对上层 App 是透明的但你每次调用 ActivityManager、PackageManager、WindowManager 等系统服务时底层都在通过 mmap 出来的映射区进行通信。第二个是匿名共享内存 ashmem。它本质上是基于 mmap 实现的一个内核驱动模块App 申请一块匿名共享内存再映射到自己进程或者通过 Binder 把 fd 传给别的进程从而实现多进程共享同一块内存。影视播放器解码后的帧、拍照后的大块 buffer都经常走这条路。恶意跨进程共享对象时效率远高于反复序列化传输。第三个是文件映射加载 dex。Android 的 ClassLoader 在加载 dex 文件时会把 dex 文件映射到进程地址空间然后直接通过内存指针解析 dex 的 header、class_defs、data 段等结构。这样做的好处是文件不用全部读进内存按需加载多个进程同时加载同一个 dex 时内核页缓存可以共享同一批物理页内存占用大幅降低。你在 Android Studio 里看到的“memory 中 dex 占用”其实有很大一部分就是映射页的 RSS。还有一种是 JNI 开发里常见的 DirectByteBuffer 配合 mmap 做高性能 IO。比如做本地数据解析、日志写入、数据库扩展时可以在 native 层先把文件 mmap 进来然后写入的数据直接落内存再通过 msync 落盘避免每次写入都触发一次系统调用。对于高频小数据写入这能带来肉眼可见的吞吐提升。1.3 为什么 Android 系统调用里 mmap 地位特殊Android 虽然是 Linux 内核血统但它并不是一台标准的 Linux 服务器。手机上内存小、IO 慢、进程多、生命周期频繁这些约束决定了 mmap 的特殊地位。它把“IO”变成了“内存访问”让操作系统得以在有限资源下支撑流畅的多任务体验。此外Android 对匿名映射也有完整支持和优化。比如 zygote 进程 preload 的类资源通过 fork 时 COW写时复制机制共享到所有 AppJava 堆分配内部也会在大块内存分配时使用匿名 mmap。这些都对 OS 的内存管理、缺页处理、回收策略提出了更高要求。你可以说 mmap 是理解 Android 这块土地的地基看不懂地基分析 OOM、jank、native crash 时就会处处碰壁。2. mmap 系统调用的核心细节参数、返回值与内存语义2.1 函数原型与参数拆解Linux 下的 mmap 函数原型是这样#include sys/mman.h void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);参数看着就六个但每一个都有讲究。addr 是建议的映射起始地址。通常传 NULL让内核自己挑一个合适的虚拟地址。如果你非要指定地址必须按页对齐即页大小整数倍而且这只是“建议值”内核不一定采纳。如果你想验证内核是否真的用了你给的地址可以检查返回值。length 是映射长度单位是字节同样必须是页大小的整数倍或者至少内核会按页向上取整。实际映射的区域 向上取整到页边界。如果你映射 100 字节系统内核会给你映射 4096 字节多出来的部分访问不越界但未定义这是很多新手踩 bug 的地方。prot 表示保护标志组合值主要有 PROT_NONE、PROT_READ、PROT_WRITE、PROT_EXEC。读、写、执行可以按位或。注意文件映射时prot 不能超过文件打开时的访问模式。比如 open 时只给了 O_RDONLYmmap 时就不能带 PROT_WRITE否则返回 EACCES。在 Android 上由于 SELinux、W^X 策略等因素同时要求可读可写可执行RWX的映射非常容易被拒绝尤其是代码段映射系统更倾向于分开。flags 参数是决定 mmap 行为的关键它至少需要一个映射类型MAP_SHARED 或 MAP_PRIVATE。这俩的区别一句话能讲清MAP_SHARED 映射下对内存的修改会写回文件或其他共享者可见MAP_PRIVATE 映射下对内存的修改只对自己可见其他映射者看不到文件本身也不会因为你的修改改变除非你后来调 msync 再写回。其他常见 flags 还包括 MAP_ANONYMOUS匿名映射fd 传 -1、MAP_FIXED强制地址不推荐乱用、MAP_POPULATE预读页、MAP_LOCKED锁页、MAP_NORESERVE不预分配 swap 空间等。Android 上匿名共享内存封装时也会用到 MAP_ANONYMOUS。fd 是文件描述符。匿名映射时置 -1。文件映射时必须传入有效 fd且该 fd 不能是 socket 或某些设备文件通常普通文件和部分设备支持。offset 是偏移量表示从文件的哪个位置开始映射。重要约束offset 必须是页大小通常是 4096的整数倍否则报 EINVAL。如果你要映射文件中间某一段偏移量必须页对齐不能随便给个 12345。2.2 返回值与错误码成功后 mmap 返回映射区的起始地址即 void *失败返回 MAP_FAILED也就是 (void *)-1。注意它不是返回 NULL很多经验不足的开发者直接判断 ptr NULL导致错误漏检。正确写法void *ptr mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (ptr MAP_FAILED) { int err errno; // 根据 errno 排查 }常见 errno 我列成表直接背下来排错会快很多errno含义常见原因EACCES权限不足fd 打开模式和 prot 不匹配文件所在目录不可执行导致无法映射EAGAIN暂时无法完成文件被锁或映射的页面数量超过限制EINVAL参数无效length 为 0offset 不对齐flags 没有 MAP_SHARED/MAP_PRIVATE 等ENOMEM内存不足进程虚拟地址空间不足或超过 vm.max_map_countENODEV设备不支持映射操作fd 指向了不支持 mmap 的文件系统或设备EOVERFLOW参数溢出32 位系统上偏移加长度超过地址空间限制Android 上最常见的两个EINVAL 多半是你 offset 忘了页对齐ENOMEM 多半是映射区总数量超过系统限制尤其是 dex、so 加载过多时后面会专门展开讲。2.3 进程地址空间映射区从哪来每个用户态进程都有一张“虚拟地址空间地图”mmap 就是在这张地图上画新地块。地图分成几个区域代码段、数据段、堆、栈、共享库段、以及各种 mmap 映射区域。在 Linux 上mmap 区域通常会分配在堆和栈之间的某段地址具体由内核根据 current-mm-mmap_base 等数据决定。Android 的 address space 天然比 PC 紧张。32 位系统只有 4GB 虚拟地址其中内核还占掉一部分进程能用的 3GB 左右里又要有 Java 堆、art 堆、栈、多个 dex/oat/so 映射一不小心就把空间耗尽。这也是为什么 Android 一直推动 64 位化的重要原因。64 位系统上虚拟地址空间足够大mmap 空间不再是主要瓶颈但也不是无限依然受 vm.max_map_count 和物理内存页管理约束。映射区域被卸载有两种方式显式调用 munmap或者进程退出时由内核自动回收。注意 munmap 的参数 addr 必须和 mmap 返回的地址完全一致长度也应匹配否则可能只卸载部分这也合法但容易造成地址空间碎片。Android 上频繁 mmap/munmap 且不好好对齐就会出现虚拟地址碎片后续大块映射因没有连续地址空间而失败。3. 实操在 Android 环境里亲手调用 mmap3.1 准备一个可复现的 JNI 工程要直观感受 mmap最快的路径是写一个 Android JNI 工程在 native 层调用 mmap然后把结果展示到 Java 层。先创建一个简单的 Native Activity 或普通 App加上 externalNativeBuild。大致步骤在 app/src/main/cpp 下新建 native-lib.cppCMakeLists.txt 里 add_library链接 liblog 或直接用 stdio在 MainActivity 里用 System.loadLibrary 加载用 native 方法触发native 方法可以定义为external fun mmapFileTest(path: String, offset: Long, length: Long): String然后在 C 里实现#include jni.h #include fcntl.h #include sys/mman.h #include sys/stat.h #include unistd.h #include android/log.h #include cstring extern C JNIEXPORT jstring JNICALL Java_com_example_mmaptest_MainActivity_mmapFileTest( JNIEnv* env, jobject, jstring path_, jlong offset, jlong length) { const char* path env-GetStringUTFChars(path_, nullptr); int fd open(path, O_RDONLY); if (fd 0) { env-ReleaseStringUTFChars(path_, path); return env-NewStringUTF(open failed); } size_t len (size_t)length; void* ptr mmap(nullptr, len, PROT_READ, MAP_PRIVATE, fd, (off_t)offset); close(fd); // 映射建立后即可关闭文件描述符 if (ptr MAP_FAILED) { return env-NewStringUTF(strerror(errno)); } // 读取前若干字节作为示例 char buf[32] {0}; memcpy(buf, ptr, std::min(len, sizeof(buf) - 1)); munmap(ptr, len); env-ReleaseStringUTFChars(path_, path); return env-NewStringUTF(buf); }这段代码证明了三件事打开文件后 mmap映射成功后即使关闭 fd内存仍然可访问因为映射已经把文件页绑定到页缓存了内核懒加载访问 ptr 才触发缺页munmap 之后地址不可再访问。3.2 参数选择要怎么定为什么 offset 要对齐实操时最容易触发 EINVAL 的就是 offset。假设你有一个文件总共 100 KB你想映射字节 5000 到字节 60000那么 offset 5000length 55000。第一反应直接这么传结果 mmap 失败。因为页大小为 40965000 % 4096 904不满足页对齐。正确的做法是将 offset 向下对齐到页边界比如 4096同时相应把 length 增加偏移差值5000 - 4096 904。映射起始地址指向第 4096 字节读取数据时 ptr 904 才是真正的第 5000 字节。如果要映射的末尾超过文件大小内核会返回 SIGBUS访问超文件尾的区域时总线错误这也是一个隐藏的坑。我在实际项目里封装过一个读文件片段的安全函数bool mapFileRegion(const char* path, off_t start, size_t size, std::vectoruint8_t out) { int fd open(path, O_RDONLY); if (fd 0) return false; struct stat st; if (fstat(fd, st) ! 0 || st.st_size 0) { close(fd); return false; } long page sysconf(_SC_PAGESIZE); off_t aligned_start start ~(page - 1); off_t delta start - aligned_start; size_t aligned_len size delta; void* ptr mmap(nullptr, aligned_len, PROT_READ, MAP_PRIVATE, fd, aligned_start); close(fd); if (ptr MAP_FAILED) return false; out.assign((uint8_t*)ptr delta, (uint8_t*)ptr delta size); munmap(ptr, aligned_len); return true; }注意 out.assign 的第二个参数不能用 begin 加 size 的死板写法保证不越界这段代码更适用于小文件片段。如果是大文件直接 std::string 拷贝出来就失去 mmap 的意义应该直接把指针交出去让调用方按需读取。3.3 匿名映射与共享内存的简单实现除了文件映射匿名映射在 Android 上更常用。比如创建一个可读写的匿名共享缓冲区并 fork 出子进程父子就能通过这个映射区通信。Android 的多进程模型不像服务器那样常 fork但 app 内部可以用 android::ashmem 或者直接 mmap MAP_ANONYMOUS | MAP_SHARED。简化的实现#include sys/mman.h #include unistd.h #include cstring int main() { const size_t pagesize sysconf(_SC_PAGESIZE); void* p mmap(nullptr, pagesize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (p MAP_FAILED) return 1; pid_t pid fork(); if (pid 0) { // 子进程写入 strcpy((char*)p, hello from child); _exit(0); } else { wait(nullptr); printf(parent read: %s\n, (char*)p); munmap(p, pagesize); } return 0; }在 Android 上不能直接 printf但逻辑一样。要注意的细节是子进程退出后映射区仍然存在直到所有映射它的进程都 munmap 或退出才真正释放。这就是多个进程共享一块内存的可视化解释。3.4 Android 中的 ashmem 与 Binder 视角如果要在 Android App 层面使用共享内存建议直接使用系统提供的 ashmem 接口android.os.SharedMemory。它基于内核的 ashmem 驱动本质上就是 mmap binder 传 fd。Java 层用法很简单val sharedMemory SharedMemory.create(shared, 4096) val mappedBuffer sharedMemory.map(android.os.MemoryFile.MODE_READ_WRITE)但从底层看SharedMemory.create 会通过 JNI 调用 ashmem_create_region然后内部执行 mmap。你传入 Binder 给另一个进程的 fd对方也可以 map 到自己的虚拟地址空间。这套机制在跨进程传递大块数据时非常高效Binder 本身默认有 1MB 事务大小限制而映射区本身并没有限制在 binder 的 data 里只需要传一个 fd实际数据物理页是共享的。这解释了为什么 Android 的 Binder 调用在传递大对象时有很多优化空间能传 fd 就不传裸数据能传映射结果就不做序列化。如果你开发系统应用或 framework 相关模块这几乎是标配玩法。4. 常见问题与排查技巧实录4.1 mmap 后访问崩溃Segmentation fault 与 Bus error两类经典崩溃SIGSEGV 和 SIGBUS很多人混为一谈。SIGSEGV 通常是访问了“未映射”的地址比如 munmap 后的区域、越过了整个映射区长度、或者映射的是 MAP_PRIVATE 且同时访问超出文件范围但仍在映射长度内的区域时行为未定义。SIGBUS 则是“映射区域本身合法但对应底层文件或设备不支持这次访问”最常见的就是映射大小超过文件大小当访问到超出文件末尾的页面时触发。从 Android tombstone 日志中你可能会看到 seizure signal 7 (SIGBUS) 的崩溃。排查思路先找到 /proc/self/maps确认崩溃地址落在哪个映射区间再检查该映射对应的 offsetlength 是否超出了文件实际长度最后检查是否访问了超过映射 length 的尾巴。对付超文件尾问题保险做法是在 mmap 之前用 fstat 获取文件大小并把 length 限制在 st_size - offset 内。4.2 映射数量超限Too many open files 和 vm.max_map_countAndroid 高版本系统对每个进程的 VMA 数量有限制通常通过 /proc/sys/vm/max_map_count 控制默认值未必很高。一个大型 App 里加载几十个 so、多个 dex、大量匿名映射很容易逼近上限。现象是 mmap 突然失败errno 是 ENOMEM但系统物理内存其实还够。你打开 /proc/self/maps 数一下行数再查看上限adb shell cat /proc/sys/vm/max_map_count adb shell cat /proc/self/maps | wc -l如果限制是 65550而你进程已经有 60000 行那就非常危险。缓解手段少开无意义映射大块映射统一管理而不是频繁小段映射用 munmap 及时释放如果无法改系统参数App 侧只能从合并映射、降低 SO 数量想办法。Android 系统属性里有的厂商还把 max_map_count 调得更低这是线上偶发 mmap 失败的一大来源。4.3 fd 关闭与内存可见性的误区很多新手以为 mmap 后 fd 必须保持打开其实映射与 fd 本身解耦。mmap 成功建立映射后内核已经持有对应文件引用你可以立刻 close(fd)映射区照常工作。但要小心如果你自己这边后来再次 open 同一文件并重新 mmap可能因文件偏移、打开模式不同造成混淆更关键的是MAP_SHARED 模式下对映射区修改会最终写回文件但写回时机是不确定的需要 msync 或 munmap 保证持久化。如果进程 crash最后未 msync 的修改有丢失风险。MAP_SHARED 与多线程也存在可见性问题。同一进程多个线程访问同一共享映射区CPU 硬件缓存通常能保证一致性但如果你在不同进程间共享就需要考虑是否需要锁来保护并发写。mmap 不提供任何锁语义它只保证内存落在同一物理页不保证业务逻辑的原子性。4.4 匿名映射的 COW 陷阱fork 后内存占用爆炸Android 上 zygote fork App 时父进程的匿名映射区会通过 COW 共享给子进程。正常情况下子进程写之前物理内存只有一个副本一旦子进程写入就会复制一份。这本来是省内存的设计但如果子进程在 fork 后大量触写那些本可以共享的映射就会造成物理内存膨胀。Android 低内存时发生的过程、应用被杀部分原因就与此相关。如果你在 framework 里 fork 并 exec 新进程尽量减少 fork 后的写操作或者使用 posix_spawn 替代 forkexec 以避开 COW 层叠。App 应用层一般不用管但做系统开发时要留意。4.5 Native crash 定位利用 maps 与 backtrace当 mmap 相关的 native 崩溃发生后tombstone 文件里会贴出崩溃地址附近的 maps。你要做的第一件事不是看寄存器而是找崩溃地址落在哪个 map 区域。例如40012000-40015000 r-xp 00001000 fd:05 12345 /system/lib64/libfoo.so如果崩溃地址是 0x40013020说明在 libfoo.so 的偏移 0x3020 处接下来用 addr2line 转行号aarch64-linux-android-addr2line -e libfoo.so 0x3020如果崩溃地址落在某个匿名映射区那么别急着找符号先想想这块匿名内存是谁创建的。可以加日志在 mmap 时保存调用栈很多 native 内存调试工具比如 malloc_debug 的 backtrace就是这么做的。我见过不少“看似随机的 native crash”最后都是 mmap 返回后用错了指针长度导致越界访问通过 maps 加内存分配栈能快速定位。5. 一些值得再挖的扩展当 mmap 遇到性能优化5.1 大文件读取用 mmap 替代 read 的场景判断并不是所有场景用 mmap 都更好。大文件顺序读取、且只读一遍read 的效率往往不差而且没有缺页开销。mmap 的优势在于访问分散、多次重复读同一数据、希望内核对页缓存做统一管理。Android 中热点就是 dex 和 so 加载因为它们的访问模式以“随机查表 热点页高频访问”为主mmap 能显著减少系统调用次数。如果你自己设计一个二进制资源文件解析框架强烈建议小文件几 KB直接一次性读入内存大文件百 MB且随机访问使用 mmap 映射超大文件还想压缩那得考虑映射后的按需解压否则映射不变态。5.2 页大小的计算与映射区对齐的意义现代 Android 手机页面大小绝大多数是 4096 字节但也有 arm64 设备使用 64 KB 页。不同页大小直接影响 offset 对齐要求和 length 向上取整的粒度。写通用代码时用 sysconf(_SC_PAGESIZE) 动态获取不要硬编码 4096。页对齐的意义不仅在于满足内核参数要求还能保证 mmap 返回的地址天然按页对齐方便后续做 16 字节对齐的 buffer 管理。你在图形、编解码等领域看到很多缓冲 allocator 对齐到 4096正是因为这些 buffer 经常要通过 mmap 和硬件设备共享。5.3 msync、madvise 与 mlock 的合理使用mmap 只是开始。写完共享映射区如果不调 msync数据不会立刻落盘。msync 有三个常用的 flagMS_SYNC同步等待写回、MS_ASYNC让内核稍后写回、MS_INVALIDATE使其他映射失效。日志系统、小型数据库模块应该按需求选择。madvise 能给内核提供访问模式的提示。访问是顺序的还是随机的是马上要用的还是用完可以丢的都会影响缺页和预读策略。Android 上 loader 在加载大文件时经常调用 madvise(MADV_RANDOM)避免内核提前预读大量无用页。使用 madvise 后卡顿和 IO 使用量能明显变化但这个函数是“建议”内核不保证一定采纳。mlock 是将映射区锁定在物理内存中避免被 swap。Android 一般没有 swap 分区但这对于想要低延迟的实时音频缓冲区、敏感密钥内存仍有意义。注意 mLock 会占用物理内存滥用会让系统内存紧张触发 lowmemorykiller应谨慎。5.4 mmap 与 Android 的 CMA、ION 等图形内存的关系SurfaceFlinger、Codec2、GPU 驱动会通过 ION/CMA 等内存分配器分配物理连续或设备可见内存上层通过 mmap 把这块物理内存映射到用户空间进行填充。如果你研究过 Surface 的生产消费流程你会发现App 在 CPU 侧绘制时拿到的是 GraphicBuffer 的 map 地址本质上就是 mmap 了 buffer 的物理页。这块内存的分配、映射、取消映射、同步直接影响渲染性能瓶颈。理解到这里你就能明白为什么有时候帧率高不起来CPU 在 mmap 区域写入后GPU 要消费中间有 cache 同步和 fencing如果频繁 mmap/munmap 或者制造大量缺页开销会非常大。所以图形领域的内存池化本质就是减少 mmap 次数维护常驻映射区。写在最后的实操心得我个人做 Android 底层性能优化踩过不少 mmap 的坑这里挑几个最痛的提醒大家。第一永远不要忽略 offset 对齐和 length 页对齐。不管传什么值先按页边界重算一次能省下无数 EINVAL 排查时间。自己在封装 mmap 工具函数时建议直接把对齐后参数打印出来方便核对。第二用 mmap 做共享内存时一定确认好生命周期。fd 可以关映射不能乱 munmap。多个进程持有同一物理页最后一个 munmap 才真正释放。可以多用 Android 的 SharedMemory让框架替你管理 fd 和映射生命周期没必要自己裸调除非做 framework。第三遇到 ENOMEM 别只盯着物理内存。先看 /proc/self/maps 行数再确认先前的映射有没有泄漏。很多线程泄漏导致黑盒其实是 mmap 泄漏导致 VMA 数量触顶行为非常诡异。用 malloc_debug 或 libmeminfo 去 dump 进程的 meminfo 和 maps能发现真相。第四生产环境里 mmap 大文件时建议配合 MADV_RANDOM并避免把整个大文件全量映射后再做顺序扫描。真想顺序读走 pread 可能更低延迟。最后想说Android framework 的很多知识点从上层看就是 API从底层看全是系统调用和内存管理。mmap 这个系统调用值得你花一个下午写 demo、开 adb、看 maps、制造缺页、看 strace。把这条链路摸透了以后你再遇到 IO 慢、OOM、native crash思维就完全不一样了。下一篇我会继续沿着 Android 系统调用的主线拆解匿名共享内存和 binder 的映射细节咱们到时接着聊。
返回列表