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 相当于重用了已经建立好映射关系的物理内存,避开了“首次触碰”带来的缺页中断和内核同步清零,从而获得极致的性能。

Android MVI GPT4 快问快答

MVI 层级架构

  • App 用于依赖注入 —— 可以访问所有层
  • Data 负责内部和外部存储 —— 可以访问 Domain
  • Domain 负责仓库和用例 —— 不访问任何层
  • Presentation 负责UI逻辑 —— 可以访问 Domain

GPT 4 块问快答:究竟什么是 MVI

为什么 Data 和 Presentation 可以访问领域层,举例子怎么访问的

在一个典型的分层架构中,每一层都有其独特的职责,而且这种架构通常允许某些层访问其他特定的层。在你提到的结构中,AppDataDomainPresentation层各自担负不同的角色,其中DataPresentation层被允许访问Domain层,主要是因为Domain层包含了应用程序的核心业务逻辑和规则,是其他层依赖的基础。

Data 层访问 Domain 层

  • 职责Data层负责处理数据的存取,无论是来自内部存储(如SQLite数据库、内存缓存等)还是外部存储(如网络API、远程数据库等)。
  • 访问方式Data层访问Domain层通常是通过实现Domain层定义的仓库接口(Repository Interfaces)。这些仓库接口定义了应用程序需要的数据操作(如获取用户数据、保存订单等),而Data层实现这些接口,具体处理数据的存取逻辑。

示例
假设在Domain层有一个UserRepository接口,它定义了获取用户信息的方法。Data层将实现这个接口,通过网络API或本地数据库获取用户信息。

Presentation 层访问 Domain 层

  • 职责Presentation层负责处理所有与用户界面(UI)相关的逻辑,如显示数据、响应用户操作等。
  • 访问方式Presentation层通过使用Domain层中定义的用例(Use Cases)或服务(Services)来获取所需的数据或执行特定的业务逻辑。用例封装了特定的业务逻辑操作,为Presentation层提供了清晰、简洁的API。

示例
如果有一个显示用户个人信息的界面,Presentation层的一个ViewModel或Presenter可能会调用一个GetUserProfile用例(从Domain层获取),该用例封装了获取用户个人信息的业务逻辑。

总结

通过允许DataPresentation层访问Domain层,可以确保应用程序的核心业务逻辑集中管理,并且易于测试和维护。同时,这种结构还有助于解耦,使得数据源的更改或UI的修改不会直接影响到业务逻辑的实现,从而提高了应用程序的灵活性和可维护性。

给出 GetUserProfile 代码例子

为了提供一个GetUserProfile的示例,我们将遵循典型的分层架构中的DomainDataPresentation层的交互方式。这个例子将使用Kotlin语言,因为它是在Android开发中常用的语言,同时也适用于Kotlin多平台移动(KMM)项目。

Domain 层

Domain层,我们定义一个用例(Use Case)GetUserProfile,它是业务逻辑的一部分。这个用例会从仓库(Repository)中获取用户的个人信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Domain层 - 定义用例
class GetUserProfile(private val userRepository: UserRepository) {
suspend fun execute(userId: String): UserProfile {
return userRepository.getUserProfile(userId)
}
}

// Domain层 - 定义仓库接口
interface UserRepository {
suspend fun getUserProfile(userId: String): UserProfile
}

// 用户个人信息的数据模型
data class UserProfile(val userId: String, val name: String, val email: String)

Data 层

Data层,我们实现了Domain层定义的UserRepository接口,提供了获取用户个人信息的实际逻辑。

1
2
3
4
5
6
7
// Data层 - 仓库实现
class UserRepositoryImpl : UserRepository {
override suspend fun getUserProfile(userId: String): UserProfile {
// 假设这里是从网络或本地数据库获取数据
return UserProfile(userId, "John Doe", "johndoe@example.com")
}
}

Presentation 层

Presentation层,我们使用GetUserProfile用例来获取用户个人信息,并展示到UI上。这里以一个简单的ViewModel为例。

1
2
3
4
5
6
7
8
9
10
11
12
// Presentation层 - ViewModel
class UserProfileViewModel(private val getUserProfile: GetUserProfile) : ViewModel() {
// LiveData用于观察数据变化,更新UI
private val _userProfile = MutableLiveData<UserProfile>()
val userProfile: LiveData<UserProfile> = _userProfile

fun loadUserProfile(userId: String) {
viewModelScope.launch {
_userProfile.value = getUserProfile.execute(userId)
}
}
}

小结

这个例子演示了在一个分层架构中,如何通过Domain层的用例来协调Data层和Presentation层的交互。Domain层定义了业务逻辑和接口,Data层实现了数据获取的逻辑,而Presentation层负责调用这些逻辑并更新UI。这种分层的方式有助于保持代码的清晰和可维护性。

描述一下,数据在几个层之间的传递,从起始到结束

数据在分层架构中的传递通常遵循一个定义良好的流程,确保应用的各个组成部分职责明确、解耦合,同时促进模块间的协作。以下是一个典型的从起始到结束的数据传递流程,以一个简化的用户个人信息获取功能为例,该功能遵循PresentationDomainData层的架构模式:

1. 用户界面触发

  • 起始点:用户在应用的界面上执行一个动作(比如点击一个按钮),请求查看个人信息。
  • 处理:这个动作被Presentation层的一个组件(例如,一个ViewModel或Activity)捕获。

2. Presentation 层处理

  • Presentation层的组件调用Domain层定义的用例(Use Case),比如GetUserProfile,来获取用户的个人信息。这个调用通常是异步的,以避免阻塞UI线程。
  • ViewModel通过用例接口,向Domain层表达获取数据的需求。

3. Domain 层中转

  • Domain层的GetUserProfile用例接收到请求,并将其转发给相应的UserRepository接口。此时,GetUserProfile仅知道需要调用仓库接口来获取数据,但不关心数据的具体来源。
  • Domain层起到了定义业务规则和操作流程的角色,但本身不处理数据的存取。

4. Data 层响应

  • Data层中,具体实现了UserRepository接口的类(如UserRepositoryImpl)负责处理数据的实际获取操作。这可能涉及访问网络API、查询本地数据库或读取缓存等操作。
  • 一旦数据被成功获取,它会被封装成一个模型(如UserProfile),并返回给调用者——在这个例子中是Domain层的GetUserProfile用例。

5. 数据回流到Presentation层

  • GetUserProfile用例收到Data层提供的用户信息后,将这些信息返回给最初请求它的Presentation层的组件。
  • Presentation层的ViewModel接收到这些数据后,更新UI绑定的数据模型,通常是通过LiveData或类似机制。

6. UI更新显示数据

  • 最后,随着ViewModel中LiveData(或其他观察者模式实现)的更新,UI监听这些变化,并据此刷新显示,向用户展示最新获取的用户个人信息。

总结

这个过程展示了数据从用户触发获取请求,通过应用的各层传递,并最终显示回用户界面的完整流程。每一层都有其特定的职责:Presentation层管理UI逻辑和用户交互,Domain层定义业务规则和数据获取的抽象接口,而Data层负责具体的数据存取逻辑。这种分层架构不仅有助于保持代码的组织性和可维护性,还促进了各模块间的清晰分离和互操作性。

解释 Domain 层如何定义业务规则

Domain层在分层架构中扮演着核心角色,它定义了应用程序的业务规则和逻辑。这一层是独立于用户界面(UI)和数据存储机制的,意味着它不应该依赖于任何特定的框架或库,以便于业务逻辑能够在不同的环境中重用和测试。Domain层的设计主要关注于应用程序的“什么”和“为什么”,而不是“怎么做”。

业务规则的定义

业务规则是指那些定义企业如何运作、决策应如何做出、业务流程应如何管理的规则。在软件开发中,这些规则转化为可以执行的代码,用于指导应用程序的行为。业务规则包括但不限于:

  • 实体(Entities):代表业务概念的对象,如用户、订单等,以及这些对象的属性和行为。
  • 用例(Use Cases)或服务(Services):封装了应用程序的核心功能,每个用例代表一个或多个业务操作,如“获取用户信息”、“创建订单”等。
  • 值对象(Value Objects):描述了业务中的值和它们的属性,但没有唯一标识符(与实体不同)。
  • 仓库接口(Repository Interfaces):定义了如何访问外部数据源(如数据库或远程服务)的抽象接口,但不实现具体的数据访问逻辑。

Domain层的职责

  • 封装业务逻辑Domain层包含应用程序的业务逻辑,确保业务操作的正确性、有效性和连贯性。
  • 定义业务模型:通过实体和值对象定义业务领域内的数据模型和它们之间的关系。
  • 业务决策的实现:实现业务决策逻辑,如验证规则、计算和数据转换等。
  • 保持独立性Domain层的设计应确保它不依赖于特定的UI实现或数据存储策略,使得业务逻辑可以在不同的应用场景下重用。

举例

假设有一个在线商店应用,其Domain层可能包含如下定义:

  • 实体Product(产品)、Customer(客户)、Order(订单)。
  • 用例PlaceOrder(下订单)、CancelOrder(取消订单)、UpdateCustomerProfile(更新客户资料)。
  • 值对象Address(地址)、Money(货币)。
  • 仓库接口OrderRepository(提供对订单数据的访问)、CustomerRepository(提供对客户数据的访问)。

在这个例子中,Domain层定义了在线商店运作的基本规则和逻辑,如何处理订单、如何管理客户信息,以及如何执行业务决策,所有这些都与特定的UI实现或数据持久化机制无关。

总结,Domain层的设计是为了确保应用程序的核心业务逻辑得到清晰定义、良好封装,从而易于维护和测试,同时提高了代码的可重用性。

解释 Domain 层定义了基本规则和逻辑,与特定的 UI 实现或数据持久化机制无关

Domain层在软件架构中的作用是封装和实现应用程序的核心业务逻辑,独立于用户界面(UI)和数据持久化(数据库等)的具体实现。这种设计方法使得业务逻辑更加模块化、清晰,并且易于测试和维护。在在线商店的例子中,Domain层定义的业务规则和逻辑涉及以下几个关键方面:

如何处理订单

Domain层中,处理订单的逻辑包括订单的创建、验证、支付、取消等步骤。这些操作反映了订单生命周期中的各个阶段,每个阶段都基于业务规则来执行。例如,订单支付前需要验证订单详情和客户支付信息的有效性;订单一旦支付成功,就不能随意取消,除非满足特定的退款条件。

如何管理客户信息

客户信息管理包括客户注册、信息更新、历史订单查询等功能。Domain层通过定义客户实体(Customer)和相关的业务操作(如UpdateCustomerProfile)来实现这一点。这些操作确保了客户信息的一致性和安全性,例如,客户密码需要加密存储,客户信息更新需要验证客户的身份。

如何执行业务决策

业务决策涉及到基于特定条件执行的逻辑选择,比如根据库存量决定是否接受订单、根据客户购买历史提供个性化推荐等。这些决策通常通过Domain层中的业务规则来实现,业务规则可能包括各种验证逻辑、计算公式和决策树。

与UI和数据持久化机制的独立性

Domain层的设计目的是保持对UI层和数据持久化层的独立性。这意味着,不管UI如何变化(无论是移动应用、Web页面还是桌面应用),Domain层的业务逻辑不需要做出改变。同样,数据存储的细节(是使用SQL数据库、NoSQL数据库还是云服务)对Domain层也是透明的。这一层只通过抽象的接口(如Repository接口)与数据交互,具体的实现细节由Data层负责。

通过将业务逻辑集中在Domain层,应用程序的核心功能和规则得到了清晰的定义和封装,这不仅有助于减少不同层之间的耦合,还使得业务逻辑更容易理解、测试和维护。此外,这种架构设计还增加了应用程序的灵活性,使得未来对应用程序的扩展和修改变得更加容易。

抽出 domain 层的意义是什么

抽出Domain层在软件架构中的意义主要体现在以下几个方面:

1. 业务逻辑集中化

将业务逻辑集中在Domain层,有助于明确应用程序的核心功能和规则,使得这些逻辑独立于其他层(如表示层和数据持久化层)的实现。这种集中化有助于保持业务规则的一致性,确保业务操作遵循同一套逻辑,无论是通过何种途径执行。

2. 提高可维护性

通过将业务逻辑与数据访问代码(Data层)和用户界面代码(Presentation层)分离,开发人员可以更容易地理解、修改和扩展业务规则。当业务需求发生变化时,通常只需要修改Domain层的代码,而无需触及到其他层的实现,这大大降低了维护成本和复杂性。

3. 增强可测试性

独立的Domain层使得业务逻辑的单元测试变得更加直接和简单。因为业务逻辑与外部的界面显示和数据存取逻辑解耦,测试可以集中在业务规则的正确性上,而不必担心UI或数据库的具体实现。这有助于提高测试覆盖率和发现潜在的业务逻辑错误。

4. 促进技术栈灵活性

由于Domain层独立于具体的技术实现,因此可以在不影响业务逻辑的前提下更换表示层框架或数据持久化技术。例如,可以从一个数据库系统迁移到另一个,或者在不同的前端技术(如Web、移动应用)之间共享相同的业务逻辑。

5. 便于多团队协作

在大型项目中,不同的团队可以负责不同的应用层,例如一个团队专注于Domain层的业务逻辑开发,而另一个团队专注于用户界面的实现。这种分工有助于提高开发效率,因为团队可以并行工作,而且减少了彼此工作的直接依赖。

6. 支持多种应用场景

Domain层的业务逻辑可以被不同的应用场景重用,比如同一个业务逻辑既能服务于Web应用,也能服务于移动应用或桌面应用。这种重用性可以显著降低开发和维护新平台应用的成本和时间。

总之,抽出Domain层是实现软件架构清晰分层、提高代码质量和维护性的重要策略,它有助于构建更加健壯、灵活和可持续发展的软件系统。

这样做为什么可以只需要修改Domain层的代码,而无需触及到其他层的实现

当业务需求发生变化时,通常只需要修改Domain层的代码而无需触及到其他层的实现,这个原理主要是基于软件设计中的分层架构和关注点分离的原则。让我们来详细解释一下为什么这样做是可行的:

分层架构

在一个典型的分层架构中,软件被划分为几个层次,每个层次负责处理不同的任务。这些层次通常包括:

  • 表示层(Presentation Layer):负责用户界面和用户交云的处理。
  • 领域层(Domain Layer):包含业务逻辑和业务规则。
  • 数据访问层(Data Access Layer):负责数据持久化和数据检索。

关注点分离

分层架构实现了关注点分离(Separation of Concerns, SoC)的原则,即不同的层次关注不同的方面。通过这种方式,每一层都可以独立于其他层变化,只要它们之间的接口保持不变。

对于业务逻辑的变化

  • 业务逻辑变化通常发生在Domain层:因为Domain层封装了应用程序的核心业务规则和逻辑。当业务需求变化时,这些变化几乎总是直接影响到业务规则和逻辑,而这些都是Domain层的职责范畴。
  • 表示层和数据访问层的独立性:由于表示层主要负责界面显示和用户交互,数据访问层主要负责数据的持久化和检索,这两层的工作与业务逻辑的具体内容相对独立。它们通过定义良好的接口与Domain层交互,只要这些接口不变,业务逻辑的变更就不会直接影响到它们。
  • 接口作为契约Domain层通过接口与其他层进行交互。当业务需求变化导致Domain层的逻辑变化时,只要接口保持不变,表示层和数据访问层就无需变化。即使接口需要变化,现代编程语言和框架提供的抽象和封装机制也能最小化这种变化的影响。

实例

假设一个电子商务应用,其中有一个计算订单总价的业务逻辑。如果业务规定更改了订单的折扣计算方式,这个变化直接关系到订单总价的计算规则,这是Domain层的一部分。只需修改Domain层中计算总价的代码即可。只要Domain层对外提供的计算总价的接口不变,表示层(可能显示总价的界面)和数据访问层(可能存储订单详情的数据库操作)就不需要任何改动。

结论

因此,将业务逻辑封装在Domain层,确保了应用程序在面对业务需求变化时,可以局部化修改影响,减少了需要修改的代码量,从而降低了维护成本和复杂性。这种设计使得软件系统更加灵活和可维护,更容易适应和响应业务变化。

给出代码例子

让我们通过一个简化的代码示例来说明当业务需求发生变化时,通常只需要修改Domain层的代码而无需触及到其他层的实现。考虑一个电子商务应用,其中包含一个功能:计算订单总价。我们将展示Domain层、Data层和Presentation层的简化代码,并展示当业务规则(例如,订单折扣规则)变化时,如何只修改Domain层代码来应对这一变化。

初始实现

Domain层

1
2
3
4
5
6
7
8
9
10
11
12
13
// Domain层 - 定义业务逻辑接口
interface OrderService {
fun calculateTotalPrice(items: List<OrderItem>): Double
}

// Domain层 - 实现
class OrderServiceImpl : OrderService {
override fun calculateTotalPrice(items: List<OrderItem>): Double {
return items.sumOf { it.price * it.quantity }
}
}

data class OrderItem(val price: Double, val quantity: Int)

Data层

假设数据层处理订单数据的存储和检索,但与计算总价无关,因此在此示例中略过。

Presentation层

1
2
3
4
5
6
7
// Presentation层 - 使用Domain层的服务展示总价
class OrderViewModel(private val orderService: OrderService) {
fun displayTotalPrice(items: List<OrderItem>) {
val totalPrice = orderService.calculateTotalPrice(items)
println("Total Price: $totalPrice")
}
}

业务需求变化

假设业务需求变化,现在需要对订单总价应用10%的折扣。

修改Domain层

我们只需要在Domain层中修改OrderServiceImpl的实现来反映这一变化:

1
2
3
4
5
6
7
8
// Domain层 - 修改后的实现
class OrderServiceImpl : OrderService {
override fun calculateTotalPrice(items: List<OrderItem>): Double {
val originalTotal = items.sumOf { it.price * it.quantity }
val discount = originalTotal * 0.1 // 应用10%的折扣
return originalTotal - discount
}
}

在这个修改中,我们没有触及Presentation层或Data层的代码。Presentation层仍然以相同的方式调用Domain层的calculateTotalPrice方法,而Data层(假设它负责数据的持久化)也不需要变化,因为它与订单价格的计算逻辑无关。

结论

通过这个简化的例子,我们可以看到,当业务规则变化(如订单折扣规则)时,只需修改Domain层中的业务逻辑实现。Presentation层和Data层不需要修改,因为它们与Domain层通过抽象的接口进行交互,这个接口(在本例中是OrderService.calculateTotalPrice)保持不变。这种设计使得应用程序能够灵活应对业务需求的变化,同时最小化了修改的影响范围,提高了代码的可维护性。

Android 音视频屏幕旋转方案

正文

在Android平台上,实现音视频播放过程中的屏幕旋转涉及到以下几个方面:界面布局的调整、Activity的生命周期管理、保存和恢复播放状态、以及处理屏幕旋转时可能引发的音视频播放问题。以下是一个基本的音视频屏幕旋转方案的步骤:

布局调整

res文件夹中创建不同方向的布局文件,例如layout/activity_main.xmllayout-land/activity_main.xml分别表示竖屏和横屏时的布局。在这些布局文件中,你可以调整控件的摆放位置以适应不同方向的屏幕。

Activity的生命周期管理

当屏幕发生旋转时,Activity会经历重新创建的过程。确保你的Activity正确处理生命周期事件,特别是onCreateonSaveInstanceStateonRestoreInstanceState等方法。

保存和恢复播放状态

onSaveInstanceState方法中保存当前的播放状态,例如播放位置、播放状态(播放、暂停等),以及其他相关信息。然后在onCreateonRestoreInstanceState方法中恢复这些状态。

处理屏幕旋转可能引发的问题

屏幕旋转可能会导致播放器重新创建或重置,从而影响播放状态。你需要在Activity重新创建时恢复播放状态,并确保播放器不会中断或重置播放进度。

使用ViewModel来保存播放状态(可选)

使用ViewModel可以更方便地在屏幕旋转时保持数据。ViewModel会在Activity重新创建时保持其状态,因此你可以将播放状态等数据存储在ViewModel中,以确保不会丢失。

锁定屏幕方向(可选)

如果你希望在播放音视频时固定屏幕方向,可以在AndroidManifest.xml中的相应Activity中使用android:screenOrientation属性来指定所需的屏幕方向(如portraitlandscape)。

总结

总之,处理音视频播放过程中的屏幕旋转需要综合考虑界面布局、生命周期管理、数据保存和恢复等方面的问题。合理使用布局文件、Activity生命周期方法、ViewModel等工具,可以帮助你实现稳定且用户友好的音视频播放体验。

Android 音视频屏幕旋转方案

Q:我应该去哪里获取不同版本的存储策略

A:要获取不同版本的 Android 存储策略(Storage Policies),您可以访问以下资源,这些资源提供了详细的版本特定存储策略和变更的信息:

官方 Android 文档

Android 官方文档是了解不同版本存储策略最可靠的来源。以下是一些重要的链接:

  1. **Android Developers Storage Documentation**:
    这是 Android 官方的存储文档,涵盖了各种存储选项和最佳实践。

  2. **Android Platform Releases**:
    这个页面提供了每个 Android 版本的详细变更,包括存储策略的变更。

  3. **Scoped Storage**:
    针对 Android 10(API Level 29)及更高版本的分区存储(Scoped Storage),是存储策略的重要变更点。

官方博客和公告

Android 官方博客和公告也是了解存储策略变更的好资源:

  1. **Android Developers Blog**:
    在这个博客上,Google 发布了关于新功能和存储策略变更的详细信息和指导。

GitHub 和社区资源

开源社区和开发者论坛也提供了大量关于存储策略的信息:

  1. **Google Samples on GitHub**:
    Google 提供的示例项目,可以查看不同版本的存储策略实现。

  2. **Stack Overflow**:
    搜索关于 Android 存储策略的问题和答案,可以获取社区的解决方案和建议。

具体版本的存储策略变更

要了解具体版本的存储策略变更,可以参考以下步骤:

  1. 访问 Android 开发者文档中的版本变更日志
    每个 Android 版本的变更日志会详细列出该版本的新特性和变更,包括存储策略。

  2. 查找特定版本的存储策略文档
    在 Android Developers 网站上,您可以找到特定版本的 API 参考和变更日志。

直接搜索具体问题

如果您有特定的存储策略问题,可以直接在 Google 搜索:

  • 例如:Android 10 Scoped Storage
  • 例如:Android 11 Storage Policy Changes

通过这些资源,您应该能够找到详细的 Android 存储策略信息,并理解每个版本的特定变更。

Android 6 以下的文件存储和读取策略

在Android 6.0(API Level 23)及以下版本中,文件存储和读取的策略主要涉及以下几个方面:

1. 存储位置

Android系统提供了两种主要的存储位置:内部存储和外部存储。

内部存储

  • 内部存储是应用的私有存储区域,只有该应用可以访问。
  • 存储在内部存储中的文件默认在应用卸载时会被删除。
  • 使用Context对象的方法可以在内部存储中读写文件,例如:
    • openFileOutput(String name, int mode):写文件
    • openFileInput(String name):读文件

外部存储

  • 外部存储可以是设备的外部存储卡或者内置存储分区。
  • 分为公共外部存储和应用私有外部存储。
  • 公共外部存储可以被所有应用访问,例如图片、音频文件等。
  • 应用私有外部存储只能被创建该文件的应用访问,在应用卸载时会被删除。
  • 使用Environment类和Context对象的方法可以在外部存储中读写文件,例如:
    • getExternalFilesDir(String type):获取应用私有外部存储目录
    • Environment.getExternalStoragePublicDirectory(String type):获取公共外部存储目录

2. 权限管理

在Android 6.0及以下版本中,应用需要在AndroidManifest.xml中声明对外部存储的读写权限:

  • 读权限:<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  • 写权限:<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

这些权限在应用安装时由用户批准。

3. 文件操作代码示例

内部存储

写文件:

1
2
3
4
5
String filename = "myfile.txt";
String fileContents = "Hello, world!";
FileOutputStream fos = openFileOutput(filename, Context.MODE_PRIVATE);
fos.write(fileContents.getBytes());
fos.close();

读文件:

1
2
3
4
5
6
7
8
9
10
FileInputStream fis = openFileInput(filename);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader bufferedReader = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
String fileContents = sb.toString();
fis.close();

外部存储

检查存储状态:

1
2
3
4
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
// 可以读写外部存储
}

写文件:

1
2
3
4
File file = new File(getExternalFilesDir(null), "myfile.txt");
FileOutputStream fos = new FileOutputStream(file);
fos.write(fileContents.getBytes());
fos.close();

读文件:

1
2
3
4
5
6
7
8
9
10
11
File file = new File(getExternalFilesDir(null), "myfile.txt");
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader bufferedReader = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
String fileContents = sb.toString();
fis.close();

这些代码示例展示了在Android 6.0及以下版本中如何进行基本的文件读写操作。随着Android版本的升级,文件存储和权限管理策略也发生了显著变化,因此在开发过程中需要根据目标API版本进行适配。

Android 6 以上文件存储和读取策略

在Android 6.0(API Level 23)及以上版本中,文件存储和读取策略发生了一些变化,特别是在权限管理和存储位置的使用方面。以下是各版本的策略变化和相应的文件存储读写方法。

1. Android 6.0 (API Level 23)

动态权限

  • 从Android 6.0开始,权限管理采用动态权限模式。用户可以在运行时授予或拒绝权限。
  • 应用在使用外部存储时,需要在AndroidManifest.xml中声明权限,并在运行时请求权限。

权限请求代码示例:

1
2
3
4
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
}

2. Android 7.0 (API Level 24)

文件URI策略

  • 从Android 7.0开始,File URI不再被允许在应用之间共享。必须使用FileProvider来共享文件URI。

FileProvider配置示例:
AndroidManifest.xml中:

1
2
3
4
5
6
7
8
9
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

res/xml/file_paths.xml中:

1
2
3
<paths>
<external-path name="external_files" path="." />
</paths>

3. Android 8.0 (API Level 26)

安全性增强

  • 在后台执行文件操作受到限制,必须在前台任务或通过服务进行文件操作。

4. Android 9.0 (API Level 28)

共享存储访问

  • 在Android 9.0中引入了新的API,用于访问共享存储,推荐使用ACTION_OPEN_DOCUMENTACTION_CREATE_DOCUMENT来访问和创建文件。

使用示例:

1
2
3
4
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT);

5. Android 10 (API Level 29)

分区存储 (Scoped Storage)

  • 引入了分区存储,应用只能访问其私有的外部存储空间,除非使用特定的API访问公共存储。
  • 可以通过请求Legacy存储模式来继续使用传统的存储访问方式(直到Android 11)。

分区存储代码示例:

1
2
3
4
File file = new File(getExternalFilesDir(null), "myfile.txt");
FileOutputStream fos = new FileOutputStream(file);
fos.write(fileContents.getBytes());
fos.close();

6. Android 11 (API Level 30)

强制分区存储

  • 强制启用分区存储,应用无法再使用requestLegacyExternalStorage标志。
  • 提供了新的访问存储的API,如MediaStore

MediaStore使用示例:

1
2
3
4
5
6
7
8
9
10
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, "myfile.txt");
values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS);
Uri uri = getContentResolver().insert(MediaStore.Files.getContentUri("external"), values);
if (uri != null) {
OutputStream outputStream = getContentResolver().openOutputStream(uri);
outputStream.write(fileContents.getBytes());
outputStream.close();
}

7. Android 12 (API Level 31) 及更高版本

进一步限制外部存储访问

  • 在Android 12中,对特定类型的文件访问进行了更多的限制,需要使用特定的API来访问不同类型的文件,如音频、视频和图片等。

总结:

  • Android 6.0及以上版本在权限管理上采用动态权限,需要在运行时请求权限。
  • Android 7.0开始引入FileProvider来共享文件URI。
  • Android 10引入分区存储,Android 11强制启用分区存储,进一步限制了应用对外部存储的访问。
  • 使用MediaStore等新的API来进行文件的读写操作,以适应最新的存储策略。

了解并适应这些变化,可以确保应用在不同Android版本上的文件存储和读取操作安全且有效。

Android aar 文件到底是什么

作者: 朕小猫与GPT4

问题起源:怎么打包发布一个含有 C++ 文件的 Library

起因是 KOOM 2.2.1 Release 存在后台分析进程在部分机型失效,想自己发布一个适合自己项目的版本。

.so文件可以被打包到最终的AAR文件中吗

是的,.so文件可以并且通常被打包到最终的AAR(Android Archive)文件中。AAR文件是Android开发中用于分发Android库的标准格式,它包含了库所需的所有资源、编译后的代码以及其他文件,比如.so文件(本地库文件)。

打包.so文件到AAR中的步骤

  1. 放置.so文件: 将你的.so文件放置在项目的src/main/jniLibs/目录下。按照不同的CPU架构,你可能需要创建相应的子目录,例如armeabi-v7aarm64-v8ax86等,然后将相应架构的.so文件放置在这些目录下。

  2. 配置Gradle构建脚本: 确保你的build.gradle文件正确配置,这样Gradle在构建过程中就会自动将jniLibs目录下的.so文件包括到AAR包中。大多数情况下,这个过程是自动完成的,无需额外配置。

示例目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
YourLibraryProject/
├── src/
│ └── main/
│ ├── java/
│ │ └── ... (your Java or Kotlin source files)
│ ├── res/
│ │ └── ... (your resources)
│ └── jniLibs/
│ ├── armeabi-v7a/
│ │ └── libyourlibrary.so
│ ├── arm64-v8a/
│ │ └── libyourlibrary.so
│ ├── x86/
│ │ └── libyourlibrary.so
│ └── x86_64/
│ └── libyourlibrary.so

构建AAR包

当你构建你的库时(例如,通过运行./gradlew assembleRelease),Gradle会自动将jniLibs目录下的所有.so文件包括到生成的AAR文件中。这样,当其他开发者将你的AAR文件作为依赖项添加到他们的项目中时,这些.so文件就会被自动解压到正确的位置,供他们的应用使用。

确保在发布你的库之前测试它,以确保.so文件被正确包含且可以按预期工作。这通常涉及到在包含不同CPU架构设备的模拟器或真实设备上运行和测试你的库。

.aar 里可以有哪些文件

一个AAR(Android Archive)文件是Android库项目的发布包格式,用于在开发者之间共享Android库。AAR文件包含了库所需要的所有内容,使其可以被其他的Android应用项目引用。以下是AAR文件通常包含的文件和目录:

必需的内容

  • classes.jar:包含库的所有编译后的Java类(.class文件)。
  • res/:包含库所使用的所有资源文件,如布局(layouts)、字符串(strings)、图片(drawables)等。
  • AndroidManifest.xml:库的Android清单文件,描述了库的包名、所需权限、活动(activities)、服务(services)等。

可选的内容

  • R.txt:包含库中所有资源的ID名称和类型的列表,这是为了在库被引用时帮助生成最终应用的R类。
  • aidl/:包含库所使用的所有AIDL(Android Interface Definition Language)文件,用于定义跨进程通信接口。
  • assets/:包含可以被库访问的原始文件,如文本、音频或视频文件。
  • libs/*.jar:包含库依赖的任何Java库的JAR文件。
  • jni/jniLibs/:包含库依赖的本地库(如.so文件),这些文件根据不同的CPU架构可能存放在不同的子目录下,如armeabi-v7a/arm64-v8a/等。
  • proguard.txt:包含用于代码混淆的ProGuard规则。
  • lint.jar:包含自定义lint检查规则的JAR,这些规则可以在使用库的项目中被lint程序使用。
  • public.txt:列出了库公开的资源ID,这些资源ID在库的使用者中是可见的。
  • annotations.zip:包含用于工具支持的注解,比如nullability注解等。

如何查看AAR文件内容

你可以通过解压AAR文件(它本质上是一个ZIP文件)来查看里面的内容。使用任何标准的压缩文件管理器,如WinRAR、7-Zip等,都可以打开AAR文件并浏览其中的内容。

使用AAR文件

当你将AAR文件作为依赖项添加到Android项目中时,项目构建系统(如Gradle)会自动处理并引入这些内容,包括解析资源、合并清单文件等,从而使库中的功能可用于应用程序。
aar文件分析.png

Android aar 文件到底是什么

作者: 朕小猫与GPT4

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
task downloadApk {
doLast {
def remoteApkPath = "/home/jason/AndroidStudioProjects/ncmusic/app/build/outputs/apk/debug/app-debug.apk"
def localApkDir = System.getProperty("user.home") + "/Downloads/"
def localApkPath = localApkDir + "app-debug.apk"

// 创建本地目录
new File(localApkDir).mkdirs()

// 使用 scp 下载远程 APK 文件到本地
def command = "sshpass -p 'password' scp jason@192.168.1.100:${remoteApkPath} ${localApkPath}"
println "Executing command: ${command}"

exec {
commandLine 'bash', '-c', command
}
}
}

task installAndStart {
dependsOn downloadApk
doLast {
def localApkPath = System.getProperty("user.home") + "/Downloads/app-debug.apk"

// 确保 adb 可用
def adbPath = '/Users/jason/Library/Android/sdk/platform-tools/adb'
if (!new File(adbPath).exists()) {
throw new GradleException("adb command not found at ${adbPath}! Please ensure Android SDK is properly installed.")
}

// 安装 APK
def installCommand = "${adbPath} install -r ${localApkPath}"

try {
exec {
commandLine 'bash', '-c', installCommand
}
} catch (Exception e) {
throw new GradleException("Failed to install APK: ${e.message}")
}

// 提取包名
def manifestFile = file("${rootProject.projectDir}/app/src/main/AndroidManifest.xml")
def packageName = ""
if (manifestFile.exists()) {
manifestFile.eachLine { line ->
if (line.contains("package=")) {
packageName = line.split('package=')[1].split('"')[1]
}
}
if (packageName == "") {
throw new GradleException("Failed to extract package name from AndroidManifest.xml")
}
} else {
throw new GradleException("AndroidManifest.xml file not found!")
}

// 启动应用
def launchCommand = "${adbPath} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1"
println "Executing command: ${launchCommand}"

try {
exec {
commandLine 'bash', '-c', launchCommand
}
} catch (Exception e) {
throw new GradleException("Failed to launch application: ${e.message}")
}
}
}

task downloadBuild {
doLast {
subprojects.each { subproject ->
def remoteRootPath = "/home/jason/AndroidStudioProjects/"
def remoteDirPath = remoteRootPath + "${rootProject.name}/${subproject.name}/build"
def localDirPath = "${subproject.projectDir}"

// 创建本地目录
new File(localDirPath).mkdirs()

// 使用 rsync 同步远程文件到本地
def command = "sshpass -p 'password' rsync -avz --delete jason@192.168.1.100:${remoteDirPath} ${localDirPath}"
println "Executing command: ${command}"

exec {
commandLine 'bash', '-c', command
}
}
}
}

task remoteBuild {
doLast {
// 远程构建命令
def command = "sshpass -p 'password' ssh jason@192.168.1.100 'cd /home/jason/AndroidStudioProjects/ncmusic/ && ./gradlew assembleDebug'"
exec {
commandLine 'bash', '-c', command
}
}
}

// 定义一个任务来执行远程构建、下载 APK、安装并启动应用,然后同步构建产物
task buildDownloadInstallAndStart {
dependsOn remoteBuild
finalizedBy installAndStart, downloadBuild
}