当前位置: 首页 > 科技观察

Android11相册适配的弯路

时间:2023-03-19 17:22:07 科技观察

一、背景近期,公司相册组件被业务方反映为新问题。在targetSdk=30的Android10手机上运行相册时,不会加载缩略图,于是开始了这次的坑之旅。定位问题首先我在相册Demo中将targetSdk设置为30,然后在Android10测试机上运行,??发现缩略图显示完美。这非常令人困惑。为什么同样的代码demo正常,但是业务方的app却不正常?一定有一些配置差异导致了这样的结果。各种找不同...发现demo的AndroidManifest.xml中多了一个属性。于是,我正式开始了我的适配之路……2.什么是requestLegacyExternalStorage?通过查看官方文档,大概知道了这个属性的含义:当配置targetSdk>=29并且应用运行在Android10以上的手机上时,可以暂时禁用“分区存储”1.“分区是什么”storage?分区存储为了让用户更好地管理他们的文件并减少混乱,针对Android10(API级别29)及更高版本的应用程序默认授予访问外部存储的权限。分区访问权限(即分区存储)。此类应用程序可以仅访问外部存储上的应用特定目录和应用创建的某些类型的媒体文件。在运行Android9(API级别28)或更低版本的设备上,任何应用都可以访问外部存储中的应用特定文件,只要其他应用具有适当的存储权限。为了让用户更好地管理他们的文件并减少混乱,默认情况下,针对Android10(API级别29)及更高版本的应用被授予对外部存储的分区访问权限(即p分区存储)。启用分区存储后,应用无法访问属于其他应用的应用特定目录。这是官方文档的摘录。“分区存储”我们可以简单的解释为,在Android10开启分区存储后,你的应用在有权限的情况下将无法访问其他外部存储空间的公共文件夹。2、“分区存储”会带来什么影响?比如在App中展示相册缩略图的时候,我们会将文件路径传给图片加载框架来帮助渲染缩略图,像这样ImageLoader.load(imageView,Uri.fromFile(path);这里的路径一般是sdcard/DCIM/...,很明显是外部存储空间的一个文件夹,不是应用程序特有的文件,这时候在图片加载框架层会抛出异常java.io.FileNotFoundException,如果你在使用Glide,图中代码位置会抛出异常3.requestLegacyExternalStorage属性在Android11中无效,继续看官方文档后,又了解到一个消息:注意:当你更新应用程序使用Android11时(API级别30)作为目标平台,如果您的应用运行在运行Android11的设备上,系统将忽略requestLegacyExternalStorage属性,因此您的应用必须准备支持分区存储并为用户迁移应用数据on那些设备。这条信息可以简单理解为requestLegacyExternalStorage=true只能解燃眉之急。在Android11上,我还需要做一些适配工作。这也为我成功的少走了弯路,埋下了伏笔……4.开始走弯路1。只兼容Android10(不推荐)在Manifest中添加我们只知道如果应用运行在Android11设备上,系统会忽略requestLegacyExternalStorage属性并强制分区启用存储。可能还是有例外(我这里并没有真正用安卓11的机器验证过)。所以我觉得默认requestLegacyExternalStorage=true只能解决近期的后顾之忧,不能解决本质。2.放弃File路径,使用Uri。如上所述,如果我们通过访问文件路径加载缩略图,将抛出java.io.FileNotFoundException。那么,我们如何做官方推荐呢?获取媒体数据id分三步:获取缩略图uri并使用uri加载缩略图valprojection=arrayOf(MediaStore.Video.Media._ID,MediaStore.Video.Media.DISPLAY_NAME,MediaStore.Video.Media.DURATION,MediaStore.Video.Media.SIZE)...valquery=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)}//Loadthumbnailofaspecificmediaitem.valthumbnail:Bitmap=applicationContext.contentResolver.loadThumbnail(.thumbnailUri,Size(640,480)),null)完整代码请参考developer.android.com/training/da...由于这次改动涉及到数据源的改动,改动比较多,ifelse是用来区分版本的,所以写了很多胶水代码...但最终使用targetSdk在手机上成功显示了缩略图=29Android10.3.新问题出现:相册图片预览功能无法使用。经过排查,发现是同样的问题。胶水代码已经写好了,在范围之内。于是,花了半个小时才把图片预览的问题改正。就在我兴奋的感觉工作即将完成的时候,我点开了视频预览……嗯,我看到了熟悉但绝望的错误信息,依赖的播放器库抛出了熟悉的异常java.io.FileNotFoundException打开失败:EACCES(权限被拒绝)。播放器也将文件路径传递给ffmpeg进行播放,但是播放器在初始化的时候因为没有权限挂掉了。4.思考解决方案首先找播放器的开发同学交流,是否可以通过uri或者FileDescriptor来初始化。得出几个不友好的结论:给Native层传uri,content://media/external/images/media/{media_id},这种UriNative层好像打不开(我没查有没有是一种通过fd到Native层的方式,可能会涉及到java层fd被Native引用无法释放的问题,如果要释放,需要打开释放fd的接口,除了相册,有很多地方需要将File路径传递给Native层,然后,开始思考如何绕过这个问题,我大概找到了2个不靠谱的方案:因为不能访问public目录,所以可以copy文件到privatedirectoryfirst(产品可能要骂你,要求MANAGE_EXTERNAL_STORAGE权限。这是一个有趣的权限,官方是这样的。绝大多数需要共享存储访问的应用程序可以遵循共享媒体文件的最佳实践和共享非媒体文件。However,一些其核心用例需要广泛访问设备上文件的应用程序无法遵循隐私保护存储最佳实践有效地完成这些操作。针对这些情况,Android提供了一种特殊的应用程序访问权限,称为“所有文件访问权限”。如果没有权限,就无法正常工作(很明显,我们的app不是别的,这个权限的描述很有意思,如果我是用户,看到一个不需要这些权限但是申请了这个权限的app,无疑是一种劝说(产品又要被骂了)5.冷静下来,当我看了文档,到了第4步的时候,才开始意识到很有可能要走弯路,而且平时的改编工作从来没有这么变态所以查了一些资料发现了这个视频https://www.youtube.com/watch?v=RjyYCUW-9tY&feature=youtu.be视频里有用的信息大概是这样的,在Android10的时候,很多开发者都反应了类似的问题,在使用一些native库的时候,FileApi无法使用,造成了很大的困难,所以在Android11中,做了兼容,可以访问了通过JavaFileApi媒体库文件(不知道要不要这个时候开心吧,安卓对开发者来说确实比苹果之父好)后来仔细翻了翻官方文档,确实发现了一小段不起眼的文字使用directfilesPath和nativelibraryaccessfiles来帮助你的app使用third-派对媒体库更流畅,Android11允许您使用MediaStoreAPI以外的API通过直接文件路径访问共享存储中的媒体文件。其中包括:文件API。本机库,例如fopen()。5.结论嗯……绕了一大圈,得到了几个结果:胶水代码可能白写了,targetSdk=29在Android10应用上运行。requestLegacyExternalStorage属性就完全够用了(白白的,一开始就鄙视了,Android11的时候就不用适配了,虽然requestLegacyExternalStorage属性失效了,但是通过FileApi访问的只是媒体库文件)专辑,不会有任何问题。如果App中有通过FileApi访问外部存储共享目录的代码,还是需要适配的。至于怎么做,本文不讨论教训。绕了一圈,得出两个教训:适配新版本到要用的时候,最好先用真机测试一下。如果它运行完美,则无需仔细调整它。只想看改编内容的朋友可以先跳过。在适配的过程中,我也按照glide中加载缩略图的流程进行了操作,也搞清楚了一些问题。顺便跟大家分享一下1.为什么给Glide传递content-uri没有报错,但是传递文件路径的时候会报错?刚刚介绍过,官方获取相册缩略图的方式是//Loadthumbnailofaspecificmediaitem.valthumbnail:Bitmap=applicationContext.contentResolver.loadThumbnail(media.thumbnailUri,Size(640,480),null)但是我们平时开发,大部分都是直接加载的图片框架,比如GlideGlide.with(imageView).asBitmap().load(uri)//或filepath.into(),当我们不适配Android10时,传递文件路径会抛出异常,我们有之前解释过。适配后,我们将content://media/external/images/media/{media_id}传给Glide,Glide是如何识别的,然后加载位图呢?有问题我跟着Glide加载图片的流程源码,这里直接说结论。privateInputStreamloadResourceFromUri(Uriuri,ContentResolvercontentResolver)throwsFileNotFoundException{switch(URI_MATCHER.match(uri)){caseID_CONTACTS_CONTACT:returnopenContactPhotoInputStream(contentResolver,uri);caseID_CONTACTS_LOOKUP:caseID_LOOKUP_BY_PHONE://IfitwasaLookupurithenresolveitfirst,thencontinueloadingthecontacturi.uri=ContactsContract.Contacts.lookupContact(contentResolver,uri);if(uri==null){thrownewFileNotFoundException("Contactcannotbefound");}returnopenContactPhotoInputStream(contentResolver,uri);caseID_CONTACTS_THUMBNAIL:caseID_CONTACTS_PHOTO:caseUriMatcher.NO_MATCH:default:returncontentResolver.openInput)分支匹配逻辑后}am(,使用contentResolver.openInputStream(uri)来读取位图,既然是通过系统的contentResolver获取的,那肯定没问题。2.浅谈Glide加载图片的过程这是我对Glide加载图片过程的简单总结。我不会详细解释。我简单介绍一下图中的关键元素:绿色圆圈是时间序列。黄色方块代表输入,粗实线代表输出。类的细实线框表示关键方法。虚线表示该方法属于哪个类。图中的过程就是运行这段代码的过程。