Todo lo que necesitas es URL

imagen

Los usuarios de VKontakte intercambian 10 mil millones de mensajes diarios. Se envían fotos, cómics, memes y otros archivos adjuntos. Le diremos cómo se nos ocurrió una aplicación iOS para cargar imágenes usando URLProtocol , y paso a paso descubriremos cómo implementar la nuestra.

Hace aproximadamente un año y medio, el desarrollo de una nueva sección de mensajes en la aplicación VK para iOS estaba en pleno apogeo. Esta es la primera sección escrita completamente en Swift. Se encuentra en un módulo separado vkm (VK Messages), que no sabe nada sobre el dispositivo de la aplicación principal. Incluso se puede ejecutar en un proyecto separado: la funcionalidad básica de leer y enviar mensajes continuará funcionando. En la aplicación principal, los controladores de mensajes se agregan a través del controlador de vista de contenedor correspondiente para mostrar, por ejemplo, una lista de conversaciones o mensajes en una conversación.

Messages es una de las secciones más populares de la aplicación móvil VKontakte, por lo que es importante que funcione como un reloj. En el proyecto de messages , luchamos por cada línea de código. Siempre nos gustó lo bien que se integran los mensajes en la aplicación, y nos esforzamos por garantizar que todo siga igual.

Llenando gradualmente la sección con nuevas funciones, abordamos la siguiente tarea: era necesario hacer que la foto que se adjunta al mensaje se muestre primero en un borrador, y después de enviarla a la lista general de mensajes. Podríamos agregar un módulo para trabajar con PHImageManager , pero las condiciones adicionales dificultaron la tarea.

imagen


Al elegir una imagen, el usuario puede procesarla: aplicar un filtro, rotar, recortar, etc. En la aplicación VK, esta funcionalidad se implementa en un componente AssetService separado. Ahora era necesario aprender a trabajar con él desde el proyecto del mensaje.

Bueno, la tarea es bastante simple, lo haremos. Esta es aproximadamente la solución promedio, porque hay muchas variaciones. Tomamos el protocolo, lo volcamos en mensajes y comenzamos a llenarlo con métodos. ¡Agregamos al AssetService, adaptamos el protocolo y agregamos nuestra implementación de caché! para viscosidad Luego colocamos la implementación en mensajes, la agregamos a algún servicio o administrador que funcionará con todo esto y comenzamos a usarla. Al mismo tiempo, todavía llega un nuevo desarrollador y, mientras trata de resolverlo todo, lo condena en un susurro ... (bueno, ya entiendes). Al mismo tiempo, el sudor aparece en su frente.

imagen


Esta decisión no fue de nuestro agrado . Aparecen nuevas entidades que los componentes del mensaje deben conocer al trabajar con imágenes de AssetService . El desarrollador también necesita hacer un trabajo extra para descubrir cómo funciona este sistema. Finalmente, había un enlace implícito adicional a los componentes del proyecto principal, que tratamos de evitar para que la sección de mensajes continúe funcionando como un módulo independiente.

Quería resolver el problema para que el proyecto no supiera nada sobre qué tipo de imagen se eligió, cómo almacenarla, si necesitaba una carga y representación especial. Además, ya tenemos la capacidad de descargar imágenes convencionales de Internet, solo que no se descargan a través de un servicio adicional, sino simplemente por URL . Y, de hecho, no hay diferencia entre los dos tipos de imágenes. Solo algunos se almacenan localmente, mientras que otros se almacenan en el servidor.

Entonces se nos ocurrió una idea muy simple: ¿qué pasa si los activos locales también se pueden aprender a cargar a través de URL ? Parece que con un solo clic de los dedos de Thanos resolvería todos nuestros problemas: no necesita saber nada sobre AssetService , agregar nuevos tipos de datos y aumentar la entropía en vano, aprender a cargar un nuevo tipo de imagen, cuidar el almacenamiento en caché de datos. Suena como un plan.

Todo lo que necesitamos es una URL


Consideramos esta idea y decidimos definir el formato de URL que usaremos para cargar activos locales:

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

Usaremos el valor de la propiedad localIdentifier de localIdentifier como PHObject , y pasaremos los parámetros width y height para cargar las imágenes del tamaño deseado. También agregamos algunos parámetros más como crop , filter , rotate , que le permitirán trabajar con la información de la imagen procesada.

Para manejar estas URL crearemos un AssetURLProtocol :

 class AssetURLProtocol: URLProtocol { } 

Su tarea es cargar la imagen a través de AssetService y devolver los datos que ya están listos para su uso.

Todo esto nos permitirá delegar casi por completo el trabajo del protocolo URL Loading System y el URL Loading System .

Dentro de los mensajes será posible operar con las URL más comunes, solo en un formato diferente. También será posible reutilizar el mecanismo existente para cargar imágenes, es muy sencillo serializar en la base de datos e implementar el almacenamiento en caché de datos a través de URLCache estándar.

¿Funcionó? Si, al leer este artículo, puede adjuntar una foto de la galería al mensaje en la aplicación VKontakte, entonces sí :)

imagen

Para dejar en claro cómo implementar su URLProtocol , propongo considerar esto con un ejemplo.

Nos propusimos la tarea: implementar una aplicación simple con una lista en la que necesita mostrar una lista de instantáneas de mapa en las coordenadas dadas. Para descargar instantáneas, utilizaremos el MKMapSnapshotter estándar de MapKit y MapKit datos a través del URLProtocol personalizado URLProtocol . El resultado podría verse más o menos así:

imagen

Primero implementamos el mecanismo para cargar datos por URL . Para mostrar la instantánea del mapa, necesitamos conocer las coordenadas del punto: su latitud y longitud ( latitude , longitude ). Defina el formato de URL personalizado mediante el cual queremos cargar información:

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

Ahora implementamos URLProtocol , que procesará dichos enlaces y generará el resultado deseado. MapURLProtocol clase MapURLProtocol , que MapURLProtocol de la clase base URLProtocol . Sin URLProtocol , a pesar de su nombre, URLProtocol es una clase abstracta. No se avergüence, aquí usamos otros conceptos: URLProtocol representa exactamente el protocolo URL y no tiene relación con los términos de OOP. Entonces MapURLProtocol :

 class MapURLProtocol: URLProtocol { } 

Ahora redefinimos algunos métodos requeridos sin los cuales el protocolo URL no funcionaría:

1. canInit(with:)


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

El canInit(with:) es necesario para indicar qué tipos de solicitudes puede manejar nuestro protocolo URL . Para este ejemplo, suponga que el protocolo solo procesará solicitudes con un esquema de map en la URL . Antes de comenzar cualquier solicitud, la URL Loading System pasa por todos los protocolos registrados para la sesión y llama a este método. El primer protocolo registrado, que en este método devolverá true , se utilizará para procesar la solicitud.

canonicalRequest(for:)


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

El método canonicalRequest(for:) está destinado a reducir la solicitud a la forma canónica. La documentación dice que la implementación del protocolo en sí mismo decide qué considerar como la definición de este concepto. Aquí puede normalizar el esquema, agregar encabezados a la solicitud, si es necesario, etc. El único requisito para que este método funcione es que para cada solicitud entrante siempre debe haber el mismo resultado, incluso porque este método también se utiliza para buscar respuestas almacenadas en caché solicitudes en URLCache .

3. startLoading()


El método startLoading() describe toda la lógica para cargar los datos necesarios. En este ejemplo, debe analizar la URL solicitud y, en función de los valores de sus parámetros de latitude y longitude , recurrir a MKMapSnapshotter y cargar la instantánea de mapa deseada.

 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) } } 

Después de recibir los datos, es necesario apagar correctamente el protocolo:

 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) } 

En primer lugar, cree un objeto de tipo URLResponse . Este objeto contiene metadatos importantes para responder a una solicitud. Luego ejecutamos tres métodos importantes para un objeto de tipo URLProtocolClient . La propiedad del client de este tipo contiene cada entidad del protocolo URL . Actúa como un proxy entre el protocolo URL y la URL Loading System completa URL Loading System , que, cuando se invocan estos métodos, saca conclusiones sobre lo que se debe hacer con los datos: caché, pasar solicitudes a completionHandler , de alguna manera procesar el cierre del protocolo, etc. y el número de llamadas a estos métodos puede variar según la implementación del protocolo. Por ejemplo, podemos descargar datos de la red con lotes y notificar periódicamente a URLProtocolClient sobre esto para mostrar el progreso de la carga de datos en la interfaz.

Si se produce un error en el funcionamiento del protocolo, también es necesario procesar y notificar correctamente a URLProtocolClient sobre esto:

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

Es este error el que luego se enviará al completionHandler la solicitud, donde se puede procesar y mostrar un hermoso mensaje al usuario.

4. stopLoading()


Se llama al método stopLoading() cuando se completa la operación del protocolo por algún motivo. Esto puede ser una finalización exitosa, o una finalización de error o una cancelación de solicitud. Este es un buen lugar para liberar recursos ocupados o eliminar datos temporales.

 override func stopLoading() { } 

Esto completa la implementación del protocolo URL ; se puede usar en cualquier lugar de la aplicación. Para saber dónde aplicar nuestro protocolo, agregue un par de cosas más.

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 } } 

Esta es una clase simple, descendiente de UIImageView , una implementación similar de la que probablemente tenga en cualquier aplicación. Aquí simplemente cargamos la imagen por la URL en el método render(url:) y la escribimos en la propiedad de la image . La conveniencia es que puede cargar absolutamente cualquier imagen, ya sea por http / https URL o por nuestra URL personalizada.

Para ejecutar solicitudes para cargar imágenes, también necesitará un objeto de tipo URLSession :

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

La configuración de la sesión es especialmente importante aquí. En URLSessionConfiguration hay una propiedad importante para nosotros: protocolClasses . Esta es una lista de los tipos de protocolos URL que puede manejar una sesión con esta configuración. De manera predeterminada, la sesión admite el procesamiento de protocolos http / https , y si se requiere soporte personalizado, deben especificarse. Para nuestro ejemplo, especifique MapURLProtocol .

Todo lo que queda por hacer es implementar el controlador de vista, que mostrará instantáneas del mapa. Su código fuente se puede encontrar aquí .

Aquí está el resultado:

imagen

¿Qué pasa con el almacenamiento en caché?


Todo parece funcionar bien, excepto un punto importante: cuando desplazamos la lista de un lado a otro, aparecen puntos blancos en la pantalla. Parece que las instantáneas no se almacenan en caché de ninguna manera y para cada llamada al método render(url:) , MKMapSnapshotter datos a través de MKMapSnapshotter . Esto lleva tiempo y, por lo tanto, tales brechas en la carga. Vale la pena implementar un mecanismo de almacenamiento en caché de datos para que las instantáneas ya creadas no se vuelvan a descargar. Aquí usamos el poder del URL Loading System , que ya cuenta con un mecanismo de almacenamiento en caché para URLCache .

Considere este proceso con más detalle y divida el trabajo con el caché en dos etapas importantes: lectura y escritura.

Lectura


Para leer correctamente los datos en caché, la URL Loading System necesita ayuda para obtener respuestas a varias preguntas importantes:

1. ¿Qué URLCache usar?

Por supuesto, ya ha finalizado URLCache.shared , pero la URL Loading System no siempre puede usarlo; después de todo, el desarrollador puede querer crear y usar su propia entidad URLCache . Para responder a esta pregunta, la URLSessionConfiguration sesión URLSessionConfiguration tiene una propiedad urlCache . Se utiliza para leer y registrar respuestas a solicitudes. URLCache algunos URLCache para estos fines en nuestra configuración existente.

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

2. ¿Necesito usar datos en caché o descargar nuevamente?

La respuesta a esta pregunta depende de la solicitud de URLRequest que estamos a punto de ejecutar. Al crear una solicitud, tenemos la oportunidad de especificar una política de caché en el argumento cachePolicy además de la URL .

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

El valor predeterminado es .useProtocolCachePolicy , que también está escrito en la documentación. Esto significa que en esta versión, la tarea de encontrar una respuesta en caché a una solicitud y determinar su relevancia recae completamente en la implementación del protocolo URL . Pero hay una manera más fácil. Si establece el valor .returnCacheDataElseLoad , al crear la siguiente entidad URLProtocol URL Loading System asumirá parte del trabajo: solicitará a urlCache respuesta en caché a la solicitud actual utilizando el cachedResponse(for:) . Si hay datos en caché, entonces un objeto de tipo CachedURLResponse se transferirá inmediatamente cuando el URLProtocol inicialice y se almacene en la propiedad cachedResponse :

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

CachedURLResponse es una clase simple que contiene datos ( Data ) y metainformación para ellos ( URLResponse ).

Solo podemos cambiar un startLoading método startLoading y verificar el valor de esta propiedad dentro de él, e inmediatamente finalizar el protocolo con estos datos:

 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) } } 

Registro


Para encontrar datos en la memoria caché, debe colocarlos allí. El URL Loading System también se encarga de este trabajo. Todo lo que se requiere de nosotros es decirle que queremos almacenar en caché los datos cuando el protocolo se cacheStoragePolicy usando la cacheStoragePolicy política de caché cacheStoragePolicy . Esta es una enumeración simple con los siguientes valores:

 enum StoragePolicy { case allowed case allowedInMemoryOnly case notAllowed } 

Significan que el almacenamiento en caché está permitido en la memoria y en el disco, solo en la memoria o está prohibido. En nuestro ejemplo, indicamos que el almacenamiento en caché está permitido en la memoria y en el disco, porque ¿por qué no?

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

Entonces, siguiendo algunos pasos simples, admitimos la capacidad de almacenar en caché las instantáneas del mapa. Y ahora la aplicación funciona así:

imagen

Como puede ver, no hay más puntos blancos: las tarjetas se cargan una vez y luego simplemente se reutilizan del caché.

No siempre es facil


Al implementar el protocolo URL , encontramos una serie de bloqueos.

El primero estaba relacionado con la implementación interna de la interacción del URL Loading System con URLCache al almacenar en caché las respuestas a las solicitudes. La documentación indica : a pesar de la seguridad del URLCache de URLCache , el funcionamiento de los cachedResponse(for:) y storeCachedResponse(_:for:) para leer / escribir respuestas a las solicitudes puede conducir a una raza de estados, por lo que este punto debe tenerse en cuenta en URLCache subclases de URLCache . Esperábamos que usando URLCache.shared este problema se resolviera, pero resultó ser incorrecto. Para solucionar esto, utilizamos un caché de ImageURLCache separado, un descendiente de URLCache , en el que ejecutamos los métodos especificados sincrónicamente en una cola separada. Como beneficio adicional, podemos configurar por separado la capacidad de caché en la memoria y en el disco por separado de otras entidades de 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) } } 

Otro problema se reprodujo solo en dispositivos con iOS 9. Los métodos para iniciar y finalizar la carga del protocolo URL se pueden realizar en diferentes subprocesos, lo que puede provocar bloqueos raros pero desagradables. Para resolver el problema, guardamos el hilo actual en el método startLoading y luego ejecutamos el código de finalización de descarga directamente en este hilo.

 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) } } } 

¿Cuándo puede ser útil un protocolo URL?


Como resultado, casi todos los usuarios de nuestra aplicación iOS de una forma u otra encuentran elementos que funcionan a través del protocolo URL . Además de descargar medios de la galería, varias implementaciones de protocolos URL nos ayudan a mostrar mapas y encuestas, así como a mostrar avatares de chat compuestos por fotos de sus participantes.

imagen

imagen

imagen

imagen

Como cualquier solución, URLProtocol tiene sus ventajas y desventajas.

Desventajas de URLProtocol


  • Falta de escritura estricta : al crear una URL esquema y los parámetros de enlace se especifican manualmente a través de cadenas. Si realiza un error tipográfico, el parámetro deseado no se procesará. Esto puede complicar la depuración de la aplicación y la búsqueda de errores en su funcionamiento. En la aplicación VKontakte, utilizamos URLBuilder especiales que forman la URL final en función de los parámetros pasados. Esta decisión no es muy hermosa y de alguna manera contradice el objetivo de no producir entidades adicionales, pero aún no hay una mejor idea. Pero sabemos que si necesita crear algún tipo de URL personalizada, entonces seguramente hay un URLBuilder especial que lo ayudará a no cometer un error.
  • URLProtocol no obvios : ya describí un par de escenarios que podrían hacer que una aplicación que usa URLProtocol bloquee. Quizás hay otros. , , , stack trace' .

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/467605/


All Articles