一、背景
最近公司中的相册组件被业务方反馈了新问题,在 targetSdk=30 的 Android 10 手机上运行相册,缩略图会加载不出来,于是就开启了这次的趟坑之路。
定位问题
首先,我在相册Demo中把 targetSdk 设置到 30, 然后在 Android 10 测试机上运行,发现缩略图完美的显示了出来。
很懵逼,为啥相同的代码 demo 上正常,业务方的 app 不正常?
一定是有什么配置不一样,才导致了这样的结果。
经过了各种找不同 ...
我发现,demo 的 AndroidManifest.xml 中多了一个属性
- <application
- android:requestLegacyExternalStorage="true"
- ...>
于是,正式开启了我的适配之路...
二、requestLegacyExternalStorage 是什么?
通过翻查官方文档,大概知道了这个属性的意思:在配置targetSdk >= 29,应用搭载在Android 10及以上版本的手机运行时,可以暂时停用「分区存储」
1.「分区存储」又是什么?
分区存储
为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储空间的分区访问权限(即分区存储)。此类应用只能访问外部存储空间上的应用专属目录,以及本应用所创建的特定类型的媒体文件。
在搭载 Android 9(API 级别 28)或更低版本的设备上,只要其他应用具有相应的存储权限,任何应用都可以访问外部存储空间中的应用专属文件。为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被授予了对外部存储空间的分区访问权限(即分区存储)。启用分区存储后,应用将无法访问属于其他应用的应用专属目录。
这是摘自官方文档的一段话,我们可以把「分区存储」简单解释为,Android 10 开启分区存储后,你的应用在有权限的情况下也无法随便访问其他外部存储空间中的公有文件夹了
2.「分区存储」会造成什么影响?
比如在App中展示相册缩略图的时候,我们会把 filepath 传给图片加载框架去帮助渲染缩略图,像这样
- ImageLoader.load(imageView, Uri.fromFile(path);
这里的 path 一般为 sdcard/DCIM/...,这明显为外部存储空间中的文件夹,且不是应用专属文件,这时在图片加载框架层就会抛出异常java.io.FileNotFoundException。
假如你用的是 Glide,会在图中的代码位置抛出异常
三、Android 11 中 requestLegacyExternalStorage 属性失效
在继续翻阅官方文档后,又得知了一个信息:
这段信息,简单可以理解为 requestLegacyExternalStorage=true 只能解燃眉之急,到了 Android 11 上,还是要做适配工作。
这也成功为我走上弯路,埋下了伏笔 ...
四、开始走弯路
1. 只适配 Android 10 (不推荐)
在Manifest中添加
- <application
- android:requestLegacyExternalStorage="true"
- ...>
我们刚才知道了,如果应用在 Android 11 的设备上运行,系统会忽略 requestLegacyExternalStorage 属性,强制开启分区存储。可能还是会出现异常(此处我并没有真正用 Android 11 的机器验证)。所以我默认认为,requestLegacyExternalStorage=true 只能解近忧,但不解本质问题。
2. 放弃 File path,使用 Uri
前文已经提到,我们用访问 File path 的方式加载缩略图,会抛出 java.io.FileNotFoundException。
那么,官方推荐我们怎么做呢?大致如下三步
- val projection = arrayOf(
- MediaStore.Video.Media._ID,
- MediaStore.Video.Media.DISPLAY_NAME,
- MediaStore.Video.Media.DURATION,
- MediaStore.Video.Media.SIZE
- )
- ...
- val query = ContentResolver.query(
- MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
- projection,
- selection,
- selectionArgs,
- sortOrder
- )
- query?.use { cursor ->
- media.id = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
- ...
- media.thumbnailUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, media.id)
- }
- // Load thumbnail of a specific media item.
- val thumbnail: Bitmap =
- applicationContext.contentResolver.loadThumbnail(
- media.thumbnailUri, Size(640, 480), null)
完整代码,可参考 developer.android.com/training/da…
由于这个变动涉及到数据源的变化,改动点非常多,并且还要用 if else 区分版本,所以写了很多胶水代码 ...
但是,最终还是成功在 targetSdk=29 Android 10 的手机上成功显示出了缩略图。
3. 新问题又出现
相册的图片预览功能也不能用了,经过排查,发现是一样的问题,胶水代码已经写好,都在射程范围内。于是,用了半小时又改掉了图片预览的问题。
正当我兴奋地觉得马上要完工的时候,点了一下视频预览 ... 好吧,看到了熟悉却又令人绝望的错误信息,依赖的播放器库抛出了熟悉的异常 java.io.FileNotFoundException open failed: EACCES (Permission denied)。播放器中也是通过 file path 传给 ffmpeg 进行播放的,但在初始化播放器的时候就因为没有权限就直接挂了。
4. 绕弯想方案
首先,我找到了播放器的开发同学进行沟通,能否用传递 uri 或者 FileDescriptor 的方式进行初始化。得到了几个不太友好的结论:
然后,开始想怎么能绕过这个问题,大概找到了 2个 不靠谱的方案:
绝大多数需要共享存储空间访问权限的应用都可以遵循共享媒体文件和共享非媒体文件方面的最佳做法。但是,某些应用的核心用例需要广泛访问设备上的文件,但无法采用注重隐私保护的存储最佳做法高效地完成这些操作。对于这些情况,Android 提供了一种名为“所有文件访问权限”的特殊应用访问权限
这段话里说的某些应用,比如「杀毒应用」「文件浏览器」,需要扫描 sdcard 的所有文件,如果没有权限就没法正常工作(很明显,我们的App不是
另外,对于这个权限的描述很有意思,长这样
如果我是用户,看到了一个不需要这些权限的App却申请了这种权限,无疑是一种劝退(产品又要骂街了
5.冷静下来,再看文档
做到第4步的时候,我开始意识到,很有可能绕弯路了,往常的适配工作还没有这么变态过。于是我又查了一些资料,找到了这个视频,https://www.youtube.com/watch?v=RjyYCUW-9tY&feature=youtu.be
视频中对我们有用的信息大概是这样,在 Android 10 的时候,很多开发者都反应了类似的问题,在使用一些 native 的库时,无法使用 File Api,造成了很多困难。于是,在 Android 11 中,又做了兼容,又可以通过 Java File Api 的方式访问媒体库文件了(此时的我不知道是不是应该高兴,Android 确实比苹果爸爸对开发者好)
后来,我又仔细的翻了翻官方文档,确实找到了一小段不起眼的文字
五、结论
好吧...
绕了一个大圈后,得到了几个结果:
教训
绕了一圈之后,得出两个教训:
* Glide 加载缩略图
最后,说个与适配不太相干的话题,只想看适配内容的朋友可以先跳过了。
我在适配的过程中也跟了一下 glide 加载缩略图的流程,也搞清了一些问题,顺便分享给大家
1. 为什么向 Glide 传 content-uri 不会出错,传 file path 会报错?
上文刚才介绍过,官方提供的获取相册缩略图的做法是
- // Load thumbnail of a specific media item.
- val thumbnail: Bitmap =
- applicationContext.contentResolver.loadThumbnail(
- media.thumbnailUri, Size(640, 480), null)
但是我们平时开发,大多都直接用图片加载框架,比如 Glide
- Glide
- .with(imageView)
- .asBitmap()
- .load(uri) //或者 file path
- .into()
在我们没适配 Android 10 的时候,传 file path 会抛出异常,这我们之前已经解释了。适配之后我们传入了 content://media/external/images/media/{media_id} 给 Glide,Glide 又是怎么识别的然后加载出 bitmap 的呢?我带着问题跟踪了一下 Glide 加载图片的过程的源码,这里我们直接先说结论。
- private InputStream loadResourceFromUri(Uri uri, ContentResolver contentResolver)
- throws FileNotFoundException {
- switch (URI_MATCHER.match(uri)) {
- case ID_CONTACTS_CONTACT:
- return openContactPhotoInputStream(contentResolver, uri);
- case ID_CONTACTS_LOOKUP:
- case ID_LOOKUP_BY_PHONE:
- // If it was a Lookup uri then resolve it first, then continue loading the contact uri.
- uri = ContactsContract.Contacts.lookupContact(contentResolver, uri);
- if (uri == null) {
- throw new FileNotFoundException("Contact cannot be found");
- }
- return openContactPhotoInputStream(contentResolver, uri);
- case ID_CONTACTS_THUMBNAIL:
- case ID_CONTACTS_PHOTO:
- case UriMatcher.NO_MATCH:
- default:
- return contentResolver.openInputStream(uri);
- }
- }
uri 经过匹配逻辑走到了 default 分支,使用 contentResolver.openInputStream(uri) 的方式来读取 bitmap,既然是通过系统的 contentResolver 获取,那一定是没问题的。
2. 浅谈 Glide 加载图片流程
这是我简单总结的 Glide 加载图片的流程,不做详细解释了,简单介绍一下图中的关键元素:
图中的过程就是这段代码运行的过程
- Glide
- .with(imageView)
- .asBitmap()
- .load(uri) //或者 file path
- .into()
1.总有一天,我们会过上我一翻身就可以偷亲你的日子。 2.即使一贫如洗,我会是...
5G网络建设加快,超前布局6G 截止目前,我国累计建成的5G基站数量超过71.8万座,...
3月15日消息 一年一度的央视财经 3.15 晚会正在进行中,从前言来看主要曝光问题...
整个欧洲向智能建筑迈进的步伐正在加快。随着各行各业的组织在客户和员工体验方...
5G切片是新商业模式的关键推动者,也是增强5G潜力的关键概念。通信服务提供商可...
1.终有那么一个人,可以随时改变着你的心情。 2.有的东西你再喜欢也不会属於你...
近年来,因高空抛物、坠物造成的伤害事件屡上报端。水瓶、西瓜皮、易拉罐,甚至...
逛个动物园要指纹打卡,连回家进小区也要刷脸验明正身会议期间,记者在浙江代表...
iOS 11~iOS 14.3的越狱工具发布了un0ver6.0.0版本 支持iOS11-iOS 14.3系统设备进...
人脸解锁扫脸支付随着人脸识别技术的不断发展,如今借助一个小小的摄像头就能让...