您只需要URL

图片

VKontakte用户每天交换100亿条消息。 他们互相发送照片,漫画,模因和其他附件。 我们将告诉您如何使用iOS应用程序使用URLProtocol上传图像,然后逐步了解如何实现自己的图像。

大约一年半之前,iOS VK应用程序中新消息部分的开发如火如荼。 这是完全用Swift编写的第一部分。 它位于单独的模块vkm (VK消息)中,该模块对主应用程序的设备一无所知。 它甚至可以在单独的项目中运行-阅读和发送消息的基本功能将继续起作用。 在主应用程序中,消息控制器通过相应的“容器视图控制器”添加,以显示例如对话列表或对话中的消息。

消息是VKontakte移动应用程序中最受欢迎的部分之一,因此,它必须像时钟一样工作,这一点很重要。 在messages项目中,我们为每一行代码而战。 我们一直非常喜欢将消息内置到应用程序中的方式多么整洁,并且我们努力确保所有内容保持不变。

逐步用新功能填充了该部分,我们完成了以下任务:我们必须确保邮件中附带的照片首先显示在草稿中,然后在发送后在邮件的常规列表中显示。 我们可以仅添加一个模块以与PHImageManager一起PHImageManager ,但是其他条件使该任务更加困难。

图片


选择快照时,用户可以对其进行处理:应用滤镜,旋转,修剪等。在VK应用程序中,此类功能是在单独的AssetService组件中实现的。 现在有必要从消息项目中学习与他合作。

好吧,任务很简单,我们会做的。 这大约是平均解决方案,因为存在很多差异。 我们采用协议,将其转储到消息中并开始用方法填充它。 我们添加到AssetService,修改协议并添加我们的缓存实现! 粘度。 然后,我们将实现放入消息中,将其添加到可以使用所有这些功能的服务或管理器中,然后开始使用它。 同时,仍然有一位新的开发人员来,尽管他试图弄清所有问题,但他还是低声谴责了……(嗯,你知道的)。 同时,额头上出现汗水。

图片


这个决定不符合我们的喜好 。 出现新的实体时,消息组件在使用AssetService图像时需要了解。 开发人员还需要做额外的工作来弄清楚该系统如何工作。 最后,还有一个指向主项目各组件的隐式链接,我们试图避免这些隐式链接,以便消息部分继续作为独立模块工作。

我想解决问题,以使该项目完全不了解选择哪种图片,如何存储图片,是否需要特殊的加载和渲染。 而且,我们已经具有从Internet下载常规图像的能力,只有它们不是通过附加服务下载的,而仅仅是通过URL 。 并且,实际上,两种图像之间没有区别。 只有一些存储在本地,而另一些存储在服务器上。

因此,我们提出了一个非常简单的想法:如果还可以学习通过URL加载本地资产怎么办? 似乎只要按一下Thanos的手指,就可以解决我们所有的问题:您无需了解AssetService任何AssetService ,添加新的数据类型和徒劳地增加熵,学会加载新型图像,处理数据缓存。 听起来像是一个计划。

我们只需要一个URL


我们考虑了这个想法,并决定定义用于加载本地资产的URL格式:

 asset://?id=123&width=1920&height=1280 

我们将localIdentifier属性的localIdentifier用作PHObject ,并将传递widthheight参数以加载所需大小的图像。 我们还添加了一些其他参数,例如cropfilterrotate ,这将使您能够处理已处理图像的信息。

为了处理这些URL我们将创建一个AssetURLProtocol

 class AssetURLProtocol: URLProtocol { } 

它的任务是通过AssetService加载图像,并返回已经准备好使用的数据。

所有这些将使我们几乎可以完全委托URL协议和URL Loading System

在消息内部,可以使用最常见的URL (仅以不同的格式)进行操作。 还可以重用现有的机制来加载图像,在数据库中序列化非常简单,并通过标准URLCache实现数据缓存。

奏效了吗? 如果您在阅读本文时可以将图库中的照片附加到VKontakte应用程序上的消息中,则可以:)

图片

为了清楚说明如何实现URLProtocol ,我建议通过一个示例来考虑这一点。

我们为自己设定了任务:使用列表实现一个简单的应用程序,您需要在该列表中显示给定坐标处的地图快照列表。 要下载快照,我们将使用MKMapSnapshotter的标准MapKit ,并将通过自定义URLProtocol加载数据。 结果可能如下所示:

图片

首先,我们实现了通过URL加载数据的机制。 要显示地图快照,我们需要知道点的坐标-它的纬度和经度( latitudelongitude )。 定义我们要用来加载信息的定制URL格式:

 map://?latitude=59.935634&longitude=30.325935 

现在,我们实现URLProtocol ,它将处理此类链接并生成所需的结果。 让我们创建MapURLProtocol类,该类URLProtocol基类URLProtocol继承。 尽管名称, URLProtocol还是一个抽象类。 别为难,这里我们使用其他概念URLProtocol表示URL协议,与OOP术语无关。 所以MapURLProtocol

 class MapURLProtocol: URLProtocol { } 

现在,我们重新定义一些必需的方法,没有这些方法, URL协议将无法使用:

1. canInit(with:)


 override class func canInit(with request: URLRequest) -> Bool { return request.url?.scheme == "map" } 

需要canInit(with:)方法来指示我们的URL协议可以处理哪些类型的请求。 对于此示例,假设协议仅处理URL具有map方案的请求。 在开始任何请求之前,“ URL Loading System将通过为该会话注册的所有协议并调用此方法。 第一个注册的协议(在此方法中将返回true )将用于处理请求。

canonicalRequest(for:)


 override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } 

canonicalRequest(for:)方法旨在将请求减少为规范形式。 该文档说,协议本身的实现决定了该概念的定义。 在这里,您可以规范化方案,如有必要,可以向请求中添加标头,等等。此方法起作用的唯一要求是,对于每个传入请求,总应具有相同的结果,包括因为此方法还用于搜索缓存的答案URLCache请求。

3. startLoading()


startLoading()方法描述了用于加载必要数据的所有逻辑。 在此示例中,您需要解析请求URL并基于其latitudelongitude参数的值,转到MKMapSnapshotter并加载所需的地图快照。

 override func startLoading() { guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems else { fail(with: .badURL) return } load(with: queryItems) } func load(with queryItems: [URLQueryItem]) { let snapshotter = MKMapSnapshotter(queryItems: queryItems) snapshotter.start( with: DispatchQueue.global(qos: .background), completionHandler: handle ) } func handle(snapshot: MKMapSnapshotter.Snapshot?, error: Error?) { if let snapshot = snapshot, let data = snapshot.image.jpegData(compressionQuality: 1) { complete(with: data) } else if let error = error { fail(with: error) } } 

收到数据后,有必要正确关闭协议:

 func complete(with data: Data) { guard let url = request.url, let client = client else { return } let response = URLResponse( url: url, mimeType: "image/jpeg", expectedContentLength: data.count, textEncodingName: nil ) client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) client.urlProtocol(self, didLoad: data) client.urlProtocolDidFinishLoading(self) } 

首先,创建URLResponse类型的对象。 该对象包含用于响应请求的重要元数据。 然后,我们对URLProtocolClient类型的对象执行三种重要的方法。 此类型的client属性包含URL协议的每个实体。 它充当URL协议和URL Loading System的整个URL Loading System之间的代理,当调用这些方法时,该代理得出有关数据需要完成的结论:缓存,将请求发送到completionHandler ,以某种方式处理协议关闭等。并且这些方法的调用次数可能会根据协议的实现而有所不同。 例如,我们可以批量从网络下载数据,并定期通知URLProtocolClient以显示界面中数据加载的进度。

如果协议的操作中发生错误,则还必须正确处理并通知URLProtocolClient

 func fail(with error: Error) { client?.urlProtocol(self, didFailWithError: error) } 

正是这个错误,然后将其发送到请求的completionHandler处理程序,可以在其中进行处理并向用户显示优美的消息。

4. stopLoading()


当协议操作由于某种原因而完成时,将调用stopLoading()方法。 这可以是成功完成,也可以是错误完成或请求取消。 这是释放占用的资源或删除临时数据的好地方。

 override func stopLoading() { } 

这样就完成了URL协议的实现;可以在应用程序中的任何位置使用它。 要在哪里应用我们的协议,请添加更多内容。

URLImageView


 class URLImageView: UIImageView { var task: URLSessionDataTask? var taskId: Int? func render(url: URL) { assert(task == nil || task?.taskIdentifier != taskId) let request = URLRequest(url: url) task = session.dataTask(with: request, completionHandler: complete) taskId = task?.taskIdentifier task?.resume() } private func complete(data: Data?, response: URLResponse?, error: Error?) { if self.taskId == task?.taskIdentifier, let data = data, let image = UIImage(data: data) { didLoadRemote(image: image) } } func didLoadRemote(image: UIImage) { DispatchQueue.main.async { self.image = image } } func prepareForReuse() { task?.cancel() taskId = nil image = nil } } 

这是一个简单的类,它是UIImageView的后代,它可能在任何应用程序中都具有类似的实现。 在这里,我们只是通过render(url:)方法中的URL加载图像并将其写入image属性。 方便之处在于,您可以通过http / https URL或我们的自定义URL上传任何图像。

要执行加载图像的请求,您还需要一个URLSession类型的对象:

 let config: URLSessionConfiguration = { let c = URLSessionConfiguration.ephemeral c.protocolClasses = [ MapURLProtocol.self ] return c }() let session = URLSession( configuration: config, delegate: nil, delegateQueue: nil ) 

会话配置在这里尤为重要。 在URLSessionConfiguration ,us的一个重要属性是protocolClasses 。 这是具有此配置的会话可以处理的URL协议类型的列表。 默认情况下,会话支持处理http / https协议,如果需要自定义支持,则必须指定它们。 对于我们的示例,指定MapURLProtocol

剩下要做的就是实现视图控制器,它将显示地图快照。 它的源代码可以在这里找到。

结果如下:

图片

缓存呢?


一切似乎都运行良好-除了一个重要点:当我们前后滚动列表时,屏幕上会出现白点。 似乎快照没有以任何方式缓存,并且对于render(url:)方法的每次调用,我们都通过MKMapSnapshotter数据。 这需要时间,因此在装载方面存在间隙。 值得实施一种数据缓存机制,以便不再下载已创建的快照。 在这里,我们使用URL Loading System ,该URL Loading System已经具有URLCache提供的URLCache缓存机制。

更详细地考虑此过程,并将缓存的工作分为两个重要阶段:读取和写入。

读书


为了正确读取缓存的数据, URL Loading SystemURL Loading System需要获得一些重要问题的答案的帮助:

1.使用什么URLCache?

当然, URLCache.shared已经完成,但是URL Loading SystemURL Loading System不能总是使用它-毕竟,开发人员可能想要创建并使用自己的URLCache实体。 为了回答这个问题, URLSessionConfiguration会话URLSessionConfiguration具有urlCache属性。 它用于读取和记录对请求的响应。 URLCache ,我们将在现有配置中URLCache一些URLCache

 let config: URLSessionConfiguration = { let c = URLSessionConfiguration.ephemeral c.urlCache = ImageURLCache.current c.protocolClasses = [ MapURLProtocol.self ] return c }() 

2.我需要使用缓存的数据还是再次下载?

这个问题的答案取决于我们将要执行的URLRequest请求。 创建请求时,除了URL之外,我们还可以在cachePolicy参数中指定一个缓存策略。

 let request = URLRequest( url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 30 ) 

默认值为.useProtocolCachePolicy ,该值也写在文档中。 这意味着在此版本中,查找对请求的缓存响应并确定其相关性的任务完全在于URL协议的实现。 但是,有一种更简单的方法。 如果设置值.returnCacheDataElseLoad ,则在创建下一个实体URLProtocol URL Loading System将承担一些工作:它将使用cachedResponse(for:)方法向urlCache询问对当前请求urlCache缓存响应。 如果存在缓存的数据,则将在初始化URLProtocol立即传输类型为CachedURLResponse的对象,并将其存储在cachedResponse属性中:

 override init( request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { super.init( request: request, cachedResponse: cachedResponse, client: client ) } 

CachedURLResponse是一个简单的类,其中包含数据( Data )和元Data信息( URLResponse )。

我们只能startLoading更改一下startLoading方法并检查其中的该属性的值,然后立即使用以下数据结束协议:

 override func startLoading() { if let cachedResponse = cachedResponse { complete(with: cachedResponse.data) } else { guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems else { fail(with: .badURL) return } load(with: queryItems) } } 

记录


要在缓存中查找数据,您需要将其放置在缓存中。 URL Loading System也负责这项工作。 我们所要做的就是告诉她我们要使用cacheStoragePolicy缓存策略cacheStoragePolicy在协议cacheStoragePolicy时缓存数据。 这是具有以下值的简单枚举:

 enum StoragePolicy { case allowed case allowedInMemoryOnly case notAllowed } 

它们意味着允许在内存和磁盘中缓存,仅在内存中或禁止缓存。 在我们的示例中,我们指出允许在内存和磁盘中进行缓存,因为为什么不这样做。

 client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) 

因此,通过执行一些简单的步骤,我们支持了缓存地图快照的功能。 现在,该应用程序的工作方式如下:

图片

如您所见,没有更多的白点-卡被加载一次,然后可以从缓存中简单地重用。

并不总是那么容易


在实施URL协议时,我们遇到了一系列崩溃。

第一个与在缓存对请求的响应时URL Loading SystemURLCache的内部实现有关。 该文档指出 :尽管URLCache具有URLCache安全性, URLCache用于读取/写入对请求的响应的cachedResponse(for:)storeCachedResponse(_:for:)方法的操作可能导致状态竞争,因此, URLCache子类中应考虑这一点。 我们期望使用URLCache.shared可以解决此问题,但事实证明这是错误的。 为了解决这个问题,我们使用了一个单独的ImageURLCache缓存( URLCache的后代),在该缓存中,我们在一个单独的队列上同步执行指定的方法。 令人高兴的是,我们可以与其他URLCache实体分别配置内存和磁盘上的缓存容量。

 private static let accessQueue = DispatchQueue( label: "image-urlcache-access" ) override func cachedResponse(for request: URLRequest) -> CachedURLResponse? { return ImageURLCache.accessQueue.sync { return super.cachedResponse(for: request) } } override func storeCachedResponse(_ response: CachedURLResponse, for request: URLRequest) { ImageURLCache.accessQueue.sync { super.storeCachedResponse(response, for: request) } } 

另一个问题仅在具有iOS 9的设备上重现。用于开始和结束URL协议加载的方法可以在不同的线程上执行,这可能导致罕见但令人不愉快的崩溃。 为了解决该问题,我们将当前线程保存在startLoading方法中,然后直接在该线程上执行下载完成代码。

 var thread: Thread! override func startLoading() { guard let url = request.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems else { fail(with: .badURL) return } thread = Thread.current if let cachedResponse = cachedResponse { complete(with: cachedResponse) } else { load(request: request, url: url, queryItems: queryItems) } } 

 func handle(snapshot: MKMapSnapshotter.Snapshot?, error: Error?) { thread.execute { if let snapshot = snapshot, let data = snapshot.image.jpegData(compressionQuality: 0.7) { self.complete(with: data) } else if let error = error { self.fail(with: error) } } } 

什么时候可以使用URL协议?


结果,几乎我们iOS应用程序的每个用户都以一种或另一种方式遇到通过URL协议起作用的元素。 除了从图库中下载媒体外, URL协议的各种实现方式还可以帮助我们显示地图和民意调查,以及显示由参与者照片组成的聊天头像。

图片

图片

图片

图片

像任何解决方案一样, URLProtocol也有其优点和缺点。

URLProtocol缺点


  • 缺乏严格的键入 -创建URL方案和链接参数是通过字符串手动指定的。 如果输入错误,则不会处理所需的参数。 这会使应用程序的调试和在其操作中查找错误的过程变得复杂。 在VKontakte应用程序中,我们使用特殊的URLBuilder ,它们根据传递的参数形成最终的URL 。 这个决定不是很漂亮,并且与不生产其他实体的目标有些矛盾,但是还没有更好的主意。 但是我们知道,如果您需要创建某种自定义URL ,那么可以肯定有一个特殊的URLBuilder可以帮助您避免犯错。
  • 非显而易见的崩溃 -我已经描述了几种可能导致使用URLProtocol的应用程序崩溃的情况。 也许还有其他。 但是,像往常一样,这些问题可以通过更仔细地阅读文档或通过深入研究堆栈跟踪并找到问题的根源来解决。

URLProtocol的好处


  • 弱组件连通性 -应用程序中启动其所需数据加载的部分可能根本不知道其组织方式:为此使用了哪些组件,如何安排缓存。我们只知道某种格式URL,并且只能与之交互。
  • 实现的简单性 -为了正确执行URL协议,只需执行几种简单的方法并注册协议即可。之后,可以在应用程序中的任何位置使用它。
  • — , , URL -. URL , URLSession , URLSessionDataTask .
  • URL - URL -, URL Loading System .
  • * API — . , API, - , URL -. , API , . URL - http / https .

URL - — . . - , - , , , — , . , , — URL .

GitHub

Source: https://habr.com/ru/post/zh-CN467605/


All Articles