目标
了解MMKV
MMKV的基本应用
MMKV的原理概念
多进程设计思想
性能对比
源码解读
简介
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
官方文档:https://github.com/Tencent/MMKV/blob/master/README_CN.md
Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由Google公司和开放手机联盟领导及开发。
了解MMKV
MMKV的基本应用
MMKV的原理概念
多进程设计思想
性能对比
源码解读
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
官方文档:https://github.com/Tencent/MMKV/blob/master/README_CN.md
Flutter项目是能运行的,打开Flutter里面的Android项目才会报下面错误。
1 2 3 4 5 6 |
8:34 Gradle sync started 8:35 Gradle sync failed: Could not create task ':image_picker:generateDebugUnitTestConfig'. this and base files have different roots: D:\Pensoon\flutter_property_check_gd\build\image_picker and C:\Users\XXX\AppData\Roaming\Pub\Cache\hosted\pub.flutter-io.cn\image_picker-0.8.3+2\android. (52 s 230 ms) 8:35 Gradle sync started 8:35 Gradle sync failed: Could not create task ':image_picker:generateDebugUnitTestConfig'. this and base files have different roots: D:\Pensoon\flutter_property_check_gd\build\image_picker and C:\Users\XXX\AppData\Roaming\Pub\Cache\hosted\pub.flutter-io.cn\image_picker-0.8.3+2\android. (2 s 588 ms) |
1 2 3 4 5 6 7 8 9 |
$ .\gradlew clean build Configuration on demand is an incubating feature. FAILURE: Build failed with an exception. * What went wrong: Could not determine the dependencies of task ':url_launcher_android:test'. > Could not create task ':url_launcher_android:testDebugUnitTest'. > this and base files have different roots: D:\Source\xxxx\build\url_launcher_android and C:\Users\Administrator\AppData\Local\Pub\Cache\hosted\pub.flutter-io.cn\url_launcher_android-6.0.25\android. |
报错的项目配置信息如下:
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 |
buildscript { ext.kotlin_version = '1.8.0' repositories { maven { url "https://maven.aliyun.com/nexus/content/groups/public/" } maven { url "https://maven.aliyun.com/nexus/content/repositories/google" } google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { maven { url "https://maven.aliyun.com/nexus/content/groups/public" } maven { url "https://maven.aliyun.com/nexus/content/repositories/google" } google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } |
Flutter一开始Android build是没问题的,开发着突然就报这个下面的错误,开始怀疑是不是有什么缓存啥的,然后各种排除都没找到什么原因,后面想着降版本吧,kotlin降了没用,后面尝试最后一个Gradle降版本竟然成功了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 报错版本 classpath 'com.android.tools.build:gradle:7.0.0' // 报错版本 classpath 'com.android.tools.build:gradle:7.3.1' // 报错版本 classpath 'com.android.tools.build:gradle:7.4.2' // 解决版本 classpath 'com.android.tools.build:gradle:4.1.3' // 解决版本 classpath 'com.android.tools.build:gradle:4.2.2' |
在 Android Studio Electric Eel | 2022.1.1 Patch 2 中构建项目的时候,出现了
1 |
removeContentEntry: removed content entry url 'file://*****' still exists after removing |
这样的报错。
经过多次尝试,发现直接删除 .idea目录是有效的。
关于该问题详细的讨论可以参考 https://stackoverflow.com/questions/66214555/gradle-sync-failed-removecontententry-removed-content-entry-url-still-ex
解决idea下removeContentEntry: removed content entry url ‘file://*****‘ still exists after removing的问题
以前在 Windows系统上的 Android Studio 上编辑 gradle.properties 的时候增加了不少的中文注释,现在升级到 Android Studio Electric Eel | 2022.1.1 Patch 2 结果发现中文都变成乱码了。
原因是升级到 Android Studio Electric Eel | 2022.1.1 Patch 2 之后,Windows系统上 gradle.properties 的默认编码格式被设置成了 ISO-8859-1 ,导致文件显示乱码。
现在调整回 UTF-8 格式即可解决问题。如下图:
继续阅读解决Android Studio Electric Eel | 2022.1.1 Patch 2系统下gradle.properties中文注释乱码
构建时间太长会拖慢您的开发过程。本页将介绍一些有助于解决构建速度瓶颈的技巧。
提高应用构建速度的一般过程如下:
开发应用时,您应尽可能将其部署到搭载 Android 7.0(API 级别 24)或更高版本的设备中。较新版本的 Android 平台有更出色的机制来向您的应用推送更新,例如 Android 运行时 (ART) 以及对多个 DEX 文件的原生支持。
注意:您完成首次干净构建后,可能会注意到后续构建(干净和增量)的执行速度明显加快了(即使您没有使用本页面介绍的任何优化措施)。这是因为 Gradle 守护程序有一个性能提升“预热”期,类似于其他 JVM 进程。
按照下面的提示操作,以提高 Android Studio 项目的构建速度。
几乎每次更新时,Android 工具都会获得构建方面的优化和新功能,本页介绍的一些提示假设您使用的是最新版本。为了充分利用最新的优化措施,请确保以下工具始终是最新版本:
避免编译和打包不测试的资源(例如,其他语言本地化和屏幕密度资源)。您可以仅为“dev”变种的版本指定一个语言资源和屏幕密度,如下面的示例中所示:
1 2 3 4 5 6 7 8 9 10 11 12 |
android { ... productFlavors { dev { ... // The following configuration limits the "dev" flavor to using // English stringresources and xxhdpi screen-density resources. resourceConfigurations "en", "xxhdpi" } ... } } |
在 Android 中,所有插件都位于 google()
和 mavenCentral()
代码库中。不过,build 可能需要使用 gradlePluginPortal()
服务解析的第三方插件。
Gradle 会按照声明的顺序搜索代码库,因此,如果先列出的代码库包含大多数插件,build 性能就会得到提升。因此,您可以尝试将 gradlePluginPortal()
条目放置在 settings.gradle
文件的代码库中最靠后的位置。在大多数情况下,这样可以最大限度地减少冗余插件搜索次数,并提高构建速度。
如需详细了解 Gradle 如何导航多个代码库,请参阅 Gradle 文档中的声明多个代码库。
始终为会进入调试 build 类型的清单文件或资源文件的属性使用静态值。
您每次想运行更改时,都需要完整的应用 build 才能使用动态版本代码、版本名称、资源或可更改清单文件的任何其他构建逻辑,即使实际更改可能仅需要 1 次热交换也是如此。如果您的 build 配置需要此类动态属性,请将这些属性隔离到您的发布 build 变体中,并使相应值对您的调试 build 保持静态,如下例所示:
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 |
... // Use a filter to apply onVariants() to a subset of the variants. onVariants(selector().withBuildType("release")) { variant -> // Because an app module can have multiple outputs when using multi-APK, versionCode // is only available on the variant output. // Gather the output when we are in single mode and there is no multi-APK. val mainOutput = variant.outputs.single { it.outputType == OutputType.SINGLE } // Create the version code generating task. val versionCodeTask = project.tasks.register("computeVersionCodeFor${variant.name}", VersionCodeTask::class.java) { it.outputFile.set(project.layout.buildDirectory.file("versionCode${variant.name}.txt")) } // Wire the version code from the task output. // map will create a lazy Provider that: // 1. Runs just before the consumer(s), ensuring that the producer (VersionCodeTask) has run // and therefore the file is created. // 2. Contains task dependency information so that the consumer(s) run after the producer. mainOutput.versionCode.set(versionCodeTask.flatMap { it.outputFile.map { it.asFile.readText().toInt() } }) } ... abstract class VersionCodeTask : DefaultTask() { @get:OutputFile abstract val outputFile: RegularFileProperty @TaskAction fun action() { outputFile.get().asFile.writeText("1.1.1") } } |
如需了解如何在项目中设置动态版本代码,请参阅 GitHub 上的 setVersionsFromTask 配方。
在 build.gradle
文件中声明依赖项时,请避免使用动态版本号(以加号结尾的版本号,例如 'com.android.tools.build:gradle:2.+'
)。使用动态版本号可能会导致意外的版本更新和难以解析版本差异,并会因 Gradle 检查有无更新而减慢构建速度。 请改用静态版本号。
在应用中查找可以转换成 Android 库模块的代码。以这种方式将您的代码模块化,可以让构建系统仅编译您修改的模块,并缓存输出以用于未来的构建。此外,这种方式也会让并行项目执行更有效(当您启用该优化时)。
创建构建性能剖析报告后,如果性能剖析报告显示相当长的一部分构建时间用在了“配置项目”阶段,请检查 build.gradle
脚本并查找您可以添加到自定义 Gradle 任务中的代码。将某些构建逻辑移到任务中后,您可以确保它仅在需要时运行,可以缓存结果以用于后续构建,并且该构建逻辑将可以并行运行(如果您已启用并行项目执行)。如需详细了解自定义构建逻辑的任务,请参阅官方 Gradle 文档。
提示:如果您的构建包含大量自定义任务,您可能需要通过创建自定义任务类来整理 build.gradle
文件。将您的类添加到 project-root/buildSrc/src/main/groovy/
目录中;Gradle 会自动将这些类添加到项目中所有 build.gradle
文件的类路径中。
WebP 是一种既可以提供有损压缩(像 JPEG 一样)也可以提供透明度(像 PNG 一样)的图片文件格式,不过与 JPEG 或 PNG 相比,WebP 格式可以提供更好的压缩。
减小图片文件大小可以加快构建速度(无需在构建时进行压缩),尤其是当应用使用大量图片资源时。不过,在解压缩 WebP 图片时,您可能会注意到设备的 CPU 使用率有小幅上升。通过使用 Android Studio,您可以轻松地将图片转换为 WebP 格式。
即使您不将 PNG 图片转换为 WebP 格式,仍然可以在每次构建应用时停用自动图片压缩,以加快构建速度。
如果您使用的是 Android Gradle 插件 3.0.0 或更高版本,则系统会在默认情况下针对“调试”编译类型停用 PNG 处理。如需针对其他 build 类型停用此优化,请将以下代码添加到 build.gradle
文件中:
1 2 3 4 5 6 7 8 |
android { buildTypes { release { // Disables PNG crunching for the "release" build type. crunchPngs false } } } |
由于 build 类型或产品变种不定义此属性,因此在构建应用的发布版本时,您需要手动将此属性设置为 true
。
通过配置 Gradle 所用的最佳 JVM 垃圾回收器,可以提升构建性能。虽然 JDK 8 默认配置为使用并行垃圾回收器,JDK 9 及更高版本已配置为使用 G1 垃圾回收器。
为提高构建性能,我们建议您使用并行垃圾回收器测试 Gradle 构建。在 gradle.properties
中设置以下内容:
1 |
org.gradle.jvmargs=-XX:+UseParallelGC |
如果此字段中已设置了其他选项,请添加一个新选项:
1 |
org.gradle.jvmargs=-Xmx1536m -XX:+UseParallelGC |
如需衡量采用不同配置时的构建速度,请参阅对构建进行性能剖析。
如果您发现构建速度较慢(尤其是在 Build Analyzer 结果中发现构建时间超时 15% 的情况),则应增加 Java 虚拟化机器 (JVM) 堆大小。 在 gradle.properties
文件中,将限制设置为 4 GB、6 GB 或 8 GB,如以下示例所示:
1 |
org.gradle.jvmargs=-Xmx6g |
然后测试构建速度是否有提升。确定最佳堆大小最简单的方法是增加限额,然后测试是否有足够的构建速度提升效果。
如果您还使用 JVM 并行垃圾回收器,则整行命令应如下所示:
1 |
org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g |
您可以通过开启 HeapDumpOnOutOfMemoryError 标记分析 JVM 内存错误。这样,JVM 会在内存耗尽时生成堆转储。
使用非传递 R
类可为具有多个模块的应用构建更快的 build。这样做有助于确保每个模块的 R
类仅包含对其自身资源的引用,而不会从其依赖项中提取引用,从而帮助防止资源重复。这样可以获得更快的 build,以及避免编译的相应优势。这是 Android Gradle 插件 8.0.0 及更高版本中的默认行为。
从 Android Studio Bumblebee 开始,新项目的非传递 R
类默认处于开启状态。 对于使用早期版本的 Android Studio 创建的项目,请依次前往 Refactor > Migrate to Non-transitive R Classes,将项目更新为使用非传递 R
类。
如需详细了解应用资源和 R
类,请参阅应用资源概览。
您可在应用和测试中使用非常量 R
类字段,以提高 Java 编译的增量并允许进行更精准的资源缩减。 对库而言,R
类字段始终不是常量,因为在为依赖于相应库的应用或测试打包 APK 时,资源会进行编号。 这是 Android Gradle 插件 8.0.0 及更高版本中的默认行为。
由于大多数项目都直接使用 AndroidX 库,因此您可以移除 Jetifier 标志,以便获得更好的构建性能。如需移除 Jetifier 标志,请在 gradle.properties
文件中设置 android.enableJetifier=false
。
Build Analyzer 可以执行一项检查,确认是否可以安全移除该标记,使您的项目能够具有更好的构建性能,并不再使用未加维护的 Android 支持库。如需详细了解 Build Analyzer,请参阅排查构建性能问题。
配置缓存是一项实验性功能,可让 Gradle 记录有关构建任务图的信息,并在后续 build 中重复使用该任务图,而不必再次重新配置整个 build。
如需启用配置缓存,请按以下步骤操作:
使用 Build Analyzer 检查项目是否与配置缓存兼容。Build Analyzer 会运行一系列测试 build,以确定是否可以为项目启用该功能。请参阅问题 13490,查看受支持的插件列表。
将以下代码添加到 gradle.properties
文件:
1 2 3 4 |
org.gradle.unsafe.configuration-cache=true # Use this flag carefully, in case some of the plugins are not fully compatible. org.gradle.unsafe.configuration-cache-problems=warn |
启用配置缓存后,当您首次运行项目时,build 输出应该会显示 Calculating task graph as no configuration cache is available for tasks
。 在后续运行期间,构建输出应该会显示 Reusing configuration cache
。
如需详细了解配置缓存,请参阅配置缓存深度解析这篇博文和有关配置缓存的 Gradle 文档。
Windows 11 系统 Android Studio 从低版本升级到 Android Studio Electric Eel | 2022.1.1 Patch 1 结果构建报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ flutter run Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source! Using hardware rendering with device sdk gphone x86 64 arm64. If you notice graphics artifacts, consider enabling software rendering with "--enable-software-rendering". Launching lib\main.dart on sdk gphone x86 64 arm64 in debug mode... FAILURE: Build failed with an exception. * What went wrong: The supplied javaHome seems to be invalid. I cannot find the java executable. Tried location: D:\Program Files\Android\Android Studio\jre\bin\java.exe * Try: > Run with --stacktrace option to get the stack trace. > Run with --info or --debug option to get more log output. > Run with --scan to get full insights. * Get more help at https://help.gradle.org Running Gradle task 'assembleDebug'... 6.3s Exception: Gradle task assembleDebug failed with exit code 1 |
原因为 Android Studio Electric Eel | 2022.1.1 Patch 1 安装目录下存在 jre 文件夹的残留,需要移除这个残留目录即可解决问题。
这是我在网上找到的一份 Android 键值对存储方案的性能测试对比(数越小越好):
可以看出,DataStore 的性能比 MMKV 差了一大截。MMKV 是腾讯在 2018 年推出的,而 DataStore 是 Android 官方在 2020 年推出的,并且它的正式版在 2021 年 8 月才发布。一个官方发布的、更(gèng)新的库,性能竟然比不过比它早两年发布的、第三方的库。而且我们能看到,更离谱的是,它甚至还比不过 SharedPreferences 。Android 官方当初之所以推出 DataStore,就是要替代掉 SharedPreferences,并且主要原因之一就是 SharedPreferences 有性能问题,可是测试结果却是它的性能不如 SharedPreferences。
所以,这到底是为什么?
首先去 Docker 官网下载 macOS 版本的 Docker Desktop 版本(目前(2023/01/06)的最新版本是 4.15.0 (93002)),并安装。
1 2 3 4 5 6 7 8 9 10 |
$ brew install mysql # 设置管理员密码 $ mysqladmin -u root password # 手工创建数据库 $ mysql -u root -p -e "create database sonic default character set utf8 collate utf8_general_ci;" # 配置域名重定向 $ echo '127.0.0.1 host.docker.internal' | sudo tee -a /etc/hosts |
Android Studio 要求: 3.0以上版本 运行的真机或模拟器要求:最好8.0以上系统,低版本的手机获取不到数据。
对于 Android R(11) 使用 ContentResolver 检索图片,音乐,视频文件,参考 Android R(11) ContentResolver报错java.lang.IllegalArgumentException: Invalid token limit 里面的代码即可实现。但是如果想上传 PDF,TXT等文件的时候,则会发现系统无任何数据返回。
下面探讨一下如何解决这个问题:
首先,我们需要对Android的储存有所了解
Android储存器可分为内部储存和外部储存,这里的内部储存和外部储存不是说有两个物理储存器而是系统在硬盘上划分了两个专用目录用作内部储存和外部储存。简单来说,我们通过系统文件管理器看到的目录都属于外部储存,外部储存又可分为三类目录,私有目录、公共目录、其它目录,而内部储存对于用户是隐藏的,如数据库、SharedPreferences等文件都放在内部储存中。
1 2 3 4 5 6 |
//内部储存的文件目录获取方法,打印路径:/data/user/0/{应用包名}/files // Context.getFilesDir() //内部储存的缓存目录获取方法,打印路径:/data/user/0/{应用包名}/cache Context.getCacheDir() |
内部储存对应的目录为/data/user/0/{应用包名},该目录下应用有权限进行文件操作,目录对外不可见,应用删除对应的目录也会被删除。
1 2 3 4 5 6 |
//私有目录的文件目录获取方法,打印路径:/storage/emulated/0/Android/data/{应用包名}/files //方法参数可选,例如传入Environment.DIRECTORY_PICTURES拿到的目录为/storage/emulated/0/Android/data/com.example.android11/files/Pictures Context.getExternalFilesDir(null) //私有目录的缓存目录获取方法,打印路径:/storage/emulated/0/Android/data/{应用包名}/cache Context.getExternalCacheDir() |
私有目录获取和内部储存获取方式类似,都有file和cache目录,且该目录下应用有权限进行文件操作,目录对外可见,应用删除对应的目录也会被删除。
从Android11开始,私有目录不能被外部访问,即使获取了“所有文件管理”权限也不行(当然也是有其它方式可以实现Data目录的访问,不过目前看来并不完美)
Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等目录都是公共目录,Android11前可以通过文件路径直接访问,Android11后需要通过MediaStore来进行访问。
外部储存中除了私有目录和公共目录外都是其它目录,Android11后不能直接对其它目录进行访问。
Android10中已经加入了分区储存机制,不过是非强制的,适配Android10只需在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"即可。而在Android11已经强制应用使用分区储存。
1 2 3 |
<!-- manifest中注册 --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> |
开启授权页面
1 2 3 4 5 |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageEmulated()) { val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } |
获取“所有文件管理”的权限可以读写除私有目录外的所有文件,但是这种权限一般为文件管理类的软件才需要申请。一般APP申请此类权限若上架Google,华为等应用市场大概率被拒。
应用如果有做文件选择上传类的功能可以使用此方式,通过启动一个系统的文件浏览页面,选择需要的文件后返回一个uri,之后将uri转成流上传或者将通过uri复制文件到私有目录再操作复制后的文件进行上传,这里切记不能直接将uri转成File去进行操作。
1 2 3 4 5 |
//启动SAF文件选择 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(MimeTypeMap.getSingleton().getMimeTypeFromExtension("pdf"));//这里以打开PDF选择为例 startActivityForResult(intent, 10086); |
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 |
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 10086 && resultCode == getActivity().RESULT_OK) { if (data.getData() != null) { uriToFileApiQ(this, data.getData()) } } } //将uri对应的文件复制一份到私有目录,之后就可以操作复制后的File了 @RequiresApi(Build.VERSION_CODES.Q) public File uriToFileApiQ(Context context, Uri uri) { File file = null; if (uri == null) return file; //android10以上转换 if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { file = new File(uri.getPath()); } else if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { //把文件复制到沙盒目录 ContentResolver contentResolver = context.getContentResolver(); String displayName = "uritofile" + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentResolver.getType(uri)); InputStream is = null; try { is = contentResolver.openInputStream(uri); File cache = new File(context.getCacheDir().getAbsolutePath(), displayName); FileOutputStream fos = new FileOutputStream(cache); byte[] b = new byte[1024]; while ((is.read(b)) != -1) { fos.write(b);// 写入数据 } file = cache; fos.close(); is.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return file; } |