V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
xloger
V2EX  ›  程序员

不是我不想支持 Android 10 的分区存储啊 Orz

  •  
  •   xloger · 137 天前 · 2392 次点击
    这是一个创建于 137 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我司 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...

    所幸我报着试一试的态度更新了库的依赖,解决了问题。而这个版本也是近几个月才更新修复的。

    _(:з)∠)_我已经写得身心俱疲了...不知道哪里会不会又有新的兼容性问题,好烦啊......

    23 条回复    2021-09-13 15:28:35 +08:00
    omysho
        1
    omysho   137 天前 via Android
    之前做过适配

    简单的来说,直接 request legacy 即可!

    原因:
    Android 11 恢复了媒体文件的 File API

    所以在 Android 11 是可以直接读写媒体文件的,只不过只能读写 正规媒体目录 的 媒体文件。
    secretman
        2
    secretman   137 天前
    第三方建议自己修改,我以前用知乎开源的 Matisse 做图片选择,后来升级 Android 版本适配不支持,就自己写了一个
    xloger
        3
    xloger   137 天前
    @omysho #1 我看文档的说法是 Android 11 开始就会忽略了 `android:requestLegacyExternalStorage` 属性,所以我是打算最后不行了才开这个凑合一下。
    然后 Android 11 恢复了 File API 这事,这个就是我傻逼了。这个说法我是之前就知道的,然后,当时我正在解决上面说的 `FFmpegMediaMetadataRetriever` 的 Crash 问题,这期间我尝试了各种做法,包括把 targetSdkVersion 升级到了 30,再给它传 filePath,依然不行。就给我留下了 File API 对我依旧不好使的印象。
    刚刚看到你的回帖,对我描述的第二个问题 `MediaExtractor`,换回 filePath 了,目前没有复现用 fileDescriptor 的阻塞 bug,真是喜大普奔。Android 这也真是的 Orz
    xloger
        4
    xloger   137 天前
    @secretman #2 一般的第三方框架还好,但我这次有问题的很多是涉及到 JNI 的,我试过了没改动_(:з)∠)_我至今仍不知道 `FFmpegMediaMetadataRetriever` 的作者代码是哪里写错了...
    Jirajine
        5
    Jirajine   137 天前 via Android
    盲猜 fd/uri 失效,通过文件选择器授权的访问应该是临时的,可能用户切出去再回来就不行了。
    gif 那个应该不算坑,你自己实现文件访问也要判断,而且你传个 image/gif 的 mime type 也可以避免未预料的文件类型。
    ho121
        6
    ho121   137 天前 via Android
    openAssetFileDescriptor 这个是读取 assets 文件夹下文件的吧?对于从 saf 拿到的 uri,好像是用 openInputStream 和 openOutputStream 吧
    xloger
        7
    xloger   137 天前
    @Jirajine #5 我访问的资源都是在公共目录的,不需要用户手动授权,所以应该是不存在失效问题的吧?我之前怀疑的是频繁读取释放后,可能哪里出错了导致文件处于被占用状态,然后我再访问就一直在等解除占用了。一个体现是在阻塞的时候如果等个两三分钟,那还是能正常加载出来的。我本来还是想 debug 一步一步看是内部具体哪个函数卡住了,但是又被其他事情拖住了。
    Gif 这个的确有我一部分问题,之前我担心自己判断类型不准确,或者用户某些杂七杂八的 Gif 格式不对,就 try catch 了这个框架的构造方法,如果没出错且获取帧数大于 1 则认定是 Gif 。之前这样做是没问题的。换成了 fd,在大部分手机上也是没问题的,但是一部分手机就会 Native Crash 了,我也没法捕获。
    xloger
        8
    xloger   137 天前
    @ho121 #6 我开始也是这样以为的,但其实并不是。而且在 `openFileDescriptor` 的注释中也提到了 "If at all possible,you should use {@link #openAssetFileDescriptor(Uri, String)}."虽然本质上它只是封了一层。

    我们还没支持通过 SAF 获取文件,目前是通过 MediaStore 获得的,然后视频播放我用到的 `MediaExtractor` 和 `MediaMetadataRetriever` 的 `setDataSource` 是不支持流的方式,只有 File 和 FileDescriptor 两套。
    当然,迫不得已,我也是可以通过 Uri 开个流把用户选择的音视频保存在自己私有目录。但这样就太怪了,而且显得仿佛偷用户隐私一样...不过如果要做 SAF 支持,应该只能这样做了吧...
    Jirajine
        9
    Jirajine   137 天前 via Android
    @xloger 我理解的是通过文件选择器 UI 打开一个文件,就是授权你打开该文件一次而已。我用的一些应用通过 intent 分享一个文件 URI,原应用被杀 URI 就失效了。你要是需要多次访问,那应该读进内存或者存到自己的 cache 里。
    等很长时间能正常加载听起来像 gc 回收 finalize 或后台进程被 Android 杀掉释放了资源。可能你哪里有内存泄漏或 data race,或者没有在退出的时候正确释放。
    你用的这个 gif 库可能没有妥善检查,给了错误的数据原生代码有 ub 也正常,还是最好自己先检查,并且系统本身提供 MIME type 的 api,也不用自己实现。
    dingwen07
        10
    dingwen07   137 天前 via iPhone
    为什么不用 Documents UI
    yujiang
        11
    yujiang   137 天前 via Android
    为啥不用系统内置的的文件选择啊
    john6lq
        12
    john6lq   137 天前 via iPhone
    我想想 v4 v7 v11 X Jetpack 就恶心,审美也是一言难尽。
    NSAgold
        13
    NSAgold   137 天前
    为什么不用 Documents +1
    "文档应用是 Android 系统的一部分"
    xloger
        14
    xloger   136 天前
    @dingwen07 #10

    @yujiang #11

    @NSAgold #13

    我们是剪辑 App,用户需要能预览,多选,预裁剪他们的素材,所以有一个内置的资源选择器是必然的。包括上面一直在提的,也不是选择器的内容啊,而是处理 Android 10 返回的 Uri 遇到的问题,这个靠 Documents UI 也是解决不了的。
    xloger
        15
    xloger   136 天前
    @Jirajine #9 谢谢您的意见。我们这个场景并不是通过 文件选择器 UI ( SAF )获得的 Uri,而是通过系统的 MediaStore 扫描公开多媒体库得到的,比如 DCIM 、Pictures 这些文件夹里的。按我的理解,这些 Uri 是没有权限问题的(或者说我申请了 Read Video 权限,就一直拥有了)。
    内存泄漏和正确释放的这些问题,我觉得我没有,但的确我还没靠数据验证过,我到时候尝试写个最小复现 Demo 来验证下这个问题。
    GIF 我之所以为啥不看 MIME,可能是之前处理视频,被各种奇怪的错误信息坑怕了,有分辨率错的,有帧率不准的,也有啥都没有的,所以对于 GIF,我之前就想着反正我是用这个库解析,它能解出来就是 GIF,不能解出来就当不是吧=。= 不然遇到我认定是 GIF 的但是它解析不出来,还要处理额外的兼容问题。
    xmumiffy
        16
    xmumiffy   135 天前 via Android
    想要完美兼容 SAF 就要学学 iOS 应用的做法,拿到 uri 立马往 cache 拷贝一份
    xloger
        17
    xloger   133 天前
    😓最后我的解决方案如一楼所说,换回了 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 了,我还要干啥啊。不过马上就要赶下一个需求了,先鸽了吧。
    omysho
        18
    omysho   133 天前
    @xloger #17

    其实我也走过你的这个坑,当时公司要求我做 Android 10 的适配,当时也是类似你这么做

    但是当时公司有 native 的音视频相关的库,测试倒是没发现什么坑,但是一进行 beta 上线就发现一堆问题。

    然后我就发现了 Android 11 恢复了 File API,当时我就感觉自己是个大 SB,连忙把所有东西全部回退,然后 requestLegacy 就完事了。

    不过后续我也没在跟进了,公司太过压榨人,然后我就提桶跑路了。
    xloger
        19
    xloger   133 天前
    @omysho #18 😂获得了安慰,原来不是我一个人这样...
    rosu
        20
    rosu   43 天前
    @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 的来源嘛?
    xloger
        21
    xloger   43 天前
    @rosu #20 我之前也是看官方文档的,所以也有类似你的误解。Android 11 的确不支持 requestLegacyExternalStorage 啊,但是这没影响。
    对于 Android 10,我们申请了 requestLegacyExternalStorage,所以一切能正常使用。
    对于 Android 11,requestLegacyExternalStorage 失效了,但是恢复了 File API 的支持,一样能正常使用。

    我负责的 App 自身行为是很规范的,信息存储在私有目录,唯一的问题在于有个内置多媒体选择器,这部分获取了相关权限后照旧用 File 操作就好了,不需要变动。
    rosu
        22
    rosu   43 天前
    @xloger File API 指的是 「 File 相关的 API 」 对吗?那我理解错了。我一开始以为是外部存储相关的东西呢...😂

    我纠结的地方在于:

    1. Android 10 必须启用 `requestLegacyExternalStorage`,才能使用 `WRITE_EXTERNAL_STORAGE` 权限
    2. Android 11 (事实上)却又直接支持 `WRITE_EXTERNAL_STORAGE` 权限,但文档里又写了不支持...所以我就有点懵
    xloger
        23
    xloger   43 天前
    @rosu #22 File API 指的是 「 File 相关的 API 」 ,是的。

    Android 11 对 WRITE_EXTERNAL_STORAGE 的支持我忘了,我是规范地获取读取了多媒体权限,然后只有一个写入视频的权限,所以这个对我没影响。如果你操作的是 SD 卡里系统相关的几个目录,那能操作是正常的,如果不是,那我也不太清楚了。(这些文档看完我就忘记得差不多了,它自己反复横跳搞不清...)
    关于   ·   帮助文档   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1172 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 18:46 · PVG 02:46 · LAX 11:46 · JFK 14:46
    ♥ Do have faith in what you're doing.