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 音视频屏幕旋转方案

正文

在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 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
}

CMake 中的静态连接和动态链接

作者: 朕小猫与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
cmake_minimum_required(VERSION 3.4.1)
project(DualVectorFoil)

# 先设置 Assimp 配置项,禁用测试、样例和文档生成
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_DOCS OFF CACHE BOOL "" FORCE)

# 添加 Assimp 目录和包含目录
add_subdirectory(${CMAKE_SOURCE_DIR}/Assimp)
include_directories(${CMAKE_SOURCE_DIR}/Assimp/include)

# 定义库文件 DualVectorFoilLib
add_library(
DualVectorFoilLib # 库名称
SHARED # 库类型
# 你的源文件列表,示例:src/main/cpp/你的文件.cpp
)

# 链接必要的库
target_link_libraries(
DualVectorFoilLib
assimp # Assimp 库
android # Android 系统库
EGL # OpenGL 库
GLESv2 # OpenGL ES 2.0
m # 数学库
z # 压缩库
atomic # 原子操作库
)

Q: 这样写完之后,apk 安装包中有两个so文件 libDualVectorFoilLib 和 libassimp,都占用 14m,但其实,libDualVectorFoilLib 中包含的就是libassimp,不应该再占用这么大体积

A: 为了减少 APK 中的体积,你可以将 libassimp 静态链接到 DualVectorFoilLib 中,这样只会生成一个包含 assimplibDualVectorFoilLib.so 文件,而不会包含独立的 libassimp.so 文件。这种方式可以避免重复打包和减小 APK 的体积。具体修改如下:

修改 CMakeLists.txt 配置,将 assimp 静态链接

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
cmake_minimum_required(VERSION 3.4.1)
project(DualVectorFoil)

# 先设置 Assimp 配置项,禁用测试、样例和文档生成
set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF) # 禁用 Assimp 的动态库生成,改为静态库

# 添加 Assimp 目录和包含目录
add_subdirectory(${CMAKE_SOURCE_DIR}/Assimp)
include_directories(${CMAKE_SOURCE_DIR}/Assimp/include)

# 定义库文件 DualVectorFoilLib
add_library(
DualVectorFoilLib # 库名称
SHARED # 库类型
# 你的源文件列表,示例:src/main/cpp/你的文件.cpp
)

# 链接必要的库
target_link_libraries(
DualVectorFoilLib
assimp # 静态链接 Assimp 库
android # Android 系统库
EGL # OpenGL 库
GLESv2 # OpenGL ES 2.0
m # 数学库
z # 压缩库
atomic # 原子操作库
)

解释

  1. **禁用 BUILD_SHARED_LIBS**:将 BUILD_SHARED_LIBS 设置为 OFF,使 Assimp 以静态库(.a 文件)形式生成,这样 Assimp 将不会生成独立的 .so 文件,而是将代码直接打包到 libDualVectorFoilLib.so 中。

  2. **静态链接 assimp**:在 target_link_libraries 中链接 assimp,这会将 Assimp 的代码直接合并进 libDualVectorFoilLib.so,避免生成和打包独立的 libassimp.so

  3. 结果:这样可以减小 APK 体积,因为不再包含重复的库文件。

之后重新构建项目,确保 APK 中只包含 libDualVectorFoilLib.so 文件,不再包含 libassimp.so 文件。

Q: 为什么改成静态连接之后小了好多

A: 改成静态链接之后 APK 大小减少,主要原因在于静态链接和动态链接的工作原理不同,以及 Android 应用打包时如何处理库文件:

  1. 避免重复加载的冗余文件:动态链接时,libassimp.so 作为独立的动态库被单独打包,并在运行时加载。而静态链接则将 libassimp 的代码直接整合到 libDualVectorFoilLib.so 中,避免了多余的 .so 文件。因此,只生成一个完整的 .so 文件,而不是两个重复的文件,节省了空间。

  2. 优化与裁剪:静态链接时,链接器会自动去除 Assimp 库中未使用的代码,只保留 DualVectorFoilLib 需要的部分。这意味着生成的 libDualVectorFoilLib.so 体积更小,因为一些多余的功能不会被包含进去。而动态链接时,通常整个库都被打包,即使项目不使用 Assimp 的所有功能,APK 也会包含整个库的内容。

  3. 减少符号表与动态加载信息:动态库需要额外的符号表和动态加载信息,以便在运行时进行地址解析和符号加载。静态链接的 .so 文件则不需要这些额外的信息,因此最终生成的文件会更小。

  4. 加载性能提升:除了体积减小,静态链接也会带来性能上的提升,因为它省去了动态加载库时的解析步骤,在某些场景下可以略微缩短应用启动时间。

通过静态链接,代码体积优化和去重工作会更加有效,因此可以显著减小 APK 大小。

Q: 如果我只想引入 assimp 不想引入 libDualVectorFoilLib.so 怎么处理

A: 如果只想引入 assimp 库,不需要 libDualVectorFoilLib.so,可以直接在 CMake 中只构建 assimp,而不创建其他自定义的 .so 库文件。这样可以实现只生成 libassimp.so,并确保它在你的 APK 中被正确引用。以下是可以实现的步骤:

  1. 直接构建 Assimp 并禁用其他库的生成
    只需要 assimp,无需自定义的库文件,可以移除自定义库的定义,只保留对 assimp 的配置与编译。

  2. 修改 CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    cmake_minimum_required(VERSION 3.4.1)
    project(DualVectorFoil)

    # 配置 Assimp:禁用测试、样例和文档生成
    set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
    set(ASSIMP_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
    set(ASSIMP_BUILD_DOCS OFF CACHE BOOL "" FORCE)

    # 添加 Assimp 目录并包含头文件
    add_subdirectory(${CMAKE_SOURCE_DIR}/Assimp)
    include_directories(${CMAKE_SOURCE_DIR}/Assimp/include)

    # Assimp 将自动生成 libassimp.so,无需自定义其他库
  3. **在 build.gradle 中指定加载 libassimp.so**:
    在你的 Android 项目中确保在 Java 或 Kotlin 代码中正确加载 libassimp.so,例如:

    1
    System.loadLibrary("assimp")
  4. **检查并确保只有 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 工具展示了两个关键指标来帮助我们理解这些差异:

  1. Raw File Size(原始文件大小):这是文件在 APK 解压后,在设备上占用的实际存储空间大小。在您提供的截图中,libassimpd.so 的原始文件大小为 14.4 MB,表示安装到设备后它会占用 14.4 MB 的空间。

  2. Download Size(下载大小):这是 APK 包含的文件在压缩后的大小。由于 APK 打包时会对文件进行压缩,libassimpd.so 的压缩后下载大小仅为 3.4 MB。这个值表示用户在下载 APK 包时实际需要传输的数据量。

总结来说:

  • Raw File Size 是文件在设备上解压后的实际存储空间需求。
  • Download Size 是文件在 APK 压缩包中的大小,直接影响 APK 的下载体积。

因此,虽然下载 APK 时 libassimpd.so 只占 3.4 MB,但安装后在设备上会占用 14.4 MB 的存储空间。

Q: So 文件是怎么实现从14M压缩到3.4M的

A: so 文件(共享库文件)的大小可以大幅度缩小,主要是因为 Android APK 打包过程中应用了压缩算法。以下是一些关键原因和过程:

1. ZIP 压缩

APK 文件本质上是一个 ZIP 文件,打包时会对资源文件和库文件应用 ZIP 压缩。共享库文件(so 文件)通常包含大量重复数据空白区域,这些内容在 ZIP 压缩时能够被有效地缩减。因此,像 libassimpd.so 这种大型的二进制文件,通过 ZIP 压缩可以大幅度减小体积。

2. 二进制格式的高压缩率

so 文件是二进制格式,包含大量的结构化数据代码段。这些二进制数据在结构上非常紧密,包含许多重复的模式和数据块,非常适合被压缩算法处理。常用的压缩算法(例如 DEFLATE)能够识别这些模式,减少冗余,从而将文件压缩到较小的尺寸。

3. 去除无用数据

一些调试信息和符号信息在生成 so 文件时可能会被包含在内,但是在发布 APK 时通常会将这些调试符号信息剥离。这会进一步减少 so 文件的体积,尤其在压缩之后,对这些无用数据的移除效果显著。

4. 压缩前未使用的代码和数据区域

so 文件可能包含一些预留的数据段对齐填充,这些部分在压缩后也会被进一步缩小。比如对齐填充区域在压缩时几乎不会占用空间,从而进一步减小了整体体积。

示例总结

在您的示例中,libassimpd.so 原始文件大小为 14.4 MB,而在 APK 中压缩后的下载大小是 3.4 MB。这种大幅缩小主要得益于 ZIP 压缩的高效性,以及 so 文件的二进制结构带来的高压缩比。

总结

APK 中的 so 文件能够实现从 14 MB 压缩到 3.4 MB,主要是由于以下原因:

  • ZIP 压缩算法的高效性
  • 二进制文件的结构适合压缩
  • 发布时剥离调试符号信息
  • 未使用的数据和对齐填充在压缩后基本不占空间

这些因素共同作用,使得 so 文件在安装包中的大小远小于其解压后的原始大小。

TextView layout_constrainedWidth 属性

layout_constrainedWidthConstraintLayout 中的一个核心属性。它主要解决的问题是:当一个 View 设置为 wrap_content 时,如果内容过长,它是否应该强制遵循约束(Constraints)所限定的边界。

1. 基础用法示例

在下面的代码中,TextView 设置了 wrap_content。如果没有 app:layout_constrainedWidth="true",当文本极长时,TextView 可能会挤压右侧的 Button 甚至超出屏幕。开启该属性后,TextView 的宽度将受限于 startend 约束。

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
<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/buttonView"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
android:text="极其冗长的文本示例,如果没有开启约束宽度,它会无视右侧按钮的约束直接顶过去..." />

<Button
android:id="@+id/buttonView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/textView"
app:layout_constraintEnd_toEndOf="parent"
android:text="按钮" />

</androidx.constraintlayout.widget.ConstraintLayout>

2. 源码解析:它是如何实现的?

layout_constrainedWidth 的核心逻辑位于 ConstraintLayout 的测量阶段,特别是 ConstraintWidget 对约束的处理逻辑。

2.1 属性解析

ConstraintLayout.LayoutParams 的构造函数中,会解析 XML 中的该属性:

1
2
// LayoutParams 内部代码片段
constrainedWidth = a.getBoolean(R.styleable.ConstraintLayout_Layout_layout_constrainedWidth, false);

2.2 核心逻辑:影响计算模式

该属性最终会传递给底层的求解器(Solver)。其核心逻辑在于 ConstraintWidgetsetHorizontalDimensionBehaviour 处理:

  1. Wrap Content 的默认行为
    当 View 设为 WRAP_CONTENT 时,其 DimensionBehaviour 默认为 WRAP_CONTENT。在求解约束时,求解器优先满足内容的大小,此时约束(Constraints)像是“软约束”,容易被内容撑开。

  2. 开启 constrainedWidth 后的变化
    如果 constrainedWidthtrueConstraintLayout 在调用 onMeasure 期间,会将该信息告知 ConstraintWidget。在求解引擎(Solver)内部,这会触发以下逻辑:

    • 即使是 WRAP_CONTENT,求解器也会为该 Widget 添加强制的边界约束(类似 0dp 的处理方式)。
    • 系统会计算:finalWidth = min(wrapContentWidth, constraintAvailableWidth)

2.3 关键源码链路

androidx.constraintlayout.core.widgets.ConstraintWidget 中(以 2.0+ 版本为例):

1
2
3
4
5
6
7
// 伪代码逻辑描述
if (mHorizontalResolution == WRAP_CONTENT && mConstrainedWidth) {
// 强制限制宽度不得超过约束定义的范围
// 系统会向线性求解器(Linear System)添加一组不等式约束:
// LeftSide + Margin <= WidgetStart
// RightSide - Margin >= WidgetEnd
}

简单来说,开启该属性后,ConstraintLayout 会在测量时动态地将该 View 的测量模式从单纯的“内容优先”调整为“内容优先,但约束封顶”。


3. 常见使用场景

  • 左右排列,一侧自适应,另一侧固定:防止自适应端内容太长把固定端挤出屏幕。
  • **Packed Chain (打包链)**:在 packed 链中,配合 bias 使用,可以让长文本在不超过边界的前提下,依然保持左对齐或居中对齐。

4. 总结

layout_constrainedWidth="true" 实际上是给 wrap_content 戴上了一个“约束的紧箍咒”。它让 View 在“想长多大就长多大”和“必须死死贴住约束”之间找到了一个平衡点:按内容显示,但绝不跨过约束红线。

拦截 View 触摸事件,判断滑动方向

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
anyView.setOnTouchListener(object : View.OnTouchListener {
private var initialX = 0f
private var initialY = 0f
private var hasScrolled = false

override fun onTouch(v: View?, event: MotionEvent?): Boolean {
event ?: return false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 记录初始触摸位置
initialX = event.x
initialY = event.y
hasScrolled = false
}
MotionEvent.ACTION_MOVE -> {
if (!hasScrolled) {
val diffX = event.x - initialX
val diffY = event.y - initialY

// 判断滑动方向,确保是水平滑动
if (abs(diffX) > abs(diffY)) {
val direction = if (diffX < 0) Direction.LEFT else Direction.RIGHT

handleHorizontalScroll(direction)
hasScrolled = true
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
hasScrolled = false
}
}

return false
}

private fun handleHorizontalScroll(direction: Direction) {
when (direction) {
Direction.LEFT -> { // 向左滑动
showToast("发生向左滑动")
// 在这里处理向左滑动的逻辑
}
Direction.RIGHT -> { // 向右滑动
showToast("发生向右滑动")
// 在这里处理向右滑动的逻辑
}
}
}

private fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}

enum class Direction {
LEFT, RIGHT
}
}

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 函数则直接操作了实参的内存地址,因此它能够修改外部变量的值。

Android Game Project 核心 Renderer.cpp

Renderer 类图组成:

  • 类名:Renderer

  • 属性

    • EGLDisplay display_: 用于OpenGL ES渲染的显示设备。它是一个与本地显示系统相关联的EGL显示连接。
    • EGLSurface surface_: OpenGL ES渲染的表面。这是一个EGL表面,代表可以渲染OpenGL ES图形的绘图目标。
    • EGLContext context_: OpenGL ES渲染的上下文。它是一个封装了OpenGL ES状态机的EGL渲染上下文。
    • int width_: 渲染表面的宽度,以像素为单位。
    • int height_: 渲染表面的高度,以像素为单位。
    • bool shaderNeedsNewProjectionMatrix_: 一个标志,指示是否需要为着色器生成新的投影矩阵。当渲染表面的大小改变时,这个标志会被设置为true
    • std::unique_ptr shader_: 指向当前使用的Shader对象的智能指针。Shader对象用于编译、链接和使用顶点和片段着色器。
    • std::vector models_: 包含所有要渲染的模型的容器。每个Model对象包含顶点数据、索引数据和纹理数据。

    方法

    • 析构函数 ~Renderer(): 清理Renderer对象,包括释放EGL资源(如显示设备、渲染表面和上下文)。
    • void render(): 执行渲染循环的一次迭代。这包括更新渲染状态、绘制模型和交换渲染表面的缓冲区。
    • void initRenderer(): 初始化渲染器,包括设置EGL上下文、选择EGL配置、创建渲染表面和上下文、初始化OpenGL ES状态和加载着色器。
    • void updateRenderArea(): 更新渲染区域的大小。如果渲染表面的大小发生变化,此方法更新width_height_属性,并标记需要为着色器生成新的投影矩阵。
    • void createModels(): 创建演示模型。这个方法加载模型的顶点、索引和纹理数据,然后将模型添加到models_容器中。
    • void handleInput(): 处理输入事件,如触摸和按键。这个方法从Android的输入事件队列中读取事件,并根据需要响应这些事件。
  • 关联和依赖关系

    • 依赖于Shader类:由于shader_属性和在initRenderer()方法中对Shader的调用。
    • 依赖于VertexIndex类型:在createModels()方法中使用这些类型来创建模型。
    • 依赖于TextureAsset类:在createModels()方法中加载纹理资源。
    • 使用了标准库中的类型,如std::vectorstd::unique_ptr

这个类图还可以展示Renderer类如何与Android的本地应用粘合层(native_app_glue)和OpenGL ES 3.0交互,特别是如何处理EGL上下文、表面创建和渲染循环。

下面是一个简化的类图表示,重点在于Renderer类及其直接关系:

1
2
3
4
5
6
7
8
[Renderer] --|> [EGLDisplay]
[Renderer] --|> [EGLSurface]
[Renderer] --|> [EGLContext]
[Renderer] ---> [Shader]
[Renderer] --* [Model]
[Model] --* [Vertex]
[Model] --* [Index]
[Model] ---> [TextureAsset]

说明:

  • --|> 表示拥有或创建关系。
  • ---> 表示依赖关系。
  • --* 表示包含或集合关系。
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#include "Renderer.h"

#include <game-activity/native_app_glue/android_native_app_glue.h>
#include <GLES3/gl3.h>
#include <memory>
#include <vector>
#include <android/imagedecoder.h>

#include "AndroidOut.h"
#include "Shader.h"
#include "Utility.h"
#include "TextureAsset.h"

// 宏定义,执行glGetString并将结果输出到logcat
#define PRINT_GL_STRING(s) {aout << #s": "<< glGetString(s) << std::endl;}

// 宏定义,如果glGetString返回一个空格分隔的列表,则将每个元素打印在新行上
/*!
* @brief if glGetString returns a space separated list of elements, prints each one on a new line
*
* This works by creating an istringstream of the input c-style string. Then that is used to create
* a vector -- each element of the vector is a new element in the input string. Finally a foreach
* loop consumes this and outputs it to logcat using @a aout
*/
#define PRINT_GL_STRING_AS_LIST(s) { \
std::istringstream extensionStream((const char *) glGetString(s));\
std::vector<std::string> extensionList(\
std::istream_iterator<std::string>{extensionStream},\
std::istream_iterator<std::string>());\
aout << #s":\n";\
for (auto& extension: extensionList) {\
aout << extension << "\n";\
}\
aout << std::endl;\
}

// 定义一种颜色,玉米花蓝色。可以直接发送给glClearColor函数。
#define CORNFLOWER_BLUE 100 / 255.f, 149 / 255.f, 237 / 255.f, 1

// 顶点着色器和片段着色器的代码,通常这些会从资源文件中加载
static const char *vertex = R"vertex(#version 300 es
in vec3 inPosition;
in vec2 inUV;

out vec2 fragUV;

uniform mat4 uProjection;

void main() {
fragUV = inUV;
gl_Position = uProjection * vec4(inPosition, 1.0);
}
)vertex";

static const char *fragment = R"fragment(#version 300 es
precision mediump float;

in vec2 fragUV;

uniform sampler2D uTexture;

out vec4 outColor;

void main() {
outColor = texture(uTexture, fragUV);
}
)fragment";


// 投影矩阵的半高度,这将给你一个从-2到2的高度为4的可渲染区域
static constexpr float kProjectionHalfHeight = 2.f;


// 投影矩阵的近平面距离。由于这是一个正交投影矩阵,负值便于排序(避免在0处的z-fighting)
static constexpr float kProjectionNearPlane = -1.f;

// 投影矩阵的远平面距离。与近平面等距的设置便于处理。
/*!
* The far plane distance for the projection matrix. Since this is an orthographic porjection
* matrix, it's convenient to have the far plane equidistant from 0 as the near plane.
*/
static constexpr float kProjectionFarPlane = 1.f;

// Renderer析构函数,处理EGL上下文的清理工作。
Renderer::~Renderer() {
if (display_ != EGL_NO_DISPLAY) {
eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (context_ != EGL_NO_CONTEXT) {
eglDestroyContext(display_, context_);
context_ = EGL_NO_CONTEXT;
}
if (surface_ != EGL_NO_SURFACE) {
eglDestroySurface(display_, surface_);
surface_ = EGL_NO_SURFACE;
}
eglTerminate(display_);
display_ = EGL_NO_DISPLAY;
}
}

// 渲染函数,包括渲染过程中的各种状态更新和绘制调用。
void Renderer::render() {
// Check to see if the surface has changed size. This is _necessary_ to do every frame when
// using immersive mode as you'll get no other notification that your renderable area has
// changed.
updateRenderArea();

// When the renderable area changes, the projection matrix has to also be updated. This is true
// even if you change from the sample orthographic projection matrix as your aspect ratio has
// likely changed.
if (shaderNeedsNewProjectionMatrix_) {
// a placeholder projection matrix allocated on the stack. Column-major memory layout
float projectionMatrix[16] = {0};

// build an orthographic projection matrix for 2d rendering
Utility::buildOrthographicMatrix(
projectionMatrix,
kProjectionHalfHeight,
float(width_) / height_,
kProjectionNearPlane,
kProjectionFarPlane);

// send the matrix to the shader
// Note: the shader must be active for this to work. Since we only have one shader for this
// demo, we can assume that it's active.
shader_->setProjectionMatrix(projectionMatrix);

// make sure the matrix isn't generated every frame
shaderNeedsNewProjectionMatrix_ = false;
}

// clear the color buffer
glClear(GL_COLOR_BUFFER_BIT);

// Render all the models. There's no depth testing in this sample so they're accepted in the
// order provided. But the sample EGL setup requests a 24 bit depth buffer so you could
// configure it at the end of initRenderer
if (!models_.empty()) {
for (const auto &model: models_) {
shader_->drawModel(model);
}
}

// Present the rendered image. This is an implicit glFlush.
auto swapResult = eglSwapBuffers(display_, surface_);
assert(swapResult == EGL_TRUE);
}

// 初始化渲染器,设置EGL上下文和OpenGL状态。
void Renderer::initRenderer() {
// Choose your render attributes
constexpr
EGLint attribs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_NONE
};

// The default display is probably what you want on Android
auto display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, nullptr, nullptr);

// figure out how many configs there are
EGLint numConfigs;
eglChooseConfig(display, attribs, nullptr, 0, &numConfigs);

// get the list of configurations
std::unique_ptr < EGLConfig[] > supportedConfigs(new EGLConfig[numConfigs]);
eglChooseConfig(display, attribs, supportedConfigs.get(), numConfigs, &numConfigs);

// Find a config we like.
// Could likely just grab the first if we don't care about anything else in the config.
// Otherwise hook in your own heuristic
auto config = *std::find_if(
supportedConfigs.get(),
supportedConfigs.get() + numConfigs,
[&display](const EGLConfig &config) {
EGLint red, green, blue, depth;
if (eglGetConfigAttrib(display, config, EGL_RED_SIZE, &red)
&& eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &green)
&& eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &blue)
&& eglGetConfigAttrib(display, config, EGL_DEPTH_SIZE, &depth)) {

aout << "Found config with " << red << ", " << green << ", " << blue << ", "
<< depth << std::endl;
return red == 8 && green == 8 && blue == 8 && depth == 24;
}
return false;
});

aout << "Found " << numConfigs << " configs" << std::endl;
aout << "Chose " << config << std::endl;

// create the proper window surface
EGLint format;
eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
EGLSurface surface = eglCreateWindowSurface(display, config, app_->window, nullptr);

// Create a GLES 3 context
EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
EGLContext context = eglCreateContext(display, config, nullptr, contextAttribs);

// get some window metrics
auto madeCurrent = eglMakeCurrent(display, surface, surface, context);
assert(madeCurrent);

display_ = display;
surface_ = surface;
context_ = context;

// make width and height invalid so it gets updated the first frame in @a updateRenderArea()
width_ = -1;
height_ = -1;

PRINT_GL_STRING(GL_VENDOR);
PRINT_GL_STRING(GL_RENDERER);
PRINT_GL_STRING(GL_VERSION);
PRINT_GL_STRING_AS_LIST(GL_EXTENSIONS);

shader_ = std::unique_ptr<Shader>(
Shader::loadShader(vertex, fragment, "inPosition", "inUV", "uProjection"));
assert(shader_);

// Note: there's only one shader in this demo, so I'll activate it here. For a more complex game
// you'll want to track the active shader and activate/deactivate it as necessary
shader_->activate();

// setup any other gl related global states
glClearColor(CORNFLOWER_BLUE);

// enable alpha globally for now, you probably don't want to do this in a game
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

// get some demo models into memory
createModels();
}

// 更新渲染区域的大小,如果有变化,则更新视口和投影矩阵。
void Renderer::updateRenderArea() {
EGLint width;
eglQuerySurface(display_, surface_, EGL_WIDTH, &width);

EGLint height;
eglQuerySurface(display_, surface_, EGL_HEIGHT, &height);

if (width != width_ || height != height_) {
width_ = width;
height_ = height;
glViewport(0, 0, width, height);

// make sure that we lazily recreate the projection matrix before we render
shaderNeedsNewProjectionMatrix_ = true;
}
}

// 创建演示模型的函数。
/**
* @brief Create any demo models we want for this demo.
*/
void Renderer::createModels() {
/*
* This is a square:
* 0 --- 1
* | \ |
* | \ |
* | \ |
* 3 --- 2
*/
std::vector <Vertex> vertices = {
Vertex(Vector3{1, 1, 0}, Vector2{0, 0}), // 0
Vertex(Vector3{-1, 1, 0}, Vector2{1, 0}), // 1
Vertex(Vector3{-1, -1, 0}, Vector2{1, 1}), // 2
Vertex(Vector3{1, -1, 0}, Vector2{0, 1}) // 3
};
std::vector <Index> indices = {
0, 1, 2, 0, 2, 3
};

// loads an image and assigns it to the square.
//
// Note: there is no texture management in this sample, so if you reuse an image be careful not
// to load it repeatedly. Since you get a shared_ptr you can safely reuse it in many models.
auto assetManager = app_->activity->assetManager;
auto spAndroidRobotTexture = TextureAsset::loadAsset(assetManager, "android_robot.png");

// Create a model and put it in the back of the render list.
models_.emplace_back(vertices, indices, spAndroidRobotTexture);
}

// 处理输入事件的函数,如触摸和按键事件。
void Renderer::handleInput() {
// handle all queued inputs
auto *inputBuffer = android_app_swap_input_buffers(app_);
if (!inputBuffer) {
// no inputs yet.
return;
}

// handle motion events (motionEventsCounts can be 0).
for (auto i = 0; i < inputBuffer->motionEventsCount; i++) {
auto &motionEvent = inputBuffer->motionEvents[i];
auto action = motionEvent.action;

// Find the pointer index, mask and bitshift to turn it into a readable value.
auto pointerIndex = (action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
>> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
aout << "Pointer(s): ";

// get the x and y position of this event if it is not ACTION_MOVE.
auto &pointer = motionEvent.pointers[pointerIndex];
auto x = GameActivityPointerAxes_getX(&pointer);
auto y = GameActivityPointerAxes_getY(&pointer);

// determine the action type and process the event accordingly.
switch (action & AMOTION_EVENT_ACTION_MASK) {
case AMOTION_EVENT_ACTION_DOWN:
case AMOTION_EVENT_ACTION_POINTER_DOWN:
aout << "(" << pointer.id << ", " << x << ", " << y << ") "
<< "Pointer Down";
break;

case AMOTION_EVENT_ACTION_CANCEL:
// treat the CANCEL as an UP event: doing nothing in the app, except
// removing the pointer from the cache if pointers are locally saved.
// code pass through on purpose.
case AMOTION_EVENT_ACTION_UP:
case AMOTION_EVENT_ACTION_POINTER_UP:
aout << "(" << pointer.id << ", " << x << ", " << y << ") "
<< "Pointer Up";
break;

case AMOTION_EVENT_ACTION_MOVE:
// There is no pointer index for ACTION_MOVE, only a snapshot of
// all active pointers; app needs to cache previous active pointers
// to figure out which ones are actually moved.
for (auto index = 0; index < motionEvent.pointerCount; index++) {
pointer = motionEvent.pointers[index];
x = GameActivityPointerAxes_getX(&pointer);
y = GameActivityPointerAxes_getY(&pointer);
aout << "(" << pointer.id << ", " << x << ", " << y << ")";

if (index != (motionEvent.pointerCount - 1)) aout << ",";
aout << " ";
}
aout << "Pointer Move";
break;
default:
aout << "Unknown MotionEvent Action: " << action;
}
aout << std::endl;
}
// clear the motion input count in this buffer for main thread to re-use.
android_app_clear_motion_events(inputBuffer);

// handle input key events.
for (auto i = 0; i < inputBuffer->keyEventsCount; i++) {
auto &keyEvent = inputBuffer->keyEvents[i];
aout << "Key: " << keyEvent.keyCode << " ";
switch (keyEvent.action) {
case AKEY_EVENT_ACTION_DOWN:
aout << "Key Down";
break;
case AKEY_EVENT_ACTION_UP:
aout << "Key Up";
break;
case AKEY_EVENT_ACTION_MULTIPLE:
// Deprecated since Android API level 29.
aout << "Multiple Key Actions";
break;
default:
aout << "Unknown KeyEvent Action: " << keyEvent.action;
}
aout << std::endl;
}
// clear the key input count too.
android_app_clear_key_events(inputBuffer);
}