肇鑫的技术博客

业精于勤,荒于嬉

Swift 5 String补遗

Swift 5中的String采用了UTF-8编码。而NSString是UTF-16编码的。NSStringString的转换是lazy的,这句话充满了刀光剑影。

所谓lazySwift中最常见的用法,简单的描述就是,当在需要复制的时候,不进行复制,而仅标记,然后如果后面的操作是读操作,就一直读,直到出现了写操作,才会真正将内容分离写入。这么做的好处,是性能比较好,如果有幸最终也没有写入操作,那么就完全省去了写入操作和额外的内存占用。

不过由于StringNSString的编码不同,这种lazy导致了一个严重的问题。就是如果你从某个框架获得了一个String,你其实是不知道它是原生的String,还是过来的NSString。比如你读取了一个String.Index,等你要用的时候,它可能已经失效了。

举一个简单的例子:

import Foundation

let ns:NSString = "ab两只老虎,两只老虎,跑得快,跑得快。"
var s = ns as String

let aIndex = s.firstIndex(of: "只")!
print(s[aIndex]) // 只
s += ""
print(s[aIndex]) // \270

为了解决上面的问题,Swift有两项硬性规定。

  1. 对于String.Index,索引只对于它自身的String。使用非自身字符串的索引,可能导致未知的问题。
  2. String只要有任何改变,String.Index都应该重新获取。

解决办法

由于String.Index非常容易失效,且不能直接使用。因此,在一个字符串使用另一个字符串的索引是需要转换才能使用。但是,这种转换,Swift本身是没有直接提供的。需要自己算一下。

import Foundation

extension String {
    func sameIndex(_ index:String.Index, of str:String) -> String.Index? {
        let offSet = self.distance(from: self.startIndex, to: index)
        return str.index(str.startIndex, offsetBy: offSet, limitedBy: str.endIndex)
    }
}

let ns:NSString = "ab两只老虎,两只老虎,跑得快,跑得快。"
var s = ns as String

let aIndex = s.firstIndex(of: "只")!
print(s[aIndex]) // 只
let s1 = s + ""
let i1 = s.sameIndex(aIndex, of: s1)!
print(s1[i1]) // 只

于此类似,Range<String.Index>也有同样的问题。更扩大一步说,只要是支持Collection类型的,都有这个问题。

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资源。

结论

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