Os usuários do VKontakte trocam 10 bilhões de mensagens diariamente. Eles enviam fotos, quadrinhos, memes e outros anexos.
URLProtocol
como criamos um aplicativo iOS para fazer upload de imagens usando o
URLProtocol
e, passo a passo, descobriremos como implementar nossos próprios.
Há cerca de um ano e meio, o desenvolvimento de uma nova seção de mensagens no aplicativo VK para iOS estava em pleno andamento. Esta é a primeira seção escrita inteiramente em Swift. Ele está localizado em um módulo separado
vkm
(VK Messages), que não sabe nada sobre o dispositivo do aplicativo principal. Pode até ser executado em um projeto separado - a funcionalidade básica de ler e enviar mensagens continuará funcionando. No aplicativo principal, os controladores de mensagens são adicionados por meio do Container View Controller correspondente para exibir, por exemplo, uma lista de conversas ou mensagens em uma conversa.
Mensagens é uma das seções mais populares do aplicativo móvel VKontakte, por isso é importante que funcione como um relógio. No projeto de
messages
, lutamos por todas as linhas de código. Sempre gostamos muito de como as mensagens são organizadas de maneira organizada no aplicativo e nos esforçamos para garantir que tudo permaneça o mesmo.
Ao preencher gradualmente a seção com novas funções, abordamos a seguinte tarefa: tivemos que garantir que a foto anexada à mensagem fosse exibida pela primeira vez em um rascunho e, depois de enviá-la, na lista geral de mensagens. Poderíamos simplesmente adicionar um módulo para trabalhar com o
PHImageManager
, mas condições adicionais tornaram a tarefa mais difícil.
Ao escolher uma imagem, o usuário pode processá-la: aplicar um filtro, girar, cortar etc. No aplicativo VK, essa funcionalidade é implementada em um componente
AssetService
separado. Agora era necessário aprender a trabalhar com ele no projeto da mensagem.
Bem, a tarefa é bem simples, vamos fazer. Esta é aproximadamente a solução média, porque há muitas variações. Pegamos o protocolo, colocamos em mensagens e começamos a preenchê-lo com métodos. Adicionamos ao AssetService, adaptamos o protocolo e adicionamos nossa implementação de cache! para viscosidade. Em seguida, colocamos a implementação em mensagens, adicionamos a algum serviço ou gerente que irá trabalhar com tudo isso e começamos a usá-lo. Ao mesmo tempo, um novo desenvolvedor ainda aparece e, enquanto tenta descobrir tudo, ele condena em um sussurro ... (bem, você entende). Ao mesmo tempo, o suor aparece na testa.
Esta decisão
não foi
do nosso agrado . Novas entidades parecem que os componentes da mensagem precisam conhecer ao trabalhar com imagens do
AssetService
. O desenvolvedor também precisa fazer um trabalho extra para descobrir como esse sistema funciona. Por fim, havia um link implícito adicional para os componentes do projeto principal, que tentamos evitar para que a seção de mensagens continue funcionando como um módulo independente.
Queria resolver o problema para que o projeto não soubesse nada sobre que tipo de imagem foi escolhida, como armazená-la, se precisava de carregamento e renderização especiais. Além disso, já temos a capacidade de baixar imagens convencionais da Internet, mas elas não são baixadas por meio de um serviço adicional, mas simplesmente por
URL
. E, de fato, não há diferença entre os dois tipos de imagens. Apenas alguns são armazenados localmente, enquanto outros são armazenados no servidor.
Então, tivemos uma idéia muito simples: e se os recursos locais também puderem ser aprendidos a carregar via
URL
? Parece que, com um clique dos dedos de
Thanos , isso resolveria todos os nossos problemas: você não precisa saber nada sobre o
AssetService
, adicionar novos tipos de dados e aumentar a entropia em vão, aprender a carregar um novo tipo de imagem, cuidar do cache de dados. Parece um plano.
Tudo o que precisamos é de um URL
Consideramos essa ideia e decidimos definir o formato do
URL
que usaremos para carregar ativos locais:
asset://?id=123&width=1920&height=1280
Usaremos o valor da propriedade
localIdentifier
de
localIdentifier
como o
PHObject
e passaremos os parâmetros de
width
e
height
para carregar as imagens do tamanho desejado. Também adicionamos mais alguns parâmetros, como
crop
,
filter
,
rotate
, o que permitirá que você trabalhe com as informações da imagem processada.
Para lidar com esses
URL
criaremos um
AssetURLProtocol
:
class AssetURLProtocol: URLProtocol { }
Sua tarefa é carregar a imagem através do
AssetService
e retornar os dados que já estão prontos para uso.
Tudo isso nos permitirá delegar quase completamente o trabalho do protocolo de
URL Loading System
e do
URL Loading System
.
Dentro das mensagens, será possível operar com os
URL
mais comuns, apenas em um formato diferente. Também será possível reutilizar o mecanismo existente para carregar imagens, é muito simples serializar no banco de dados e implementar o cache de dados por meio do
URLCache
padrão.
Isso deu certo? Se, lendo este artigo, você puder anexar uma foto da galeria à mensagem no aplicativo VKontakte, então sim :)
Para deixar claro como implementar seu
URLProtocol
, proponho considerar isso com um exemplo.
Nós nos propusemos a tarefa: implementar um aplicativo simples com uma lista na qual você precisa exibir uma lista de instantâneos de mapa nas coordenadas fornecidas. Para fazer o download de instantâneos, usaremos o
MKMapSnapshotter
padrão do
MapKit
e carregaremos dados através do
URLProtocol
personalizado. O resultado pode ser algo como isto:
Primeiro, implementamos o mecanismo para carregar dados por
URL
. Para exibir o instantâneo do mapa, precisamos conhecer as coordenadas do ponto - latitude e longitude (
latitude
,
longitude
). Defina o formato de
URL
personalizado pelo qual queremos carregar informações:
map://?latitude=59.935634&longitude=30.325935
Agora, implementamos o
URLProtocol
, que processará esses links e gerará o resultado desejado. Vamos criar a classe
MapURLProtocol
, que herdaremos da classe base
URLProtocol
. Apesar do nome, o
URLProtocol
é uma classe abstrata, no entanto. Não tenha vergonha, aqui usamos outros conceitos: o
URLProtocol
representa exatamente o protocolo de
URL
e não tem relação com os termos de POO. Então
MapURLProtocol
:
class MapURLProtocol: URLProtocol { }
Agora redefinimos alguns métodos necessários sem os quais o protocolo de
URL
não funcionará:
1. canInit(with:)
override class func canInit(with request: URLRequest) -> Bool { return request.url?.scheme == "map" }
O
canInit(with:)
é necessário para indicar quais tipos de solicitações nosso protocolo de
URL
pode manipular. Neste exemplo, suponha que o protocolo processe apenas solicitações com um esquema de
map
na
URL
. Antes de iniciar qualquer solicitação, o
URL Loading System
percorre todos os protocolos registrados para a sessão e chama esse método. O primeiro protocolo registrado, que neste método retornará
true
, será usado para processar a solicitação.
canonicalRequest(for:)
override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request }
O método
canonicalRequest(for:)
tem como objetivo reduzir a solicitação para o formato canônico. A documentação diz que a própria implementação do protocolo decide o que é considerado a definição desse conceito. Aqui você pode normalizar o esquema, adicionar cabeçalhos à solicitação, se necessário, etc. O único requisito para esse método funcionar é que, para cada solicitação recebida, sempre haja o mesmo resultado, inclusive porque esse método também é usado para procurar respostas em cache solicitações no
URLCache
.
3. startLoading()
O método
startLoading()
descreve toda a lógica para carregar os dados necessários. Neste exemplo, é necessário analisar a
URL
da solicitação e, com base nos valores de seus parâmetros de
latitude
e
longitude
,
MKMapSnapshotter
para
MKMapSnapshotter
e carregue a captura instantânea do mapa desejada.
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) } }
Após receber os dados, é necessário desligar corretamente o 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) }
Primeiro, crie um objeto do tipo
URLResponse
. Este objeto contém metadados importantes para responder a uma solicitação. Em seguida, executamos três métodos importantes para um objeto do tipo
URLProtocolClient
. A propriedade do
client
deste tipo contém cada entidade do protocolo de
URL
. Ele atua como um proxy entre o protocolo da
URL
e a
URL Loading System
inteira
URL Loading System
, que, ao chamar esses métodos, tira conclusões sobre o que precisa ser feito com os dados: armazenar em cache, enviar solicitações para o
completionHandler
, de alguma forma processar o desligamento do protocolo etc. e o número de chamadas para esses métodos pode variar dependendo da implementação do protocolo. Por exemplo, podemos baixar dados da rede com lotes e notificar periodicamente o
URLProtocolClient
sobre isso para mostrar o progresso do carregamento de dados na interface.
Se ocorrer um erro na operação do protocolo, também será necessário processar e notificar corretamente o
URLProtocolClient
sobre isso:
func fail(with error: Error) { client?.urlProtocol(self, didFailWithError: error) }
É esse erro que será enviado para a
completionHandler
solicitação, onde pode ser processado e uma bela mensagem exibida ao usuário.
4. stopLoading()
O método
stopLoading()
é chamado quando a operação do protocolo foi concluída por algum motivo. Isso pode ser uma conclusão bem-sucedida, uma conclusão de erro ou um cancelamento de solicitação. É um bom lugar para liberar recursos ocupados ou excluir dados temporários.
override func stopLoading() { }
Isso completa a implementação do protocolo de
URL
e pode ser usado em qualquer lugar do aplicativo. Para estar onde aplicar nosso protocolo, adicione mais algumas coisas.
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 é uma classe simples, descendente de
UIImageView
, uma implementação semelhante da qual você provavelmente possui em qualquer aplicativo. Aqui, simplesmente carregamos a imagem pela
URL
no método
render(url:)
e a gravamos na propriedade da
image
. A conveniência é que você pode fazer upload de absolutamente qualquer imagem, seja pelo
URL
http
/
https
ou pelo nosso
URL
personalizado.
Para executar solicitações para carregar imagens, você também precisará de um objeto do tipo
URLSession
:
let config: URLSessionConfiguration = { let c = URLSessionConfiguration.ephemeral c.protocolClasses = [ MapURLProtocol.self ] return c }() let session = URLSession( configuration: config, delegate: nil, delegateQueue: nil )
A configuração da sessão é especialmente importante aqui. No
URLSessionConfiguration
existe uma propriedade importante para nós -
protocolClasses
. Esta é uma lista dos tipos de protocolos de
URL
que uma sessão com esta configuração pode manipular. Por padrão, a sessão suporta o processamento de protocolos
http
/
https
e, se for necessário suporte personalizado, eles deverão ser especificados. Para o nosso exemplo, especifique
MapURLProtocol
.
Tudo o que falta fazer é implementar o View Controller, que exibirá instantâneos do mapa. Seu código fonte pode ser encontrado
aqui .
Aqui está o resultado:
E o cache?
Tudo parece funcionar bem - exceto por um ponto importante: quando rolamos a lista para a frente e para trás, manchas brancas aparecem na tela. Parece que os instantâneos não são armazenados em cache de forma alguma e, para cada chamada ao método
render(url:)
,
MKMapSnapshotter
dados por meio do
MKMapSnapshotter
. Isso leva tempo e, portanto, essas lacunas no carregamento. Vale a pena implementar um mecanismo de armazenamento em cache de dados para que os snapshots já criados não sejam baixados novamente. Aqui usamos o poder do
URL Loading System
, que já possui um mecanismo de cache para o
URLCache
fornecido para isso.
Considere esse processo com mais detalhes e divida o trabalho com o cache em dois estágios importantes: leitura e escrita.
Leitura
Para ler corretamente os dados em cache, o
URL Loading System
precisa de ajuda para obter respostas para várias perguntas importantes:
1. Qual URLCache usar?Obviamente, o
URLCache.shared
já foi concluído, mas a
URL Loading System
nem sempre pode usá-lo - afinal, o desenvolvedor pode querer criar e usar sua própria entidade
URLCache
. Para responder a essa pergunta, a
URLSessionConfiguration
sessão
URLSessionConfiguration
possui uma propriedade
urlCache
. É usado para ler e gravar respostas a solicitações. Nós
URLCache
algum
URLCache
para esses propósitos em nossa configuração existente.
let config: URLSessionConfiguration = { let c = URLSessionConfiguration.ephemeral c.urlCache = ImageURLCache.current c.protocolClasses = [ MapURLProtocol.self ] return c }()
2. Preciso usar dados em cache ou baixar novamente?A resposta a esta pergunta depende da solicitação
URLRequest
que estamos prestes a executar. Ao criar uma solicitação, temos a oportunidade de especificar uma política de cache no argumento
cachePolicy
, além da
URL
.
let request = URLRequest( url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 30 )
O valor padrão é
.useProtocolCachePolicy
, que também está escrito na documentação. Isso significa que, nesta versão, a tarefa de encontrar uma resposta em cache para uma solicitação e determinar sua relevância reside inteiramente na implementação do protocolo de
URL
. Mas existe uma maneira mais fácil. Se você definir o valor
.returnCacheDataElseLoad
, ao criar a próxima entidade
URLProtocol
URL Loading System
URLProtocol
parte do trabalho: ele solicitará ao
urlCache
resposta em cache à solicitação atual usando o método
cachedResponse(for:)
. Se houver dados em cache, um objeto do tipo
CachedURLResponse
será transferido imediatamente quando o
URLProtocol
inicializado e armazenado na propriedade
cachedResponse
:
override init( request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { super.init( request: request, cachedResponse: cachedResponse, client: client ) }
CachedURLResponse
é uma classe simples que contém dados (
Data
) e meta-informações para eles (
URLResponse
).
Só podemos alterar um
startLoading
o método
startLoading
e verificar o valor dessa propriedade dentro dele - e finalizar imediatamente o protocolo com esses dados:
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) } }
Record
Para encontrar dados no cache, você precisa colocá-lo lá. O
URL Loading System
também cuida deste trabalho. Tudo o que é necessário para nós é dizer a ela que queremos armazenar em cache os dados quando o protocolo for
cacheStoragePolicy
usando a
cacheStoragePolicy
política de cache
cacheStoragePolicy
. Esta é uma enumeração simples com os seguintes valores:
enum StoragePolicy { case allowed case allowedInMemoryOnly case notAllowed }
Eles significam que o armazenamento em cache é permitido na memória e no disco, apenas na memória ou é proibido. Em nosso exemplo, indicamos que o cache é permitido na memória e no disco, porque por que não?
client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
Portanto, seguindo algumas etapas simples, apoiamos a capacidade de armazenar em cache instantâneos de mapa. E agora o aplicativo funciona assim:
Como você pode ver, não há mais pontos em branco - os cartões são carregados uma vez e simplesmente reutilizados no cache.
Nem sempre é fácil
Ao implementar o protocolo de
URL
, encontramos uma série de falhas.
O primeiro foi relacionado à implementação interna da interação do
URL Loading System
com o
URLCache
ao armazenar em cache as respostas às solicitações. A documentação
declara : apesar da segurança do
URLCache
do
URLCache
, a operação dos
cachedResponse(for:)
e
storeCachedResponse(_:for:)
para leitura / gravação de respostas a solicitações pode levar a uma corrida de estados, portanto, esse ponto deve ser levado em consideração nas subclasses do
URLCache
. Esperávamos que, usando o
URLCache.shared
esse problema fosse resolvido, mas acabou errado. Para corrigir isso, usamos um cache
ImageURLCache
separado, um descendente do
URLCache
, no qual executamos os métodos especificados de forma síncrona em uma fila separada. Como um bônus agradável, podemos configurar separadamente a capacidade do cache na memória e no disco separadamente de outras entidades do
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) } }
Outro problema foi reproduzido apenas em dispositivos com iOS 9. Os métodos para iniciar e finalizar o carregamento do protocolo de
URL
podem ser executados em diferentes segmentos, o que pode levar a falhas raras, porém desagradáveis. Para resolver o problema, salvamos o encadeamento atual no método
startLoading
e, em seguida, executamos o código de conclusão do download diretamente nesse encadeamento.
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) } } }
Quando um protocolo de URL pode ser útil?
Como resultado, quase todos os usuários de nosso aplicativo iOS, de uma maneira ou de outra, encontram elementos que funcionam através do protocolo de
URL
. Além de baixar mídia da galeria, várias implementações de protocolos de
URL
nos ajudam a exibir mapas e pesquisas, além de mostrar avatares de bate-papo compostos por fotografias de seus participantes.
Como qualquer solução, o
URLProtocol
tem suas vantagens e desvantagens.
Desvantagens do URLProtocol
- Falta de digitação estrita - ao criar uma
URL
parâmetros URL
esquema e do link são especificados manualmente por meio de strings. Se você digitar um erro de digitação, o parâmetro desejado não será processado. Isso pode complicar a depuração do aplicativo e a busca de erros em sua operação. No aplicativo VKontakte, usamos URLBuilder
especiais que formam o URL
final com base nos parâmetros passados. Essa decisão não é muito bonita e contradiz um pouco o objetivo de não produzir entidades adicionais, mas ainda não há uma idéia melhor. Mas sabemos que, se você precisar criar algum tipo de URL
personalizado, com certeza haverá um URLBuilder
especial que o ajudará a não cometer erros. - Falhas não óbvias - Eu já descrevi alguns cenários que podem causar
URLProtocol
um aplicativo usando o URLProtocol
. Talvez haja outros. , , , stack trace' .
URLProtocol
- — , , , : , .
URL
— . - —
URL
- . . - — , ,
URL
-. URL
, URLSession
, URLSessionDataTask
. - —
URL
- URL
-, URL Loading System
. - * API — . , API, - ,
URL
-. , API , . URL
- http
/ https
.
URL
- — . . - , - , , , — , . , , —
URL
.
GitHub