我司 App 最近终于打算上架 Google Play 了,因此在做一些准备工作。Google Play 的上架要求是 targetSdkVersion 最低 29,也就是要适配分区存储 /存储隔离。
当然,还有一种临时的方案就是在 application 设置 android:requestLegacyExternalStorage="true"
。不过这显然也不是啥长久之计,我决定还是直接支持为好。
说一下 App 情况:我们本身行为是很干净的,只有在 DCIM 下创建了一个我们的文件夹,用于用户导出的视频。然后再有一个内置的图片 /视频 /音频选择器,供用户导入,预览内容。
我看了一通 Android 自己的 Android 10 兼容文档,和一些相关的适配文章,<del>很快就适配好了</del>。
大致思路是,我们没法直接操作非我们私有目录文件的 File 了,我们能得到的是 Uri,然后需要读就调 contentResolver.openAssetFileDescriptor
之类的方法,操作 FileDescriptor 即可。
看着是不是很简单!我也觉得!
然后我就愉快地开始测试了=。=
然后发现,项目用到的一个第三方框架,一款获取视频某个关键帧画面的框架 FFmpegMediaMetadataRetriever
,在用 FileDescriptor 解析一部分视频时,会发生 Native Crash 。我想着人家是开源项目嘛,我自己琢磨下咋修呗,然后研究了半天,也试了不同的场景,甚至还找到了当年该作者关于这个问题的提问贴,但看了一圈感觉写得没问题啊...
最后放弃了,给作者提了个 Issue 。
我把用到该框架的功能封装了一下改用 Android 自己的 MediaMetadataRetriever 实现了,姑且算是规避了这个问题。
然后,过了两天,我又发现,我如果反复加载视频(对应的用户操作就是在项目列表页反复进入编辑页退出),以前的操作是直接调系统的一个接口 MediaExtractor.setDataSource(filePath)
,没有问题。但是兼容 Android 10 传 uri 后,操作就是先 contentResolver.openAssetFileDescriptor(uri, "r")
,再 MediaExtractor.setDataSource(fileDescriptor)
,然后当操作次数多了后,openAssetFileDescriptor
就有概率阻塞住,需要过很久才会响应。
我依旧对其进行了很久的分析,自然是有 close 的,而且它的几个不同的 setDataSource
函数本质上也是互相调用的,折腾了一圈,最终结论就是似乎不是我的锅...
而且这个只在一部分手机上会出现。比如我自己的小米 11 就正常。 暂时无解。
再然后,今天早上,测试小姐姐又跟我说,在一台测试机上导图片又会 Crash 啦。我看了一下,大致原因是我们的 Gif 支持是用的一个第三方框架 android-gif-drawable
,它在 Android 11 上(也可能是一部分 Android 11 手机)如果传入一张非 Gif 图,会产生 Native Crash 。
我们可以选择先判断 exif 信息之类的,只对 gif 调用。或者再给作者提个 Issue...
所幸我报着试一试的态度更新了库的依赖,解决了问题。而这个版本也是近几个月才更新修复的。
_(:з)∠)_我已经写得身心俱疲了...不知道哪里会不会又有新的兼容性问题,好烦啊......
1
omysho 2021-06-11 16:37:06 +08:00 via Android
之前做过适配
简单的来说,直接 request legacy 即可! 原因: Android 11 恢复了媒体文件的 File API 所以在 Android 11 是可以直接读写媒体文件的,只不过只能读写 正规媒体目录 的 媒体文件。 |
2
secretman 2021-06-11 16:49:28 +08:00
第三方建议自己修改,我以前用知乎开源的 Matisse 做图片选择,后来升级 Android 版本适配不支持,就自己写了一个
|
3
xloger OP @omysho #1 我看文档的说法是 Android 11 开始就会忽略了 `android:requestLegacyExternalStorage` 属性,所以我是打算最后不行了才开这个凑合一下。
然后 Android 11 恢复了 File API 这事,这个就是我傻逼了。这个说法我是之前就知道的,然后,当时我正在解决上面说的 `FFmpegMediaMetadataRetriever` 的 Crash 问题,这期间我尝试了各种做法,包括把 targetSdkVersion 升级到了 30,再给它传 filePath,依然不行。就给我留下了 File API 对我依旧不好使的印象。 刚刚看到你的回帖,对我描述的第二个问题 `MediaExtractor`,换回 filePath 了,目前没有复现用 fileDescriptor 的阻塞 bug,真是喜大普奔。Android 这也真是的 Orz |
4
xloger OP @secretman #2 一般的第三方框架还好,但我这次有问题的很多是涉及到 JNI 的,我试过了没改动_(:з)∠)_我至今仍不知道 `FFmpegMediaMetadataRetriever` 的作者代码是哪里写错了...
|
5
Jirajine 2021-06-11 17:19:58 +08:00 via Android
盲猜 fd/uri 失效,通过文件选择器授权的访问应该是临时的,可能用户切出去再回来就不行了。
gif 那个应该不算坑,你自己实现文件访问也要判断,而且你传个 image/gif 的 mime type 也可以避免未预料的文件类型。 |
6
ho121 2021-06-11 17:42:34 +08:00 via Android
openAssetFileDescriptor 这个是读取 assets 文件夹下文件的吧?对于从 saf 拿到的 uri,好像是用 openInputStream 和 openOutputStream 吧
|
7
xloger OP @Jirajine #5 我访问的资源都是在公共目录的,不需要用户手动授权,所以应该是不存在失效问题的吧?我之前怀疑的是频繁读取释放后,可能哪里出错了导致文件处于被占用状态,然后我再访问就一直在等解除占用了。一个体现是在阻塞的时候如果等个两三分钟,那还是能正常加载出来的。我本来还是想 debug 一步一步看是内部具体哪个函数卡住了,但是又被其他事情拖住了。
Gif 这个的确有我一部分问题,之前我担心自己判断类型不准确,或者用户某些杂七杂八的 Gif 格式不对,就 try catch 了这个框架的构造方法,如果没出错且获取帧数大于 1 则认定是 Gif 。之前这样做是没问题的。换成了 fd,在大部分手机上也是没问题的,但是一部分手机就会 Native Crash 了,我也没法捕获。 |
8
xloger OP @ho121 #6 我开始也是这样以为的,但其实并不是。而且在 `openFileDescriptor` 的注释中也提到了 "If at all possible,you should use {@link #openAssetFileDescriptor(Uri, String)}."虽然本质上它只是封了一层。
我们还没支持通过 SAF 获取文件,目前是通过 MediaStore 获得的,然后视频播放我用到的 `MediaExtractor` 和 `MediaMetadataRetriever` 的 `setDataSource` 是不支持流的方式,只有 File 和 FileDescriptor 两套。 当然,迫不得已,我也是可以通过 Uri 开个流把用户选择的音视频保存在自己私有目录。但这样就太怪了,而且显得仿佛偷用户隐私一样...不过如果要做 SAF 支持,应该只能这样做了吧... |
9
Jirajine 2021-06-11 20:08:57 +08:00 via Android
@xloger 我理解的是通过文件选择器 UI 打开一个文件,就是授权你打开该文件一次而已。我用的一些应用通过 intent 分享一个文件 URI,原应用被杀 URI 就失效了。你要是需要多次访问,那应该读进内存或者存到自己的 cache 里。
等很长时间能正常加载听起来像 gc 回收 finalize 或后台进程被 Android 杀掉释放了资源。可能你哪里有内存泄漏或 data race,或者没有在退出的时候正确释放。 你用的这个 gif 库可能没有妥善检查,给了错误的数据原生代码有 ub 也正常,还是最好自己先检查,并且系统本身提供 MIME type 的 api,也不用自己实现。 |
10
dingwen07 2021-06-11 21:16:03 +08:00 via iPhone
为什么不用 Documents UI
|
11
yujiang 2021-06-11 23:19:19 +08:00 via Android
为啥不用系统内置的的文件选择啊
|
12
john6lq 2021-06-11 23:37:31 +08:00 via iPhone
我想想 v4 v7 v11 X Jetpack 就恶心,审美也是一言难尽。
|
13
NSAgold 2021-06-11 23:47:52 +08:00
为什么不用 Documents +1
"文档应用是 Android 系统的一部分" |
14
xloger OP |
15
xloger OP @Jirajine #9 谢谢您的意见。我们这个场景并不是通过 文件选择器 UI ( SAF )获得的 Uri,而是通过系统的 MediaStore 扫描公开多媒体库得到的,比如 DCIM 、Pictures 这些文件夹里的。按我的理解,这些 Uri 是没有权限问题的(或者说我申请了 Read Video 权限,就一直拥有了)。
内存泄漏和正确释放的这些问题,我觉得我没有,但的确我还没靠数据验证过,我到时候尝试写个最小复现 Demo 来验证下这个问题。 GIF 我之所以为啥不看 MIME,可能是之前处理视频,被各种奇怪的错误信息坑怕了,有分辨率错的,有帧率不准的,也有啥都没有的,所以对于 GIF,我之前就想着反正我是用这个库解析,它能解出来就是 GIF,不能解出来就当不是吧=。= 不然遇到我认定是 GIF 的但是它解析不出来,还要处理额外的兼容问题。 |
16
xmumiffy 2021-06-13 11:37:29 +08:00 via Android
想要完美兼容 SAF 就要学学 iOS 应用的做法,拿到 uri 立马往 cache 拷贝一份
|
17
xloger OP 😓最后我的解决方案如一楼所说,换回了 File 。白瞎我写好了一套 File Uri 兼容的接口。
我在最开始做兼容时,想着是遵循规范,所以尽管可以开 `requestLegacyExternalStorage` 凑合一下,我也没有选择这个。然后自己弄好了所有对 Uri 的支持,并且一部分自己的私有文件还是得用 File,也做好了相关的处理。 然后就遇到了主楼所说的各种兼容性问题。我知道 Android 11 恢复了 File API,但是在我的测试机( Android 10 )上还是不行,这是显而易见的因为我测试机还是 10 嘛,而我不可能不对 10 做兼容。 =。=然后我发现我傻逼了,还有 `requestLegacyExternalStorage` 啊,这个 API 只在 Android 10 生效。 所以,解决这个破问题的完美方案就是,继续用 File 那套,开启 `requestLegacyExternalStorage`。target SDK 用 29,30 都可以。去 TM 的 Uri,去 TM 的 FileDescriptor,一堆问题还让我用。 我本将心照明月,奈何... ps:我本来想去验证一下 `openFileDescriptor` 阻塞这个问题真不是我的锅的。用完 FileDescriotor 我就 close 了,MediaMetadataRetriever 用完我也 release 了,我还要干啥啊。不过马上就要赶下一个需求了,先鸽了吧。 |
18
omysho 2021-06-15 14:43:13 +08:00
@xloger #17
其实我也走过你的这个坑,当时公司要求我做 Android 10 的适配,当时也是类似你这么做 但是当时公司有 native 的音视频相关的库,测试倒是没发现什么坑,但是一进行 beta 上线就发现一堆问题。 然后我就发现了 Android 11 恢复了 File API,当时我就感觉自己是个大 SB,连忙把所有东西全部回退,然后 requestLegacy 就完事了。 不过后续我也没在跟进了,公司太过压榨人,然后我就提桶跑路了。 |
20
rosu 2021-09-13 10:55:32 +08:00
@xloger 所以 Google 又改回去了嘛? Android 11 的存储机制变更文章( https://developer.android.com/about/versions/11/privacy/storage )我看了好多遍,自认为没有看漏,就是不支持 `requestLegacyExternalStorage `:
> If your app targets Android 11, both the WRITE_EXTERNAL_STORAGE permission and the > WRITE_MEDIA_STORAGE privileged permission no longer provide any additional access. 结果真机一测试,发现 it juts works ?! 我现在有点懵,能请求下关于恢复 File API 的来源嘛? |
21
xloger OP @rosu #20 我之前也是看官方文档的,所以也有类似你的误解。Android 11 的确不支持 requestLegacyExternalStorage 啊,但是这没影响。
对于 Android 10,我们申请了 requestLegacyExternalStorage,所以一切能正常使用。 对于 Android 11,requestLegacyExternalStorage 失效了,但是恢复了 File API 的支持,一样能正常使用。 我负责的 App 自身行为是很规范的,信息存储在私有目录,唯一的问题在于有个内置多媒体选择器,这部分获取了相关权限后照旧用 File 操作就好了,不需要变动。 |