作者: 朕小猫与GPT4
1 | task downloadApk { |
作者: 朕小猫与GPT4
1 | task downloadApk { |
作者: 朕小猫与GPT4
1 | cmake_minimum_required(VERSION 3.4.1) |
Q: 这样写完之后,apk 安装包中有两个so文件 libDualVectorFoilLib 和 libassimp,都占用 14m,但其实,libDualVectorFoilLib 中包含的就是libassimp,不应该再占用这么大体积
A: 为了减少 APK 中的体积,你可以将 libassimp
静态链接到 DualVectorFoilLib
中,这样只会生成一个包含 assimp
的 libDualVectorFoilLib.so
文件,而不会包含独立的 libassimp.so
文件。这种方式可以避免重复打包和减小 APK 的体积。具体修改如下:
assimp
静态链接1 | cmake_minimum_required(VERSION 3.4.1) |
**禁用 BUILD_SHARED_LIBS
**:将 BUILD_SHARED_LIBS
设置为 OFF
,使 Assimp 以静态库(.a
文件)形式生成,这样 Assimp 将不会生成独立的 .so
文件,而是将代码直接打包到 libDualVectorFoilLib.so
中。
**静态链接 assimp
**:在 target_link_libraries
中链接 assimp
,这会将 Assimp 的代码直接合并进 libDualVectorFoilLib.so
,避免生成和打包独立的 libassimp.so
。
结果:这样可以减小 APK 体积,因为不再包含重复的库文件。
之后重新构建项目,确保 APK 中只包含 libDualVectorFoilLib.so
文件,不再包含 libassimp.so
文件。
Q: 为什么改成静态连接之后小了好多
A: 改成静态链接之后 APK 大小减少,主要原因在于静态链接和动态链接的工作原理不同,以及 Android 应用打包时如何处理库文件:
避免重复加载的冗余文件:动态链接时,libassimp.so
作为独立的动态库被单独打包,并在运行时加载。而静态链接则将 libassimp
的代码直接整合到 libDualVectorFoilLib.so
中,避免了多余的 .so
文件。因此,只生成一个完整的 .so
文件,而不是两个重复的文件,节省了空间。
优化与裁剪:静态链接时,链接器会自动去除 Assimp 库中未使用的代码,只保留 DualVectorFoilLib
需要的部分。这意味着生成的 libDualVectorFoilLib.so
体积更小,因为一些多余的功能不会被包含进去。而动态链接时,通常整个库都被打包,即使项目不使用 Assimp 的所有功能,APK 也会包含整个库的内容。
减少符号表与动态加载信息:动态库需要额外的符号表和动态加载信息,以便在运行时进行地址解析和符号加载。静态链接的 .so
文件则不需要这些额外的信息,因此最终生成的文件会更小。
加载性能提升:除了体积减小,静态链接也会带来性能上的提升,因为它省去了动态加载库时的解析步骤,在某些场景下可以略微缩短应用启动时间。
通过静态链接,代码体积优化和去重工作会更加有效,因此可以显著减小 APK 大小。
Q: 如果我只想引入 assimp 不想引入 libDualVectorFoilLib.so 怎么处理
A: 如果只想引入 assimp
库,不需要 libDualVectorFoilLib.so
,可以直接在 CMake 中只构建 assimp
,而不创建其他自定义的 .so
库文件。这样可以实现只生成 libassimp.so
,并确保它在你的 APK 中被正确引用。以下是可以实现的步骤:
直接构建 Assimp 并禁用其他库的生成:
只需要 assimp
,无需自定义的库文件,可以移除自定义库的定义,只保留对 assimp
的配置与编译。
修改 CMakeLists.txt:
1 | cmake_minimum_required(VERSION 3.4.1) |
**在 build.gradle
中指定加载 libassimp.so
**:
在你的 Android 项目中确保在 Java 或 Kotlin 代码中正确加载 libassimp.so
,例如:
1 | System.loadLibrary("assimp") |
**检查并确保只有 libassimp.so
**:
构建 APK 后,检查生成的 APK 文件,确认只有 libassimp.so
被打包,而没有 libDualVectorFoilLib.so
。
这样配置后,APK 中只会包含 libassimp.so
,并且该库可以直接使用,不再生成不必要的库文件。
Q: So 文件的 Raw File Size 和 Download Size 分别是什么?
A: 在 APK 文件的打包和发布过程中,文件会经历压缩处理,这使得 APK 下载包的体积通常小于安装后在设备上实际占用的空间。在 Android Studio 中,APK Analyzer 工具展示了两个关键指标来帮助我们理解这些差异:
Raw File Size(原始文件大小):这是文件在 APK 解压后,在设备上占用的实际存储空间大小。在您提供的截图中,libassimpd.so
的原始文件大小为 14.4 MB,表示安装到设备后它会占用 14.4 MB 的空间。
Download Size(下载大小):这是 APK 包含的文件在压缩后的大小。由于 APK 打包时会对文件进行压缩,libassimpd.so
的压缩后下载大小仅为 3.4 MB。这个值表示用户在下载 APK 包时实际需要传输的数据量。
总结来说:
因此,虽然下载 APK 时 libassimpd.so
只占 3.4 MB,但安装后在设备上会占用 14.4 MB 的存储空间。
Q: So 文件是怎么实现从14M压缩到3.4M的
A: so
文件(共享库文件)的大小可以大幅度缩小,主要是因为 Android APK 打包过程中应用了压缩算法。以下是一些关键原因和过程:
APK 文件本质上是一个 ZIP 文件,打包时会对资源文件和库文件应用 ZIP 压缩。共享库文件(so
文件)通常包含大量重复数据和空白区域,这些内容在 ZIP 压缩时能够被有效地缩减。因此,像 libassimpd.so
这种大型的二进制文件,通过 ZIP 压缩可以大幅度减小体积。
so
文件是二进制格式,包含大量的结构化数据和代码段。这些二进制数据在结构上非常紧密,包含许多重复的模式和数据块,非常适合被压缩算法处理。常用的压缩算法(例如 DEFLATE)能够识别这些模式,减少冗余,从而将文件压缩到较小的尺寸。
一些调试信息和符号信息在生成 so
文件时可能会被包含在内,但是在发布 APK 时通常会将这些调试符号信息剥离。这会进一步减少 so
文件的体积,尤其在压缩之后,对这些无用数据的移除效果显著。
so
文件可能包含一些预留的数据段或对齐填充,这些部分在压缩后也会被进一步缩小。比如对齐填充区域在压缩时几乎不会占用空间,从而进一步减小了整体体积。
在您的示例中,libassimpd.so
原始文件大小为 14.4 MB,而在 APK 中压缩后的下载大小是 3.4 MB。这种大幅缩小主要得益于 ZIP 压缩的高效性,以及 so
文件的二进制结构带来的高压缩比。
APK 中的 so
文件能够实现从 14 MB 压缩到 3.4 MB,主要是由于以下原因:
这些因素共同作用,使得 so
文件在安装包中的大小远小于其解压后的原始大小。
在说解决方案之前,我们需要先明确两个问题
每当我向 Android 同学提出第一问时,通常他们会回答我:阴影是个10-25%透明度的线性减淡的黑色图层
但是当你尝试在移动端去绘制这样一个黑色图层去代表阴影,其视觉效果肉眼可见的差强人意
于是绝大多数 Android 同学会提议你去找设计出个带阴影的图,或者使用诸如 ShadowLayout 这样复杂的控件
https://github.com/lihangleo2/ShadowLayoutShadowLayout 使用了 1600 行代码只为实现边框阴影
再看看 github 上其他绘制阴影的方案,不是采用 RenderScript 这种即将被谷歌废弃的图像处理库,就是使用 Android 12 以上才有的还要依赖硬件加速的 RenderEffect,更有甚者者干脆用 Canvas 硬画,不仅效果不好,性能更是很差,稍有复杂的情况,甚至可能导致卡顿发热
我们不经要发问:Android 10多年了,难道就没有一个不需要和设计掰扯,能兼容所有机型,对开发简单友好的阴影解决方案吗?
先来看下阴影的组成部分
我们可以将阴影大致分成两个部分,全影/umbra 和 半影/penumbra 。
半影区域就是阴影的过渡区, 也就是软阴影,有半影的阴影过渡时,视觉效果会好很多。
通俗点说:全影就是大家常说的 10 - 25% 透明度的黑色图层,通常我们需要绘制的边框阴影是半影。
半影的绘制实际上是一个多次采样模糊的过程
1 | float PCF(float2 uv, float currentDepth) { |
在上述代码中,对 3* 3 的区域进行了模糊采样,也就是说每个像素点的颜色依赖其周围相邻的一圈像素点的颜色的计算结果,实现逐步衰减
首先明确一点:切图和代码实现的本质区别是什么?
是预渲染和实时渲染的区别
如果我们当前卡片所在的背景是可枚举且数量较少的,我们应该倾向使用预渲染
举个例子:
切图虽然方便,却有以下几种弊端
解决方案
当我们想要绘制一个带阴影的圆角矩形的时候,我们会发现除了矩形的四个角外,整个矩形绝大部份都是重复可拉伸的
如果我切出一个这样的圆角矩形最小图,并设置其非圆角的区域为拉伸区域
那我们就可以用一个只有几 kb 大小的 .9 图实现固定圆角的带边框阴影的矩形
考虑到实际情况,我们可以切出 0 - 12度的圆角图共13张图,圆角越小的图片越小,易得总共大约 (1kb + 5kb ) / 2 * 13 = 39kb
再算上深色模式,未打包压缩前总共只需要 80kb
而 1600 行的 ShadowLayout.java 未压缩前有 55kb,引入 Androidx 的 CardView 就更大了,还需要考虑机型的兼容性和硬件加速开关
而使用 .9 图,开发同学只需要选择对应圆角的 .9 图设置为背景即可
轻松使用
1 | <View |
我们可以进一步,将圆角和矩形分离,使用原子化的图片素材 + 微量代码,组合各种圆角与阴影效果
以下是 预渲染(.9图) 和 代码实现(如ShadowLayout / CardView) 的优劣势对比表格,结合性能、开发效率、灵活性等维度分析:
对比维度 | 预渲染(.9图) | 代码实现(动态绘制) |
---|---|---|
性能 | ✅ 极低开销(仅纹理采样) | ⚠️ 较高开销(实时计算阴影、图层叠加) |
内存占用 | ✅ 小 | ⚠️ 动态(取决于具体代码) |
开发效率 | ✅ 直接设置背景,无需编码 | ⚠️ 需编写/维护复杂逻辑(如阴影算法) |
灵活性 | ❌ 仅支持预定义的圆角/阴影样式 | ✅ 动态调整参数(颜色、圆角、模糊度等) |
适配性 | ✅ 无兼容性问题(Android/iOS通用) | ⚠️ 需处理机型兼容(如硬件加速、API差异) |
扩展性 | ❌ 修改样式需重新切图 | ✅ 可通过代码快速迭代新效果 |
文件体积 | ✅ 极小(如80KB未压缩) | ⚠️ 较大(代码+依赖库,如CardView) |
动态效果支持 | ❌ 无法实现动画或实时变化 | ✅ 支持动态阴影(如跟随光源移动) |
多分辨率适配 | ✅ 自动拉伸(.9图特性) | ⚠️ 需额外处理多分辨率下的绘制精度 |
维护成本 | ✅ 低(替换图片即可) | ⚠️ 高(需测试不同机型/系统版本) |
优先使用.9图:
• 样式固定(如标准化设计语言中的卡片阴影)。
• 性能敏感场景(如列表项、高频刷新界面)。
• 需要快速开发且无动态效果需求。
优先代码实现:
• 需要动态效果(如阴影动画、交互反馈)。
• 样式高度自定义(如设计师要求的特殊渐变阴影)。
• 混合方案:对静态部分使用.9图,动态部分叠加代码绘制(如悬浮按钮的阴影)。
• 工具化:将.9图生成和代码阴影封装成工具,供团队按需选择
通过明确场景需求,可以高效选择最优方案。
layout_constrainedWidth
是ConstraintLayout
中的一个属性,用于在处理宽度约束时提供更多的灵活性和控制。当你使用ConstraintLayout
构建复杂的布局时,可能会遇到某些视图在其内容过长时需要被约束宽度的情况,以避免它们超出屏幕边界或影响其他视图的布局。layout_constrainedWidth
属性就是为了解决这类问题而设计的。
1 | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
1 | anyView.setOnTouchListener(object : View.OnTouchListener { |
MAX_CONCURRENT_FRAMES
参数是Vulkan多帧并行处理的核心设计,理解它需要从硬件工作原理和性能调优两个维度来分析:
GPU流水线特性:现代GPU采用深度流水线设计,类似CPU的指令级并行。
当设置为2时:
1 | CPU帧1记录 → GPU执行帧1 → CPU帧2记录 → GPU执行帧2 |
内存带宽影响:每增加一帧需要额外存储:
理论上吞吐量提升遵循:
1 | 理论最大帧率 = 1 / max(CPU帧处理时间, GPU帧渲染时间) |
当MAX_CONCURRENT_FRAMES
=N时,可掩盖的延迟为:
1 | 可隐藏延迟 = (N-1) × min(CPU_time, GPU_time) |
1 | 理论最大帧率 = \frac{1}{\max(T_{CPU}, T_{GPU})} |
T_CPU
包含命令记录+资源更新,T_GPU
包含所有渲染阶段耗时假设某场景:
1 | 可隐藏延迟 = (N-1) \times \min(T_{CPU}, T_{GPU}) |
1 | 时间轴 CPU活动 GPU活动 |
此时可隐藏的延迟 = (3-1)*min(3ms,5ms) = 6ms
意味着系统能容忍6ms的突发延迟而不掉帧
瓶颈方 | 优化建议 | 数学原理 |
---|---|---|
CPU受限 | 增加MAX_CONCURRENT_FRAMES | 增大N使T_GPU×N > T_CPU |
GPU受限 | 降低画质/分辨率 | 减小分母max(T_CPU,T_GPU) |
均衡状态 | 保持N=2~3 | 达到1/max(T,T)的理论极限 |
1 | auto calc_optimal_N = [](float cpu_time, float gpu_time) { |
在RDNA3/Turing等架构中,由于:
实际观测到公式需要修正为:
1 | 有效帧率 = \frac{1}{\max(T_{CPU}, \alpha \cdot T_{GPU})} |
其中α是GPU并行化系数(通常0.7~0.9)
在VR系统中,通常需要:
1 | N = \left\lceil \frac{motion-to-photon}{\min(T_{CPU},T_{GPU})} \right\rceil + 1 |
例如Oculus Rift要求的20ms延迟:
这种场景下就需要更复杂的帧交错(Frame Interleaving)技术。
在代码中体现为三重资源组:
1 | // 每组资源包含: |
根据硬件特性调整:
硬件配置 | 推荐值 | 理论收益 |
---|---|---|
集成显卡(移动设备) | 2 | 避免内存带宽饱和 |
独立显卡(桌面级) | 3 | 利用高显存带宽 |
多GPU交火系统 | 4 | 掩盖跨GPU通信延迟 |
增加该值会引入:
1 | 总延迟 = 呈现延迟 + (N-1)×帧间隔 |
例如60FPS下:
可通过Vulkan调试扩展验证实际利用率:
1 | void checkPipelineState() { |
配合VK_KHR_timeline_semaphore
扩展可实现动态帧数:
1 | uint32_t dynamicFrameCount = calculate_optimal_frame_count(); |
最佳实践:从2开始逐步增加,使用
VK_LAYER_KHRONOS_performance
层监测GPU空闲率,当达到95%以上GPU利用率时停止增加。
作者:朕小猫与GPT4
在 C++ 程序中,几乎所有元素(包括变量、对象、函数等)都可以通过它们的内存地址来引用或操作。这一点归根结底是因为 C++ 是一种底层的编程语言,它允许直接访问内存和底层硬件资源。这种能力带来了极高的灵活性和控制能力,但同时也要求程序员对程序的内存使用有深入的理解。下面是如何通过内存地址来引用或操作元素的几个方面:
在 C++ 中,每个变量或对象都占据内存中的一个或多个连续位置。你可以使用 &
操作符获得一个变量的内存地址,这个地址可以赋给一个指针变量。通过指针,你可以读取或修改存储在该地址的数据。这对于数组和自定义数据结构尤其有用,因为可以通过指针算术进行高效的遍历和访问。
C++ 允许获取函数的地址,并将其赋给函数指针。这使得程序可以在运行时决定调用哪个函数,提高了代码的灵活性和可重用性。函数指针广泛应用于回调机制、事件驱动编程和接口设计中。
C++ 的引用机制实际上是一种特殊的语法糖,它在内部使用地址来实现。引用允许你创建一个变量的别名,通过别名可以直接访问和修改原始变量,而无需显式解引用。这样不仅代码更加简洁,而且避免了指针可能带来的错误。
C++ 通过 new
和 delete
操作符提供了动态内存分配的能力。使用 new
分配的内存位于堆区,它会返回指向分配内存的指针。通过这个指针,程序可以在运行时动态地分配、访问和释放内存。
通过上述机制,C++ 程序可以精确地控制内存的使用方式,从而优化程序性能和资源管理。然而,这也意呀着程序员必须对指针、内存地址以及内存管理有充分的理解,才能有效地利用这些特性编写高效、安全的代码。
编写教程时,目标是清晰、简洁且具有实用性。以下是一个关于“如何在 C++ 中使用指针和地址”教程的示例内容。这段内容旨在介绍初学者到 C++ 的指针和地址概念,帮助他们理解和运用“万物皆地址”的原则。
在 C++ 中,理解指针和地址是掌握语言的基石。本教程将引导你通过简单的步骤来理解和使用指针和地址。
在 C++ 中,每个变量都存储在内存的一个特定位置,这个位置可以通过地址来访问。你可以使用 &
操作符来获取变量的地址。
1 |
|
这段代码会输出变量 var
的内存地址。
指针是存储另一个变量地址的变量。在 C++ 中,你可以使用 *
来声明指针。
1 |
|
在这个例子中,ptr
是一个指向 var
地址的指针。通过 *ptr
,我们可以访问这个地址上的值。
指针在 C++ 中的应用非常广泛,包括:
new
和 delete
操作符分配和释放内存。1 |
|
这个练习演示了如何使用指针来交换两个变量的值。
在 C++ 中,通过地址修改值是指针操作的一个基本用途。指针是一种特殊的变量,其值为另一个变量的内存地址。
通过指针,你可以直接访问和修改它指向的内存位置上存储的数据。
这一过程涉及几个关键步骤:获取变量的地址、使用指针指向该地址、通过指针修改该地址上的值。
下面通过一个简单的示例来说明这一过程:
1 |
|
在上述代码中:
int
的变量 value
并初始化为 10。int*
的指针 ptr
并将其初始化为 value
的地址(&value
)。cout
输出语句,我们可以看到 value
的原始值。ptr
来修改 value
的值。这里的 *ptr = 20;
表示将 ptr
指向的内存位置(即 value
的位置)上的数据修改为 20。*ptr
是解引用操作符的应用,它获取指针指向的内存地址上存储的值。cout
输出 value
的值,可以看到它已经被修改为 20。在 C++ 中,理解函数传参的内存地址涉及到两个主要概念:按值传递(Pass by Value)和按引用传递(Pass by Reference)。理解这些概念有助于深入理解 C++ 如何在函数调用中处理参数的内存地址。
当函数参数是按值传递时,函数接收的是实参的一个副本。这意味着函数内部对参数所做的任何修改都不会影响到原始数据。在内存层面,这个过程涉及到将原始数据的值复制到新的内存地址中。这个新的地址是函数参数在函数调用栈上的局部地址。
按引用传递意味着函数接收的是实参的引用(或者说是内存地址)。这样,函数内部对参数的任何修改都会直接影响到原始数据。在内存层面,这避免了数据的复制,函数参数直接使用了实参的地址。
假设我们有一个简单的函数,目的是修改一个整数的值。
1 | // 按值传递 |
在这个例子中,addTenByValue
函数无法修改外部变量的值,因为它仅操作了参数的副本。而 addTenByReference
函数则直接操作了实参的内存地址,因此它能够修改外部变量的值。
类名:Renderer
属性
true
。Shader
对象的智能指针。Shader
对象用于编译、链接和使用顶点和片段着色器。Model
对象包含顶点数据、索引数据和纹理数据。~Renderer()
: 清理Renderer
对象,包括释放EGL资源(如显示设备、渲染表面和上下文)。width_
和height_
属性,并标记需要为着色器生成新的投影矩阵。models_
容器中。关联和依赖关系:
Shader
类:由于shader_
属性和在initRenderer()
方法中对Shader
的调用。Vertex
和Index
类型:在createModels()
方法中使用这些类型来创建模型。TextureAsset
类:在createModels()
方法中加载纹理资源。std::vector
、std::unique_ptr
。这个类图还可以展示Renderer
类如何与Android的本地应用粘合层(native_app_glue)和OpenGL ES 3.0交互,特别是如何处理EGL上下文、表面创建和渲染循环。
下面是一个简化的类图表示,重点在于Renderer
类及其直接关系:
1 | [ ] --|> [EGLDisplay] |
说明:
--|>
表示拥有或创建关系。--->
表示依赖关系。--*
表示包含或集合关系。1 |
|
这段代码定义了一个名为 uniformBuffers
的数组,它包含 MAX_CONCURRENT_FRAMES
个 UniformBuffer
对象。这种设计通常用于图形渲染中的多缓冲技术(如双缓冲或三缓冲),目的是实现帧重叠(frame overlapping)并避免资源冲突。
帧重叠(Frame Overlapping):
**MAX_CONCURRENT_FRAMES
**:
VK_MAX_FRAMES_IN_FLIGHT
。**UniformBuffer
**:
工作流程示例:
1 | // 帧N使用它的UBO |
这是现代图形编程中实现高效资源管理的典型模式。