iOS开发tips-PhotoKit
概述
photokit应该是ios 8 开始引入为了替代之前alassetslibrary的相册资源访问的标准库,后者在ios 9开始被弃用。当然相对于alassetslibrary其扩展性更高,api使用起来也更加的强大,但这并非今天讨论的重点,这里主要讨论photokit使用的一些技巧和容易踩的坑。
phimagemanager or custom
访问相册资源常用的操作还是获取资源,比如获取一张相册的图片,可以通过phimagemanager.default().requestimage(xxx)一个回调就可以了,其实除了简单的图片拉取苹果建议大家使用一个自己维护的phcachingimagemanager(phimagemanager子类)单例,因为从名字可以看出它可以进行资源缓存(具体用法参见:phcachingimagemanager),这个在列表滑动过程中比较有用,因为可以使用startcachingimages(for:targetsize:contentmode:options:)预加载还未显示的资源,在不需要使用的时候调用stopcachingimages(for:targetsize:contentmode:options:)移除缓存,除此之外其实它更多的是使用父类的功能。
关于requestimage(for:targetsize:contentmode:options:resulthandler:)方法其实它的options是更容易让大家使用的时候经常迷糊的地方,因为ios相册的照片很可能并不在本地而是在icloud上面存储,本地只有缩略图:
- isnetworkaccessallowed:是不是打开网络加载,对于icloud优化的图片本地只有缩略图,需要了开启网络加载,默认是false,但是建议非特殊情况下不要关闭网络加载设为true
- issynchronous:获取图片时是否同步获取,否则异步获取,非特殊情况下不建议使用同步获取会阻塞线程,默认为false
- version:使用资源的版本,例如说一个视频获取过程中是要获取编辑过的还是获取原始视频(因为ios的相册编辑时可以恢复的,所以其实里面包含了编辑信息),默认current对于未编辑的则获取原始资源,编辑过则获取编辑过的资源。
- deliverymode:字面意思资源交付模式,就是当发起一个请求后系统如何提供请求的资源(不过很可能这个资源并不在本地),默认是opportunistic同步调用只返回一种资源异步调用返回多种资源(例如不同尺寸的图片,这也就是说和上面的issynchronous相关);highqualityformat任何情况下只返回高质量的一种资源;fastformat任何情况下只返回一种结果,它可能是低清图片(是不是低清晰度可以通过resulthandler结果中的info字段phimageresultisdegradedkey来判断是不是低清晰度)。
- resizemode:重设尺寸模式,这个其实关系到方法中targetsize参数,默认是fast代表当本地是原图则返回原图,本地是缩略图则使用targetsize来获取一个最优图片,但是尺寸可能比targetsize要略大(注意如果targetsize是phimagemanagermaximumsize则会拉取原图此属性此时没有意义直接忽略);none则返回原图大小;exact同fast但是返回的高清图。但是在异步情况下因为可能存在两种图片会稍有不同,下面分两种情况介绍。
issynchronous和deliverymode、resizemode之间的关系(前提是开启网络加载isnetworkaccessallowed = true):
issynchronous = true同步加载:此时deliverymode会被忽略掉,所以只要看resizemode
- none:返回原图尺寸
- fast:原图为缩略图时使用targetsize优化,返回一个可能比targetsize稍大的图片
- exact:返回指定targetsize的高清图片
issynchronous = false异步加载:此时要看deliverymode和resizemode两者的变化情况
-
deliverymode = opportunistic
- none:先返回低清图片,再返回原图
- fast:先返回低清图片,再返回使用targetsize优化,可能比targetsize稍大的图片
- exact:先返回低清图片,再返回指定targetsize的高清图片
-
deliverymode = highqualityformat
- none:返回原图
- fast:返回使用targetsize优化,可能比targetsize稍大的图片
- exact:返回指定targetsize的高清图片
-
deliverymode = fastformat
- none:返回低清图片
- fast:返回低清图片
- exact:返回低清图片
对于是否返回两次结果总结起来对于返回结果只有异步请求在不设置deliverymode或者deliverymode = opportunistic 时会发送两次请求。
没错上面的情况如果了解不清楚很容易掉进坑里,一般的情况下只要打开网络,使用默认值即可。但是话说没有网络的情况下是什么情况呢?
无网络情况
上面说了那么多配置那么对于没有网络的情况呢?因为phimagerequestoptions默认其实就是没有网络的。先看一下下面的请求:
let requestoption = phimagerequestoptions() requestoption.resizemode = .exact let scale:cgfloat = uiscreen.main.scale let newsize = cgsize(width: 200.0, height: 200) phimagemanager.default().requestimage(for: asset, targetsize: newsize, contentmode: .aspectfill, options: requestoption,resulthandler: completehandler)
首先没有设置isnetworkaccessallowed属性,那么默认值就是false,这个时候也就是没有任何网络请求,其次resizemode = .exact也就是请求比较高清的图片。但是一个重要的问题是这个图片可能会是一个低清图片,甚至达不到一个200200的缩略图的预期,有可能特别模糊。
按照前面说的,deliverymode 没有设置默认是opportunistic,issynchronous*没有设置默认是false,在异步情况下是 首先这种情况下也会返回两个结果,打印info信息:
第一次:
[anyhashable("phimageresultisdegradedkey"): 1, anyhashable("phimageresultrequestidkey"): 1100]
第二次:
[anyhashable("phimageresultisdegradedkey"): 0, anyhashable("phimageerrorkey"): error domain=nscocoaerrordomain code=-1 "(null)", anyhashable("phimageresultisincloudkey"): 1, anyhashable("phimageresultrequestidkey"): 1100]
可以看出第一次是低清图片,第二次是高清,但是此时出错了phimageresultisincloudkey = 1,本地并没有高清图,但是因为无法访问网络去获取就报错了,抛出phimageerrorkey信息,同时此时返回的image = nil,这样的结果就是只能看到低清图片。因此是否返回清晰照片除了上面说的,还和是否允许网络请求有直接关系。
综上来看除非特殊情况下,请打开网络请求使用异步请求,并且在合适的时机判断是否请求返回两次。
视频获取
对于视频资源的获取和图片还有些不同,因为使用photokit上传视频的过程并不是直接获取到视频路径来做的,当然如果不熟悉这个过程很可能使用下面的方式:
let option = phvideorequestoptions() option.isnetworkaccessallowed = true option.version = .current option.deliverymode = .highqualityformat phimagemanager.default().requestavasset(forvideo: phasset, options: option) { (avasset, _, _) in if let avasset = avasset as? avurlasset { let fileurl = avasset.url // upload by fileurl } }
这套代码遇到的问题很可能是ios 10之前的系统上传失败,之后的基本还是可以传成功的,主要是因为这个url路径在低版本系统访问受限。
正确的姿势应该是先把视频导出到沙盒然后从本地上传,那么如何copy到本地呢?答案是使用phassetresourcemanager,这个类出现的比较晚在ios 9才引入的,就是为了方便资源管理,可以通过他将本地相册的资源写入沙盒然后上传沙盒的资源。
let resources = phassetresource.assetresources(for: phasset) let options = phassetresourcerequestoptions() options.isnetworkaccessallowed = true phassetresourcemanager.default().writedata(for: assetresource, tofile: videourl, options: options, completionhandler: { (error) in // upload by videourl })
但是这并非就万事大吉了,接下来的一个bug就是,通过这种方式如果上传用户已经编辑过的资源会发现它上传的是编辑之前的(比如说用户将60s的视频通过系统相册剪辑到50s,它上传的还是60s的),因为它相比较于phimagemanager请求可以设置phvideorequestoptions.version它缺少了这个信息,查找api也没有发现可以用的设置,所以这种方式还是不可取(如果有朋友知道怎么使用这种方式获取不同的version可以留言告诉我)。所以这时可以采用phimagemanager的另一个方法requestexportsession(xxx)将视频导出到沙盒然后上传,但是这么做的另一个小问题就是如果想要导出原始视频就不太可能了,只能尽可能拿到一个更高清晰度的视频。
let videooption = phvideorequestoptions() videooption.version = phvideorequestoptionsversion.current videooption.isnetworkaccessallowed = true phimagemanager.default().requestexportsession(forvideo: phasset, options: videooption, exportpreset: avassetexportpresethighestquality) { (exportsession, info) in if let exportsession = exportsession { exportsession.outputurl = videourl exportsession.outputfiletype = avfiletype.mp4 exportsession.exportasynchronously { if exportsession.status == avassetexportsession.status.completed { // upload by videourl } } }
话说到了这里是不是就真的没事了?试着用下面的视频上传试试:
testvideo.zip
exportsession.status应该会失败,当然如果你是ios 13应该是可以的,但是如果是ios 13以下在当前环境测试都是通不过的,这个视频查看meta信息也没有什么特殊的,但是这个是android导出的一个视频,失败信息是:无法完成此操作。除此之外并没有有用的信息,初步猜测导出时应该是编解码出错了。
其实到了这里,用上面的方式应该就无法解决这个问题了,至少目前没有找到有效的办法,兜底策略就是失败后采用上面的第二种方式。