肇鑫的技术博客

业精于勤,荒于嬉

CGImageSource对照片自动旋转问题的解决

前两天用咕唧分享照片的时候,我发现分享出去的照片,与我查看时的角度不一致,被转了90°。这是一个需要解决的问题。

分析问题1

一开始,我以为是咕唧的隐私保护功能导致的。为了保护隐私,咕唧默认会移除照片中的GPS信息和Exif信息。此外,用户可以在设置中,选择分享这些信息。

我们知道,平常拍照片,你的手机可能是横着拍,也可能是竖着拍,偶尔可能倒过来拍。但是不管你拍照时,手机处于何种角度,系统在显示该照片的时候,都会正确的显示该照片为当时你在屏幕上见到的样子。因为系统会读取照片属性中的方向信息,从而知道你拍照时手机的状态,这样就能正确的显示。

我首先就是认为Exif信息被咕唧删除了,系统没有了参照,只能按照默认的方式处理,因而与实际的方式造成了偏差。

解决问题1

我创建了一个测试项目,然后将那张出问题的照片作为资源,做同样的缩小操作,之后查看照片属性。结果我发现其实方向Orientation属性并没有保存在Exif信息,而是在图片的属性中。也就是说,事实上,咕唧缩小的图片是包含这个信息的。之前的分析是错误的。

那么是什么原因导致了包含了正确方向信息的照片,实际显示的反而是错误的呢?

分析问题2

排除了一切不可能,剩下的就是唯一的可能。

我们知道,系统在读取照片的时候,会根据方向Orientation做正确的处理。也就是说,CGImageSourceCreateImageAtIndex(_:_:_:)获得的照片本身就是正确方向的。那么这个方向正确的照片。在通过CGImageDestination重新保存时,就应该改变Orientation值为默认值,而不是继续保存原始值。

解决问题2

如果上面的分析正确,思路就很简单了。保存之前,检查方向Orientation是否为默认值,如果不是,则修改为默认值。

注意:是只要CGImageSource加载了图片,就会造成这个问题。因此,不仅缩小照片有这个问题,仅仅是去除Exif或者GPS信息,都会存在这个问题,同样需要修改方向Orientation为默认值。

思考

我在想要不要把这个问题,作为bug报告给苹果。思考的结果是不要。因为如果苹果修复了这个问题,将新照片变成与原始照片的方向一致。那么之前有做过特殊处理的应用,照片的方向就又都会从正确变成错误。这相当于是API不延续了。

为了保证之前的兼容性,好多时候,只能遗留不正确的代码。这个就是API设计出问题的代价。

参考资料

CGImageDestination写入属性时的架构

func CGImageDestinationAddImage(CGImageDestination, CGImage, CFDictionary?)func CGImageDestinationSetProperties(CGImageDestination, CFDictionary?)都可以设定一个字典参数做为写入到图片的属性,但是这两个是存在区别的。

前者是写入属性到图片,后者是写入属性到图片的容器。举例来说,如果你保存一个JPEG的图片,那么写入Exif信息,就是写入到图片。而如果你是写入一个动态Gif图片,那么动图里面的每一帧,都是单独一张图片,每个图片有自己的属性。此外,动图本身是一个容器,有自己的属性,比如一共包含了多少张图片,播放时,间隔多长时间放一张,播放结束之后是否循环播放等。

如果本该写入到图片的属性,写入到了容器,就会出现属性丢失的现象。

切记切记。

相关问题

CGImageSource对照片自动旋转问题的解决

CPU还是内存,编程中的取舍之道

最近解决了咕唧存在的一个占用内存过多的问题。

以2GB内存的iPad Pro 9.7为例,如果分享扩展使用的内存超过120MB,系统就会强制将分享扩展关闭。
从用户的角度来看,就是分享扩展闪退了。

调试模式下,苹果会在Xcode中提示内存超限时代码执行的位置,并且显示为紫色的断点。

咕唧出错的地方是图片转换的部分。当用户选择图片分享时,咕唧会在后台做多件事,以适应微博和推特服务器的要求。比如:

  1. 微博服务器要求图片大小不能超过5MB。推特要求静态图片大小不超过5MB,Gif动图不超过15MB。
  2. 此外,微博服务器对于短边超过1080像素的图片还会自动缩小到1080像素。

咕唧本身也有一些设置会影响到图片。比如:

  1. 出于隐私保护的目的,咕唧有设置默认在图片上传前,会删除图片的Exif信息和GPS信息。
  2. 出于节省流量的目的,咕唧有设置默认会缩小发布到推特的图片。

综合以上的几点,咕唧在发布前,会根据用户的账户类型不同,对于图片做额外的转换和修改。因为咕唧是跨平台的,同时支持iOS/watchOS/macOS,在图片处理时我使用的是Image I/O框架。并且当时的算法是CPU优先。因为程序中会多次使用CGImageSource,于是函数中将它做为了较长时间存在的临时变量。这样的好处是可以避免多次生成它。

这么做在iOS应用中没有问题,但是在分享扩展运行的时候,有时就会因为内存占用过多而闪退。

解决思路

Image I/O是苹果提供的底层调用,这些对象与我们平时用到的对象不一样,都是不透明的,也就是只能用,不能查看细节。

既然如此,我决定将所有使用到Image I/O框架的部分封装起来,单独构造一个类,将需要的功能暴露为函数,这样从使用者的角度,就完全看不出来是否使用了Image I/O框架。

而在这个单独构造的类中,不使用CPU优先,而是使用内存优先。虽然每次操作都会使用到CGImageSource,但是我每次使用时都会重新创建它,使用结束立即释放。这样的好处就是内存中不会长期存在一个中间变量,坏处就是会额外占用一些CPU资源。

结论

改造很成功。尝试了之前会导致分享扩展崩溃的几个图片,现在都能正常分享了。