WMS 专项 - 底层支撑系统 (SurfaceFlinger, Input, View)

WMS 的运行并非孤立,它深度依赖于图形合成、输入分发和视图系统。理解这些支撑系统,是掌握 WMS 源码逻辑的基石。


1. SurfaceFlinger:WMS 的图形“甲方”

WMS 负责管理窗口的逻辑层级,而 SurfaceFlinger 负责将这些层级真实地合成并渲染到屏幕上。

核心考点 详细解析 学习目的
图形缓冲区 (BufferQueue) Surface 内部维护的生产者-消费者模型队列。 理解应用绘制的数据是如何传递给 SurfaceFlinger 的。
Layer (层) SurfaceFlinger 内部对窗口的映射,每个窗口对应一个 Layer。 理解 WMS 的 WindowState 最终是如何映射为渲染层的。
合成原理 (Composition) VSync 触发,将多个 Layer 按照 Z-Order 合并。 掌握 Android 60/120 fps 流畅度的物理基础。

2. Input 系统:事件分发的“中转站”

WMS 协助 InputManagerService (IMS) 将触摸事件分发到正确的窗口。

核心考点 详细解析 学习目的
IMS 与 Socket 通信 IMS 通过 InputChannel(基于 Unix Domain Socket)与应用通信。 理解为什么输入事件分发不使用 Binder。
窗口定位 WMS 维护全局窗口位置,告知 IMS 哪个坐标对应哪个窗口。 理解点击屏幕后,系统如何“定位”到你的按钮。
事件分发链 IMS -> WMS (定位) -> ViewRootImpl -> DecorView -> ViewGroup -> View。 建立完整的事件分发全景图。

3. View 系统:WMS 的“应用端代理”

应用端通过 View 系统与 WMS 建立联系。

核心考点 详细解析 学习目的
ViewRootImpl 整个 View 树的根节点,负责与 WMS 的 Session 进行 IPC 通信。 理解 WindowManager.addView 背后的第一站。
DecorView 窗口的根视图,包含状态栏和内容区。 明确 Activity 布局的物理边界。
Measure/Layout 触发 relayoutWindow 成功后,WMS 反向驱动应用端执行绘制。 理清“系统驱动”与“应用主动”的逻辑闭环。

4. 总结:三大系统与 WMS 的协作关系

  • WMS + SurfaceFlinger:解决“在哪里画,画多大”的问题。
  • WMS + Input 系统:解决“谁接收点击”的问题。
  • WMS + View 系统:解决“具体画什么”的问题。

掌握了这三者的协作,就掌握了 Android 系统 UI 展示的核心脉络。

Android Framework 专项 - IPC Binder 机制(一)

Binder 是 Android 系统中最重要的进程间通信(IPC)机制。

1. 为什么 Android 选用 Binder?

在 Linux 系统中,已经存在多种 IPC 方式,如管道、信号、消息队列、共享内存、Socket 等。Android 选用 Binder 主要出于以下考虑:

  • 性能(效率):共享内存虽然最快(0 次拷贝),但管理复杂;Socket 效率低(2 次拷贝);Binder 只需要 1 次数据拷贝,性能仅次于共享内存。
  • 安全:传统 IPC 的接收方无法获得发送方可靠的 UID/PID,难以进行权限校验。Binder 在内核层添加了身份校验,安全性更高。
  • 稳定性:Binder 基于 Client-Server 架构,职责明确,容错性好。

2. Binder 的分层架构

Binder 的实现可以分为四个层次:

  1. **内核层 (Binder Driver)**:运行在内核空间,是所有 Binder 通信的枢纽。负责物理内存映射(mmap)和数据的跨进程传递。
  2. **驱动抽象层 (Libbinder)**:在 Native 层对驱动操作进行封装(如 ProcessState, IPCThreadState)。
  3. **框架层 (Java Binder)**:提供给 Java 开发者使用的接口(如 IBinder, Binder, Stub, Proxy)。
  4. 应用层:具体的业务实现(如 AIDL 定义的 Service)。

3. Binder 通信的核心:内存映射 (mmap)

Binder 之所以能实现“一次拷贝”,核心在于 mmap

  • Binder 驱动会在内核空间分配一段缓冲区。
  • 驱动会将这段内核缓冲区同时映射到接收进程的用户空间
  • 发送进程通过 copy_from_user 将数据拷贝到内核缓冲区,由于映射关系存在,接收进程直接就能在自己的用户空间读到这些数据。

4. 总结

Binder 是 Android 系统的灵魂。理解了 Binder,才能真正理解 AMS、WMS 等系统服务是如何与应用进程协同工作的。

Android Framework 专项 - Zygote 进程与进程孵化机制

Zygote(受精卵)进程是 Android 系统中所有应用进程和 SystemServer 进程的“鼻祖”。理解 Zygote 机制是深入掌握 Android 系统启动及应用进程创建的关键。


1. Zygote 的核心职责

Zygote 进程在系统启动时由 init 进程解析 init.rc 脚本启动。它的主要工作可以概括为以下三点:

职责 详细描述
预加载资源 (Preload) 加载系统常用的类库(Framework 类)、资源(Drawables, XML)及共享库,供所有子进程共享。
启动 SystemServer 孵化出 Android 的核心服务进程 SystemServer,由其启动 AMS, WMS 等系统服务。
孵化应用进程 (Fork) 通过 Socket 监听请求,每当需要启动新应用时,通过 fork 机制快速创建子进程。

2. 核心机制:Fork 与写时复制 (COW)

为什么 Android 选择使用 Zygote 来孵化进程,而不是直接启动?

  • 启动速度快:Zygote 已经预加载了大量的共享类和资源。通过 Linux 的 fork() 系统调用创建子进程时,子进程直接继承了 Zygote 的内存状态。
  • **节省内存 (Copy-On-Write)**:
    • fork 后的子进程与父进程(Zygote)共享相同的物理内存页。
    • 只有当子进程尝试修改这些内存页时,内核才会真正为子进程分配新的内存副本。
    • 这种机制极大地减少了系统内存的冗余开销。

3. Zygote 的启动流程分析

关键步骤 执行角色 核心行为
1. 解析脚本 init 进程 解析 init.zygote.rc 文件,执行 app_main.cpp
2. 开启虚拟机 AppRuntime 调用 AndroidRuntime.start(),启动 ART/Dalvik 虚拟机并注册 JNI 方法。
3. 进入 Java 层 ZygoteInit 通过 JNI 调用 ZygoteInit.main(),标志着 Zygote 正式从 C++ 切换到 Java 环境。
4. 开启 Socket 监听 ZygoteServer 建立 /dev/socket/zygote 本地套接字,等待来自 AMS 的进程创建请求。

4. 从 AMS 启动应用进程的调用链

当 AMS 发现目标进程不存在时,会触发以下调用:

  1. AMS 服务端:调用 Process.start() 发起进程创建请求。
  2. 消息传递:通过 ZygoteProcess/dev/socket/zygote 写入启动参数。
  3. Zygote 服务端:监听到 Socket 消息,调用 Zygote.forkAndSpecialize()
  4. 进程诞生
    • 父进程(Zygote):继续循环监听 Socket。
    • 子进程(新 App):初始化 ActivityThread,进入 main 函数,开启主线程 Loop。

5. 总结

关键术语 定义与作用
AppRuntime Zygote 的启动载体,负责虚拟机的创建与 JNI 环境初始化。
Preload 资源预取技术,是 Android 应用能够快速启动的根本原因。
LocalSocket AMS 与 Zygote 通信的唯一方式(而非 Binder,因为 Fork 之后 Binder 通信会产生不可控的状态冲突)。

Zygote 的设计精妙地结合了 Linux 系统的底层特性(Fork/COW)与 Android 资源共享的需求,是 Android 系统稳定运行的基石。

Zygote 通信机制:为什么是 Socket 而不是 Binder?

在 Android 系统中,绝大多数进程间通信(IPC)都使用 Binder,但唯独 AMS 向 Zygote 请求创建进程这一步,使用的是 LocalSocket。这是一个非常经典的面试题,涉及到了 Linux fork 机制的核心原理。


1. 通信概览

组件 角色 核心类 物理路径
AMS 客户端 ZygoteProcess /dev/socket/zygote
Zygote 服务端 ZygoteServer 监听套接字请求

2. 核心原因:fork() 与多线程的冲突

这是不使用 Binder 的最根本原因。Binder 机制是基于多线程的(每个 Binder 服务都有线程池),而 Linux 的 fork() 函数有以下特性:

  • 只拷贝调用线程:当一个多线程程序调用 fork() 时,新进程中只有发起调用的那个线程是存活的,其他线程都会瞬间消失。
  • 状态不一致(死锁风险)
    • 如果父进程中的某个线程在 fork 发生时正持有一把锁(Mutex)。
    • 在子进程中,那个持有锁的线程已经消失了,但锁的状态(Locked)依然被拷贝到了子进程的内存中。
    • 结果:子进程中没有任何人能释放这把锁,任何尝试获取该锁的操作都会导致永久死锁

Binder 的复杂性:Binder 驱动内部维护了极其复杂的线程状态、引用计数和锁。如果 Zygote 使用 Binder,在 fork 瞬间,子进程几乎必然会陷入某种形式的死锁或状态混乱。


3. Binder 句柄与内存映射问题

  • 文件描述符共享fork 会拷贝文件描述符表。如果子进程继承了 Zygote 的 Binder 句柄,它们将共享同一个 /dev/binder 的连接上下文。这会导致多个应用进程在内核层“打架”,无法区分彼此的身份。
  • 映射区污染:Binder 依赖 mmap 分配内存。子进程继承父进程的内存映射后,由于 Binder 驱动对内存地址有严格的管理逻辑,这种继承会导致接收缓冲区冲突。

4. 为什么 LocalSocket 是安全的?

  1. 协议简单:Socket 通信是基于流的,不涉及复杂的线程池管理。
  2. 连接独立:Zygote 在监听到 Socket 请求后,先进行 fork。子进程诞生后,可以立即关闭继承来的 Socket 连接,从而保证环境的纯净。
  3. 无锁化设计:Zygote 的 ZygoteServer 采用单线程循环监听,避免了并发冲突,完美契合了 fork 机制对简单环境的要求。

5. 总结

通信方式 适用场景 为什么不选它做进程孵化?
Binder 普通业务 IPC 多线程模型与 fork 存在死锁冲突;驱动状态难以重置。
LocalSocket 进程启动请求 单线程模型,环境简单,fork 安全性高。

一语道破: Zygote 的使命是“孵化”出一个纯净、稳定的进程环境。为了避开多线程 fork 带来的死锁“地雷”,它选择了最稳妥、最原始的 Socket 通信。

Android Framework 专项 - GUI 渲染系统核心机制

Android 的 GUI 渲染系统是一个高度复杂的异步系统,其核心任务是将应用绘制的原始数据(Buffer)最终显示在物理屏幕上。该系统的基石是 SurfaceFlingerHardware Composer (HWC)BufferQueue


1. 生产者-消费者模型:BufferQueue

BufferQueue 是 Android 图形架构的心脏,它连接了图形数据的生产者(应用进程)和消费者(SurfaceFlinger)。

核心组件 职责描述 对应角色
Producer (生产者) 通常是应用进程,通过 Canvas、OpenGL ES 或 Vulkan 渲染数据并“入队”。 Surface / GraphicBufferProducer
Consumer (消费者) 通常是 SurfaceFlinger,负责从队列中“出队”缓冲区进行合成。 SurfaceFlinger / GraphicBufferConsumer
BufferQueue 管理缓冲区的流转逻辑,处理同步和排队。 中间协调层

流程简述:

  1. Dequeue: 生产者申请一块可用的缓冲区。
  2. Queue: 生产者绘制完成后,将缓冲区还给队列。
  3. Acquire: 消费者从队列中获取该缓冲区。
  4. Release: 消费者合成完毕后,释放缓冲区供生产者重用。

2. 图形合成之王:SurfaceFlinger

SurfaceFlinger 是系统级服务,负责接收多个来源的图形 Buffer,并按照 Z-Order 合成到同一个帧缓冲区(FrameBuffer)中。

核心任务 技术细节
Layer 管理 每个窗口在 SurfaceFlinger 中对应一个 Layer(层)。
VSync 响应 监听硬件 VSync 信号,驱动整个合成流程(处理 Input、动画、绘制、合成)。
合成策略 决定使用 GPU (Client Composition) 还是 HWC (Device Composition) 进行合成。

3. 硬件合成加速:Hardware Composer (HWC)

HWC 是 Android 硬件抽象层(HAL)的一部分,旨在减轻 GPU 的负担。

  • **GPU 合成 (Client)**:所有 Layer 由 GPU 绘制到一个 Buffer,SF 将该 Buffer 送显。耗电且占用显存。
  • HWC 合成 (Device):SF 告知 HWC 所有的 Layer 及其坐标,由屏幕控制器直接将多块内存叠加显示。极度节省功耗

HWC 决策逻辑:
SurfaceFlinger 会询问 HWC:这些 Layer 你能处理吗?HWC 会根据硬件能力(层数限制、缩放限制等)返回哪些层它自己合,哪些需要 SF 用 GPU 准备好。


4. 屏幕刷新与 VSync 信号

VSync(垂直同步信号)是保证图形不掉帧、不撕裂的核心机制。

  1. Choreographer:应用端的“节拍器”。它收到 VSync 后触发 View 的 doFrame(Measure/Layout/Draw)。
  2. **Triple Buffering (三缓冲)**:为了解决双缓冲在 GPU/CPU 耗时较长时导致的掉帧问题,Android 引入三缓冲,增加了一块缓冲空间作为“临时仓库”。
  3. 帧调度
    • VSync-app:提醒应用开始产出 Buffer。
    • VSync-sf:提醒 SurfaceFlinger 开始合成 Buffer。

5. 总结

关键术语 一句话定义
Surface 应用端的绘图表面,持有 BufferQueue 的生产者端。
GraphicBuffer 真实的内存空间,存储像素数据。
HWC 硬件合成器,能用低功耗方式“拼图”的专用芯片。
VSync 整个 GUI 系统的跳动心脏,同步产出与消费的节奏。

Android Framework 专项 - NNAPI 核心机制

Android Neural Networks API (NNAPI) 是一个 Android C API,旨在运行计算密集型操作进行机器学习。NNAPI 为更高级别的机器学习框架(如 TensorFlow Lite 和 Caffe2)提供了一个基础层。


1. NNAPI 的核心职责

NNAPI 的主要作用是利用设备上的加速器(如 GPU、DSP 和 NPU)来加速机器学习模型的推断过程。

职责 详细描述
设备发现与选择 自动发现设备上的加速硬件,并根据模型需求选择最合适的驱动。
计算分发 将复杂的机器学习算子(Operators)分发给对应的硬件加速器(NPU/GPU/DSP)。
内存管理 优化模型数据在不同处理器之间的传输效率,支持内存映射(mmap)。
执行编排 协调模型编译、执行请求,并返回计算结果。

2. NNAPI 的分层架构

NNAPI 在 Android 系统中处于承上启下的位置:

  1. **应用层 (App Layer)**:使用 TFLite 等框架编写机器学习应用。
  2. **框架层 (Framework Layer)**:TFLite 通过 NNAPI 接口与系统通信。
  3. **系统服务层 (NNAPI Runtime)**:负责加载驱动、验证模型、分发计算任务。
  4. **硬件抽象层 (HAL Layer)**:厂商提供的驱动程序(HIDL/AIDL 接口),直接操控 NPU 等硬件。

3. 如何将任务分发给 NPU?

NNAPI 将任务分发给硬件加速器的核心流程如下:

关键步骤 执行角色 核心行为
1. 获取设备 ANeuralNetworks_getDevice 查询系统内支持的加速设备(如 Qualcomm NPU)。
2. 模型编译 ANeuralNetworksCompilation 将 TFLite 模型转换为特定硬件支持的指令集(Binary)。
3. 内存映射 ANeuralNetworksMemory 将模型权重数据映射到共享内存,减少数据拷贝开销。
4. 异步执行 ANeuralNetworksExecution 发起计算请求,驱动程序在 NPU 上运行模型,结果通过回调返回。

4. 为什么需要 NNAPI?

  • 性能加速:NPU 处理张量运算的效率远高于 CPU,功耗更低。
  • 降低延迟:本地处理 AI 任务,无需上传云端,保护隐私且实时性强。
  • 硬件无关性:开发者只需对接 NNAPI 接口,无需关心底层是哪家厂商的 NPU 驱动。

5. 总结

关键术语 定义与作用
NPU (Neural Processing Unit) 专门为神经网络运算设计的硬件加速器。
TFLite Delegate 代理机制,允许 TFLite 将计算任务“委托”给 NNAPI 处理。
Operand (操作数) NNAPI 中的基本数据单位(如张量、标量)。

NNAPI 是 Android 迈向端侧人工智能(On-device AI)的核心组件,它通过标准化的分发逻辑,屏蔽了硬件差异,让 AI 应用能够在不同设备上高效运行。

Android HWUI 硬件加速原理解析

Android HWUI 硬件加速原理解析:从架构深度看渲染效率

在深入分析 Bitmap 的物理内存映射与“首次触碰”惩罚后,我们必须回答一个核心架构问题:这些像素是如何跨越进程、突破 CPU 瓶颈,最终呈现在屏幕上的?

Android 没有直接让开发者操作 OpenGL,而是构建了 HWUI(Hardware UI)系统。本文将深度对比 HWUI 与游戏引擎、动画引擎的性能优化差异,揭示其底层设计的精妙之处。


1. 架构哲学的巅峰对决:Proactive vs Reactive

1.1 游戏引擎:主动式即时渲染 (Proactive Rendering)

游戏引擎(Unity/UE)是为高度动态的场景设计的。

  • Main Loop 驱动:引擎运行在一个高频死循环中。每一帧都要经历 输入 -> 脚本逻辑 -> 物理模拟 -> 场景图遍历 -> 材质排序 -> 提交 GPU
  • 每一帧都是“重新构建”:虽然引擎有 Command Buffer 重用技术,但其本质是主动评估。即使场景静止,CPU 依然在进行视锥剔除(Frustum Culling)和状态检查。
  • 性能权衡:这种架构能处理极其复杂的 3D 变换,但对移动端而言,它意味着 CPU 无法进入 C-State 深度睡眠,是电量杀手

1.2 Android HWUI:被动式保留渲染 (Reactive / Retained Mode)

Android UI 是为局部更新长时间静止设计的。

  • DisplayList 编译机制:View 的 onDraw 不产生像素,而是将 Canvas API 映射为二进制指令集(DisplayList)。这类似于一种“中间语言”。
  • RenderNode 属性化:RenderNode 封装了 DisplayList 及其变换属性(Alpha, Matrix, Clip)。平移、缩放动画只需修改 RenderNode 的属性,完全不触发 onDraw
  • 核心优势真正的零负载静止。当画面不动时,UI 线程和 RenderThread 都会挂起,直到下一个 VSync 信号或输入事件触发 invalidate()

2. 双线程异步架构:源码级的深度协同

HWUI 的核心性能保障源于 Android 5.0 引入的任务拆分模型。其核心代码位于 Native 层的 frameworks/base/libs/hwui/

2.1 录制阶段 (UI Thread - CPU 密集)

当调用 invalidate() 时,UI 线程遍历 View Tree:

  1. **Record (录制)**:调用 DisplayListCanvas 记录绘制操作。此时并不执行绘图,只是在内存中追加指令。
  2. Sync (同步点):在 VSync 到达时,UI 线程会触发 CanvasContext::prepareTree。此时 UI 线程会阻塞极短的时间,将最新的 DisplayList 和资源句柄(如 Bitmap 指针)“同步”给 RenderThread。这是两个线程唯一需要加锁同步的时刻。

2.2 回放阶段 (RenderThread - GPU 密集)

RenderThread 是一个独立的 Native 线程(源码见 RenderThread.cpp),它是整个 App 进程中唯一拥有 GPU 上下文的线程。

  • 独立的 EGL Context:在 EglManager::initialize() 中,HWUI 会为 RenderThread 创建专有的 EGL Context。
    • 独立性:这意味着 UI 线程的任何卡顿(如大量的 Java 逻辑计算)都不会直接导致 GPU 驱动层面的阻塞。
  • **指令重排 (Deferred Rendering)**:在 BakedOpRendererFrameBuilder 中,HWUI 会对 DisplayList 进行“二次编译”。它不按顺序执行,而是根据材质(Texture/Shader)对指令进行重排,合并 Draw Call。
  • 异步纹理上传:RenderThread 利用 Uploader 在后台静默将 Bitmap 上传到 GPU 纹理,避开了 UI 线程的负载。

3. 文本与动画引擎的优化差异

优化点 游戏引擎 (SDF 方案) 动画引擎 (Lottie/Flare) Android HWUI
文字质量 使用有向距离场 (SDF),缩放不模糊但极小字号下边缘发虚。 依赖底层的 Canvas 实现。 Skia 栅格化 + 图集缓存。CPU 负责产生高清 Alpha 掩码,GPU 负责合成,达到印刷级精度。
动画驱动 脚本每一帧计算。 关键帧插值,通常在 CPU 计算路径。 RenderThread 属性动画。硬件加速动画(如 Alpha)在渲染线程闭环,UI 线程卡顿也不影响动画流畅度。
路径渲染 昂贵的几何拆分(Tessellation)。 大量 CPU 顶点计算。 **路径预掩码 (Path Masking)**。HWUI 倾向于将复杂 Path 缓存为临时纹理,而非实时拆分三角形。

4. 跨进程的“终极快车道”:HWC (Hardware Composer)

HWUI 产生的像素 Buffer 并不会直接刷到屏幕,而是交给系统的 SurfaceFlinger

  • GPU 合成 (GL Composition):早期的 SurfaceFlinger 用 GPU 把所有窗口画到一个 Buffer 里。缺点:费电,占用大量显存带宽。
  • **HWC 硬件合成 (Overlay)**:手机芯片中有一个专门的硬件模块(Display Controller)。
    • **Zero-copy (零拷贝)**:SurfaceFlinger 告诉 HWC:“这块 Buffer 是状态栏,那块是 App 内容。”
    • 直接读取:HWC 硬件在扫描线输出到屏幕的一瞬间,直接从两块内存中读取像素并叠加。GPU 完全不参与,甚至可以处于断电状态。

5. 性能陷阱:为什么硬件加速下还是会“掉帧”?

即使有 HWUI 保护,以下操作依然会击穿性能底线:

  1. **隐式 Readback (回读)**:在 onDraw 中调用 canvas.copySurfaceToBitmap()。这会导致 GPU 管道流水线(Pipeline)彻底清空(Flush)以等待像素返回,产生严重的 CPU 阻塞(源码见 GlLayer::copyback)。
  2. **过度录制 (Re-recording)**:在动画里频繁修改 View 的内容(如 textView.setText)而非属性(如 setTranslationX),迫使 UI 线程每一帧都执行 onDraw 和指令录制。
  3. **纹理抖动 (Texture Thrashing)**:单帧内绘制的 Bitmap 总量超过了 GPU 纹理缓存限制(通常为几百 MB),导致 RenderThread 不断地在丢弃和重传纹理。

6. 总结:HWUI 的本质

HWUI 并不是一个通用的 3D 渲染器,它是一个深度理解 UI 规律的编译器

  • 它利用 Retained Mode 减少重复工作(跳过未变动的 View)。
  • 它利用 RenderThread 屏蔽驱动层面的波动(Jitter)。
  • 它利用 Skia/Vulkan 后端平衡了跨平台的一致性与极致的硬件性能。

对于开发者而言,理解 “录制 (UI Thread) -> 同步 (Sync) -> 回放 (RenderThread) -> 合成 (HWC Overlay)” 这一全链路,是进行深度 UI 优化的前提。

Android Bitmap 创建流程深度分析

本文将分析从 Java 层 Bitmap.createBitmap 调用开始,经过 JNI 层 Bitmap_creator,最终到达 Native 层 allocateHeapBitmap 的完整创建流程,并深入探讨内存分配函数选择及性能耗时点。

1. Java 层:Bitmap.createBitmap

在 Java 层,用户通常通过 Bitmap.createBitmap(...) 静态方法创建位图。

核心代码路径: frameworks/base/graphics/java/android/graphics/Bitmap.java

以最常见的创建方法为例:

1
2
3
public static Bitmap createBitmap(int width, int height, @NonNull Config config, boolean hasAlpha) {
return createBitmap(null, width, height, config, hasAlpha);
}

经过一系列参数检查后,它会调用 native 方法:

1
2
Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
colorSpace == null ? 0 : colorSpace.getNativeInstance());

nativeCreate 是一个 JNI 方法,它将复杂的创建请求传递给 Native 层。


2. JNI 层:Bitmap_creator

nativeCreate 在 C++ 中对应 Bitmap_creator 函数。

核心代码路径: frameworks/base/libs/hwui/jni/Bitmap.cpp

2.1 关键逻辑分析

static jobject Bitmap_creator(...) 是 JNI 调用的入口点。其核心步骤如下:

  1. 参数映射:将 Java 的 Config 映射为 Skia 的 SkColorType
    1
    SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
  2. 色彩空间处理:获取 Native 层的色彩空间对象。
  3. 构建 SkBitmap 元数据:创建一个 SkBitmap 对象并设置其 SkImageInfo(包含宽高、颜色类型、Alpha 类型和色彩空间)。
  4. 调用 Native 分配器
    1
    sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap);
  5. 包装与返回:分配成功后,通过 createBitmap (JNI helper) 将 Native 层的 Bitmap 指针包装成 Java 层的 Bitmap 对象返回。

3. Native 层:allocateHeapBitmap 与内存分配

3.1 内存分配实现

真正的堆内存分配发生在 Bitmap::allocateHeapBitmap(size_t size, ...) 中:

1
2
3
4
5
6
7
8
sk_sp<Bitmap> Bitmap::allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
// 使用 calloc 分配内存
void* addr = calloc(size, 1);
if (!addr) {
return nullptr;
}
return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}

3.2 calloc vs malloc:为什么选择 calloc?

在 Bitmap 创建中,Android 选择 calloc 而非 malloc 有以下考虑:

特性 malloc calloc
初始化 分配的内存内容是随机的(脏内存)。 分配后自动将内存清零。
性能 速度稍快,因为它只标记内存可用,不处理内容。 速度稍慢,因为需要遍历并清零内存页。
安全性 存在隐私泄露风险,可能读取到其他进程留下的残留数据。 更安全,确保初始状态为“干净”的透明或黑色。
Bitmap 意义 必须手动调用 memset 或填充像素,否则位图会有花屏。 天然满足需求,确保新建位图内容确定。

4. 深度疑难解答:为什么 calloc 慢,而后续操作快?

这是一个非常经典的问题:既然 calloc 内部也是在“清空”内存,那为什么它比我们手动调用 Canvas.drawColor 或者 eraseColor 慢得多?

4.1 “First-Touch”(首次触碰)惩罚

calloc 分配的内存通常是新鲜的虚拟内存。当你执行 calloc 时,内核并不会立即给你真正的物理内存。

  1. 内核零页分配:内核需要找到物理页。为了安全,内核必须亲自将这些物理页清零(防止你看到上一个 App 的隐私)。
  2. **缺页中断 (Page Faults)**:当 calloc 开始写入零值时,每跨越 4KB(一个内存页),硬件就会触发一次缺页中断,陷入内核,由内核分配物理页并建立映射。
  3. CPU 计算:这一切都是在 CPU 上串行完成的。

4.2 后续 Clear/Draw 为什么快?

当你已经创建好 Bitmap,调用 bitmap.eraseColor(Color.TRANSPARENT) 或在上面绘图时:

  1. 内存已驻留:物理内存页已经分配好了,映射关系已经建立,不再有缺页中断
  2. GPU 加速:如果开启了硬件加速,eraseColor 最终可能转换成一条 GPU 指令(如 OpenGL 的 glClear 或 Vulkan 的 Clear 指令)。GPU 拥有数百个核心和极高的内存带宽,清空几个 MB 的数据只需要几微秒。
  3. 缓存友好:即使是 CPU 绘图,此时内存已经由于之前的操作被“预热”在 CPU 缓存中,写入速度远高于触发内核行为。

4.3 总结:Bitmap 究竟耗时在哪里?

Bitmap 创建的性能瓶颈不在于数据的量,而在于 CPU 与内核的“交互成本”

  1. 内核页表锁定:内核分配内存时需要持有复杂的锁。
  2. 安全性强制清零:内核强制在 CPU 上同步清零新分配的物理页,这是无法通过硬件加速绕过的“安全成本”。
  3. 上下文切换:从用户态到内核态的频繁切换(由缺页中断引起)。

5. 结论

Android Bitmap 的创建流程是一个从上层策略到底层执行的转换过程。

  • 耗时根源:主要不在于 C++ 对象的创建,而在于大内存块的物理分配(缺页中断)与内核级清零操作
  • 优化建议:在高性能要求的场景下(如列表滚动),应尽量**重用 Bitmap (BitmapPool)**。重用已有 Bitmap 相当于重用了已经建立好映射关系的物理内存,避开了“首次触碰”带来的缺页中断和内核同步清零,从而获得极致的性能。

C++ 第一课-万物皆地址

作者:朕小猫与GPT4

怎么理解C++万物皆地址

在 C++ 程序中,几乎所有元素(包括变量、对象、函数等)都可以通过它们的内存地址来引用或操作。这一点归根结底是因为 C++ 是一种底层的编程语言,它允许直接访问内存和底层硬件资源。这种能力带来了极高的灵活性和控制能力,但同时也要求程序员对程序的内存使用有深入的理解。下面是如何通过内存地址来引用或操作元素的几个方面:

  1. 变量和对象的内存地址

    在 C++ 中,每个变量或对象都占据内存中的一个或多个连续位置。你可以使用 & 操作符获得一个变量的内存地址,这个地址可以赋给一个指针变量。通过指针,你可以读取或修改存储在该地址的数据。这对于数组和自定义数据结构尤其有用,因为可以通过指针算术进行高效的遍历和访问。

  2. 函数的地址

    C++ 允许获取函数的地址,并将其赋给函数指针。这使得程序可以在运行时决定调用哪个函数,提高了代码的灵活性和可重用性。函数指针广泛应用于回调机制、事件驱动编程和接口设计中。

  3. 引用

    C++ 的引用机制实际上是一种特殊的语法糖,它在内部使用地址来实现。引用允许你创建一个变量的别名,通过别名可以直接访问和修改原始变量,而无需显式解引用。这样不仅代码更加简洁,而且避免了指针可能带来的错误。

  4. 动态内存分配

    C++ 通过 newdelete 操作符提供了动态内存分配的能力。使用 new 分配的内存位于堆区,它会返回指向分配内存的指针。通过这个指针,程序可以在运行时动态地分配、访问和释放内存。

通过上述机制,C++ 程序可以精确地控制内存的使用方式,从而优化程序性能和资源管理。然而,这也意呀着程序员必须对指针、内存地址以及内存管理有充分的理解,才能有效地利用这些特性编写高效、安全的代码。

编写教程时,目标是清晰、简洁且具有实用性。以下是一个关于“如何在 C++ 中使用指针和地址”教程的示例内容。这段内容旨在介绍初学者到 C++ 的指针和地址概念,帮助他们理解和运用“万物皆地址”的原则。


C++ 指针和地址入门

在 C++ 中,理解指针和地址是掌握语言的基石。本教程将引导你通过简单的步骤来理解和使用指针和地址。

1. 理解地址

在 C++ 中,每个变量都存储在内存的一个特定位置,这个位置可以通过地址来访问。你可以使用 & 操作符来获取变量的地址。

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;

int main() {
int var = 10;
cout << "变量的地址: " << &var << endl;
return 0;
}

这段代码会输出变量 var 的内存地址。

2. 使用指针

指针是存储另一个变量地址的变量。在 C++ 中,你可以使用 * 来声明指针。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;

int main() {
int var = 10;
int *ptr = &var;
cout << "ptr 指向的地址: " << ptr << endl;
cout << "ptr 指向的地址上的值: " << *ptr << endl;
return 0;
}

在这个例子中,ptr 是一个指向 var 地址的指针。通过 *ptr,我们可以访问这个地址上的值。

3. 指针的使用场景

指针在 C++ 中的应用非常广泛,包括:

  • 动态内存管理:使用 newdelete 操作符分配和释放内存。
  • 函数参数传递:通过传递指针或引用来修改函数外的变量。
  • 构建复杂数据结构:如链表、树等。

4. 练习:使用指针交换两个变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

void swap(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}

int main() {
int a = 5, b = 10;
swap(&a, &b);
cout << "a: " << a << ", b: " << b << endl;
return 0;
}

这个练习演示了如何使用指针来交换两个变量的值。

C++ 通过地址修改值

在 C++ 中,通过地址修改值是指针操作的一个基本用途。指针是一种特殊的变量,其值为另一个变量的内存地址。
通过指针,你可以直接访问和修改它指向的内存位置上存储的数据。
这一过程涉及几个关键步骤:获取变量的地址、使用指针指向该地址、通过指针修改该地址上的值。
下面通过一个简单的示例来说明这一过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int main() {
int value = 10; // 定义一个变量value并初始化为10
int *ptr = &value; // 定义一个指针ptr,将其初始化为value的地址

cout << "Original value: " << value << endl; // 显示原始的value值

*ptr = 20; // 通过指针ptr修改value的值

cout << "Modified value: " << value << endl; // 显示修改后的value值

return 0;
}

在上述代码中:

  1. 我们首先定义了一个类型为 int 的变量 value 并初始化为 10。
  2. 然后,我们定义了一个类型为 int* 的指针 ptr 并将其初始化为 value 的地址(&value)。
  3. 通过 cout 输出语句,我们可以看到 value 的原始值。
  4. 接着,我们通过指针 ptr 来修改 value 的值。这里的 *ptr = 20; 表示将 ptr 指向的内存位置(即 value 的位置)上的数据修改为 20。*ptr 是解引用操作符的应用,它获取指针指向的内存地址上存储的值。
  5. 最后,再次通过 cout 输出 value 的值,可以看到它已经被修改为 20。

函数传参的内存地址

在 C++ 中,理解函数传参的内存地址涉及到两个主要概念:按值传递(Pass by Value)和按引用传递(Pass by Reference)。理解这些概念有助于深入理解 C++ 如何在函数调用中处理参数的内存地址。

按值传递(Pass by Value)

当函数参数是按值传递时,函数接收的是实参的一个副本。这意味着函数内部对参数所做的任何修改都不会影响到原始数据。在内存层面,这个过程涉及到将原始数据的值复制到新的内存地址中。这个新的地址是函数参数在函数调用栈上的局部地址。

优点

  • 保护了原始数据,避免了意外修改。
  • 对于基本数据类型,这种方式简单且效率较高。

缺点

  • 对于大型结构或类实例,复制可能导致性能下降。
  • 无法在函数外部反映函数内部对数据的修改。

按引用传递(Pass by Reference)

按引用传递意味着函数接收的是实参的引用(或者说是内存地址)。这样,函数内部对参数的任何修改都会直接影响到原始数据。在内存层面,这避免了数据的复制,函数参数直接使用了实参的地址。

优点

  • 可以直接修改原始数据。
  • 避免了大型数据结构的复制,提高了效率。
  • 可以通过返回多个结果值(通过修改传入的引用或指针参数)。

缺点

  • 如果不希望修改原始数据,需要谨慎操作。
  • 使用不当可能导致错误或数据损坏。

举个例子

假设我们有一个简单的函数,目的是修改一个整数的值。

1
2
3
4
5
6
7
8
9
10
11
// 按值传递
void addTenByValue(int number) {
number += 10;
// 这里修改的是number的副本,外部的原始变量不受影响
}

// 按引用传递
void addTenByReference(int &number) {
number += 10;
// 这里直接修改的是传入变量的值,外部的原始变量也会被修改
}

在这个例子中,addTenByValue 函数无法修改外部变量的值,因为它仅操作了参数的副本。而 addTenByReference 函数则直接操作了实参的内存地址,因此它能够修改外部变量的值。

C++ 函数指针与指针函数,C++11 新特性

作者:朕小猫与GPT4

C++函数指针

在 C++ 中,函数指针是指向函数的指针,允许通过指针调用函数。这种特性让程序在运行时能够选择要调用的函数,增加了代码的灵活性和动态性。理解函数指针对于高级编程技巧,如回调函数、命令模式等是非常重要的。

定义函数指针

函数指针的定义需要指定函数的返回类型、指针名称以及函数参数的类型。定义函数指针的基本语法如下:

1
返回类型 (*指针变量名)(参数类型列表);

例如,如果你有一个返回类型为 int 并接受两个 int 类型参数的函数,你可以这样定义一个指向该函数的指针:

1
int (*funcPtr)(int, int);

使用函数指针

一旦定义了函数指针,就可以将其指向具有相应签名的任何函数,然后通过该指针调用函数。这里有一个使用函数指针的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

// 定义一个简单的函数
int add(int x, int y) {
return x + y;
}

int main() {
// 定义一个函数指针并使其指向add函数
int (*funcPtr)(int, int) = add;

// 通过函数指针调用函数
int sum = funcPtr(2, 3);

std::cout << "The sum is: " << sum << std::endl;

return 0;
}

函数指针的用途

函数指针最常见的用途包括:

  • 回调函数:允许库或框架调用在使用库的代码中定义的函数。
  • 事件处理:在发生特定事件时动态决定调用哪个函数。
  • 接口实现:通过函数指针数组或结构体实现类似于面向对象编程中接口或虚函数的功能。
  • 命令模式:将操作封装为对象,允许存储、传递和调用操作。

高级话题

随着 C++11 的引入,现代 C++ 更倾向于使用 std::function 和 lambda 表达式来替代裸函数指针,因为它们提供了更高的灵活性和更简洁的语法。例如,std::function 可以存储并调用任何可调用的实体,包括普通函数、lambda 表达式、以及其他具有 operator() 的对象。

举例子,说明函数指针使用的几种场景

函数指针在 C++ 中的应用非常广泛,提供了编程的灵活性和动态性。以下是几种典型场景,展示了函数指针的使用:

1. 回调函数

回调函数是由用户编写的,但由系统或库在适当的时候调用的函数。函数指针允许用户提供具体的回调函数实现,使得库或框架可以在运行时调用用户定义的代码。

示例: 设计一个简单的事件处理器,当发生某个事件时,调用用户提供的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <vector>

// 定义回调函数类型
using Callback = void(*)(int);

// 事件处理器类
class EventProcessor {
public:
void registerCallback(Callback cb) {
callbacks.push_back(cb);
}

void triggerEvent(int eventData) {
for (auto& cb : callbacks) {
cb(eventData); // 调用回调函数
}
}

private:
std::vector<Callback> callbacks; // 回调函数列表
};

// 用户定义的回调函数
void onEventTriggered(int data) {
std::cout << "Event triggered with data: " << data << std::endl;
}

int main() {
EventProcessor ep;
ep.registerCallback(onEventTriggered); // 注册回调函数
ep.triggerEvent(42); // 触发事件,调用回调函数
return 0;
}

2. 函数指针数组

函数指针数组可以存储指向不同函数的指针,使得程序可以基于运行时决策调用不同的函数。

示例: 创建一个简单的菜单系统,用户输入不同的选项执行不同的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

void function1() {
std::cout << "Executing Function 1" << std::endl;
}

void function2() {
std::cout << "Executing Function 2" << std::endl;
}

void function3() {
std::cout << "Executing Function 3" << std::endl;
}

int main() {
void(*functions[3])() = {function1, function2, function3};

int choice;
std::cout << "Enter a choice (0-2): ";
std::cin >> choice;

if (choice >= 0 && choice < 3) {
(*functions[choice])(); // 调用选中的函数
} else {
std::cout << "Invalid choice" << std::endl;
}

return 0;
}

3. 接口实现和策略模式

通过函数指针,可以模拟面向对象编程中的接口或策略模式,允许在运行时选择不同的算法或行为。

示例: 设计一个简单的排序策略,用户可以选择不同的排序算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <algorithm>
#include <vector>
#include <iostream>

using SortAlgorithm = void(*)(std::vector<int>&);

void ascendingSort(std::vector<int>& data) {
std::sort(data.begin(), data.end());
}

void descendingSort(std::vector<int>& data) {
std::sort(data.begin(), data.end(), std::greater<int>());
}

void sortData(std::vector<int>& data, SortAlgorithm algorithm) {
algorithm(data); // 使用指定的排序算法
}

int main() {
std::vector<int> data = {4, 2, 5, 1, 3};

sortData(data, ascendingSort);
for (int n : data) std::cout << n << " ";
std::cout << std::endl;

sortData(data, descendingSort);
for (int n : data) std::cout << n << " ";
std::cout << std::endl;

return 0;
}

这些示例展示了函数指针在回调、决策和策略选择等不同编程场景中的应用,证明了其为 C++ 编程带来的灵活性和动态性。

使用 std::function 和 lambda 表达式来替代裸函数指针,举例子

std::function 和 lambda 表达式在 C++11 及之后的版本中提供了一种更加灵活和方便的方式来处理可调用对象。它们使得代码更加简洁且易于理解,特别是在需要函数作为参数或回调时。

Lambda 表达式

Lambda 表达式提供了一种匿名函数的实现,允许你在需要函数对象的地方快速定义一个函数。Lambda 表达式的基本语法如下:

1
2
3
[捕获列表](参数列表) -> 返回类型 {
函数体
};

使用 std::function 和 Lambda 表达式

std::function 是一个模板类,它可以用来存储任何可以调用的目标,如普通函数、Lambda 表达式、成员函数和函数对象。这提供了比裸函数指针更大的灵活性和功能。

下面是一个示例,展示了如何使用 std::function 和 Lambda 表达式来替代裸函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <functional> // 必须包含这个头文件

// 定义一个普通的函数
int add(int x, int y) {
return x + y;
}

int main() {
// 使用 std::function 存储普通函数
std::function<int(int, int)> func = add;
std::cout << "The sum using a normal function: " << func(2, 3) << std::endl;

// 使用 std::function 存储 Lambda 表达式
std::function<int(int, int)> lambdaFunc = [](int x, int y) -> int {
return x + y;
};
std::cout << "The sum using a lambda expression: " << lambdaFunc(2, 3) << std::endl;

return 0;
}

在这个示例中,我们首先定义了一个普通的函数 add。然后,我们创建了两个 std::function 对象,一个用来存储普通函数 add,另一个用来存储一个实现相同功能的 Lambda 表达式。通过这种方式,我们可以看到 std::function 和 Lambda 表达式如何提供了一种更加灵活的方式来处理可调用对象,而不必直接使用函数指针。

优点

  • std::function 可以存储任何类型的可调用对象,提供了极大的灵活性。
  • Lambda 表达式使得定义和使用匿名函数变得简单,特别是在作为一次性使用函数或局部函数时非常有用。
  • 使用 std::function 和 Lambda 表达式可以使代码更加清晰和易于维护,特别是在复杂的项目中。

通过这种方式,C++11 提供的这些特性让处理函数和回调等场景变得更加优雅和高效。

C ++ 指针函数

在 C++ 中,术语“指针函数”通常被误解或使用不当。实际上,人们通常是指返回指针的函数,或者简称为“函数返回指针”。这种类型的函数在使用时非常有用,尤其是当你需要从函数中返回动态分配的内存或数组、多级指针、或者指向不同数据结构的指针时。

定义返回指针的函数

要定义一个返回指针的函数,你需要在函数声明的返回类型前加上*来指明返回的是指针。这里有一个简单的例子,说明如何定义和使用返回指针的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

// 定义一个返回int指针的函数
int* getIntPtr() {
int* ptr = new int(10); // 动态分配内存
return ptr; // 返回指向int的指针
}

int main() {
int* myPtr = getIntPtr(); // 调用函数,获取返回的指针
std::cout << "Value: " << *myPtr << std::endl; // 使用指针访问值
delete myPtr; // 释放分配的内存
return 0;
}

在这个例子中,getIntPtr函数分配了一个int类型的动态内存,并返回一个指向这块内存的指针。在main函数中,我们调用了getIntPtr,并使用返回的指针来访问和操作内存。最后,不要忘记使用delete释放动态分配的内存,以避免内存泄露。

注意事项

  • 返回局部变量的地址是危险的,因为当函数执行完毕后,局部变量的存储空间会被回收,这样返回的指针会指向一个不再有效的内存区域,导致未定义行为。
  • 当你的函数返回指向动态分配内存的指针时,确保在适当的时候释放这些内存,以避免内存泄露。
  • 考虑使用智能指针(如std::unique_ptrstd::shared_ptr)来管理动态分配的内存,这样可以自动管理内存的生命周期,减少内存泄露的风险。

返回指针的函数提供了一种灵活的方式来创建和管理动态资源,但同时也需要谨慎处理指针和内存管理相关的问题。

如何使用智能指针(如std::unique_ptr或std::shared_ptr)来管理动态分配的内存

在 C++ 中,智能指针是一种模板类,用于自动管理动态分配的内存,确保资源在不再需要时被正确释放,从而避免内存泄露。C++11 引入了两种主要的智能指针类型:std::unique_ptrstd::shared_ptr,它们分别用于独占所有权和共享所有权的场景。

使用 std::unique_ptr

std::unique_ptr 是一种独占所有权的智能指针,保证同一时间只有一个 std::unique_ptr 指向特定的资源。当 std::unique_ptr 被销毁或被重新分配时,它指向的对象也会被自动删除。

基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <memory>
#include <iostream>

class MyObject {
public:
MyObject() { std::cout << "MyObject created\n"; }
~MyObject() { std::cout << "MyObject destroyed\n"; }
void myMethod() { std::cout << "MyMethod called\n"; }
};

int main() {
std::unique_ptr<MyObject> myUniquePtr = std::make_unique<MyObject>();
myUniquePtr->myMethod(); // 使用->操作符调用成员函数

// 不需要手动删除对象,当unique_ptr离开作用域时,对象会被自动销毁
return 0;
}

使用 std::shared_ptr

std::shared_ptr 是一种共享所有权的智能指针,允许多个 std::shared_ptr 实例指向同一个对象。内部使用引用计数来跟踪有多少个 std::shared_ptr 指向同一个资源,当最后一个这样的指针被销毁时,所指向的对象也会被删除。

基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <memory>
#include <iostream>

class MyObject {
public:
MyObject() { std::cout << "MyObject created\n"; }
~MyObject() { std::cout << "MyObject destroyed\n"; }
void myMethod() { std::cout << "MyMethod called\n"; }
};

int main() {
std::shared_ptr<MyObject> mySharedPtr1 = std::make_shared<MyObject>();
{
std::shared_ptr<MyObject> mySharedPtr2 = mySharedPtr1; // 共享所有权
mySharedPtr2->myMethod();
// 当mySharedPtr2离开作用域时,对象不会被销毁,因为mySharedPtr1仍然存在
}
// 只有当最后一个指向对象的shared_ptr(这里是mySharedPtr1)离开作用域时,对象才会被销毁
return 0;
}

注意事项

  • 使用智能指针可以减少内存泄露的风险,但仍需要注意循环引用问题,尤其是在使用 std::shared_ptr 时。循环引用会阻止引用计数达到零,导致内存泄露。解决循环引用问题通常使用 std::weak_ptr
  • std::unique_ptr 通过移动语义实现所有权的转移,不能被复制。
  • std::shared_ptr 适用于资源需要被多个所有者共享的情况,但增加了额外的开销(引用计数管理)。

智能指针是现代 C++ 管理动态资源的首选方式,相比裸指针,它们提供了更安全、更简洁的资源管理机制。