Tudo que você precisa é de URL

imagem

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.

imagem


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.

imagem


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

imagem

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:

imagem

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:

imagem

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:

imagem

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.

imagem

imagem

imagem

imagem

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

Source: https://habr.com/ru/post/pt467605/


All Articles