肇鑫的技术博客

业精于勤,荒于嬉

咕唧在iPad浮屏时,有时会意外地重新加载账户头像问题的处理

咕唧在iPad浮屏时,既屏幕右侧向里滑动弹出咕唧,有时会出现全部头像闪动一下的问题。这个问题没什么规律,如果立即你把它划回去,再重新滑出来,它就又没有这个问题了。另外,我在iOS 12的时候也没有遇到过这个问题。

我认为这可能是iOS 13 beta独有的一个问题。

我尝试解决这个问题。

方案一

因为视图是UITableView,我尝试减少reloadData()的次数。仔细阅读代码,修改了几处限定条件,减少了reloadData()。并且出于调试目的,我在预期不应该发生的位置,插入fatalError()强制结束应用。

测试结果

方案一减少了重新加载表格视图的次数,但是对于解决该问题无效。测试中发现,偶尔还是会出现重新加载头像的问题,并且,应用没有崩溃,也就是说,我预期不会执行的部分,也的确没有执行。

方案二

因为问题是和头像有关,于是着重查看头像部分的代码。

cell.imageView?.image = profileImage
            
DispatchQueue.main.async {
    ProfileImageHelper.decorate(cell.imageView!, for: accountCore.type)
}

这段代码使用了异步代码,在UI队列里执行了对于UIImageView的装饰。这里必须使用的原因是cell.imageView是只读属性,不能直接替代。并且,当cell.imageView?.image = profileImage之后,如果立即装饰图片视图,图片视图的尺寸还是0,必须在下一个事件循环才能获得实际尺寸。

imageView_decoration

上图是咕唧账户头像的部分截图。照片原本是正方形,在实际显示时,被处理为圆形。并且,根据账户类型的不同,加上了颜色不同的圆圈。

我们知道,异步的代码,与同步不同,是分成两次执行的。因此,存在一种可能,既第一部分的代码执行了之后,很久,第二部分才执行。这在显示上,就会出现先显示方形的头像,然后才变成圆形和圆圈,就像看幻灯片那样。

须要将此部分的代码由异步改为同步。

此前采用异步的原因是无法立即知道图片视图的大小,改成同步,就必须修改代码,手动指定视图的大小。我们知道,这在平时其实是一种不好的行为。代码写死了,就失去了灵活性。但是这里,我们不得不把它写死。

通过调试,我们知道这里目前的大小是48.0点。(点,point,是苹果使用的相对大小的尺寸单位)。

cell.imageView?.image = profileImage
ProfileImageHelper.decorate(cell.imageView!, for: accountCore.type)

测试结果

这次的测试很成功,之后没有发现再次出现这个问题。

方案三

虽然问题解决了,但是写死代码还是不够灵活。思考了一下,虽然图片视图的尺寸不能立即知道,但是图片的尺寸是可以知道的。因此,将写死的代码,改为使用图片的尺寸。这样问题就彻底解决了。

思考

虽然这个问题很有可能是iOS 13 beta引入的,但是解决掉这个问题,还是有收获的。我们在实际编程中,应该尽量减少异步代码的使用。因为,有时,异步代码会带来意想不到的问题。

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对照片自动旋转问题的解决