Tout ce dont vous avez besoin est une URL

image

Les utilisateurs de VKontakte échangent quotidiennement 10 milliards de messages. Ils s'envoient des photos, des bandes dessinées, des mèmes et d'autres pièces jointes. Nous allons vous expliquer comment nous avons créé une application iOS pour télécharger des images à l'aide de URLProtocol , et étape par étape, nous découvrirons comment implémenter la nôtre.

Il y a environ un an et demi, le développement d'une nouvelle section de messages dans l'application VK pour iOS battait son plein. Ceci est la première section entièrement écrite en Swift. Il est situé dans un module séparé vkm (VK Messages), qui ne sait rien du dispositif de l'application principale. Il peut même être exécuté dans un projet distinct - la fonctionnalité de base de lecture et d'envoi de messages continuera de fonctionner. Dans l'application principale, des contrôleurs de messages sont ajoutés via le conteneur Container View Controller correspondant pour afficher, par exemple, une liste de conversations ou de messages dans une conversation.

Messages est l'une des sections les plus populaires de l'application mobile VKontakte, il est donc important qu'elle fonctionne comme une horloge. Dans le projet de messages , nous nous battons pour chaque ligne de code. Nous avons toujours vraiment aimé la façon dont les messages sont intégrés à l'application, et nous nous efforçons de veiller à ce que tout reste le même.

En remplissant progressivement la section de nouvelles fonctions, nous avons abordé la tâche suivante: nous devions nous assurer que la photo jointe au message était d'abord affichée dans un brouillon, et après l'avoir envoyée, dans la liste générale des messages. Nous pourrions simplement ajouter un module pour travailler avec PHImageManager , mais des conditions supplémentaires ont rendu la tâche plus difficile.

image


Lors du choix d'un instantané, l'utilisateur peut le traiter: appliquer un filtre, faire pivoter, rogner, etc. Dans l'application VK, cette fonctionnalité est implémentée dans un composant AssetService distinct. Maintenant, il fallait apprendre à travailler avec lui à partir du projet de message.

Eh bien, la tâche est assez simple, nous allons le faire. C'est approximativement la solution moyenne, car il y a beaucoup de variations. Nous prenons le protocole, le vidons dans les messages et commençons à le remplir avec des méthodes. Nous ajoutons à AssetService, adaptons le protocole et ajoutons notre implémentation de cache! pour la viscosité. Ensuite, nous mettons l'implémentation dans des messages, l'ajoutons à un service ou un gestionnaire qui fonctionnera avec tout cela, et commencerons à l'utiliser. Dans le même temps, un nouveau développeur vient toujours et, tout en essayant de tout comprendre, il condamne à voix basse ... (enfin, vous comprenez). En même temps, de la sueur apparaît sur son front.

image


Cette décision n'était pas à notre goût . De nouvelles entités apparaissent que les composants de message doivent connaître lorsqu'ils travaillent avec des images d' AssetService . Le développeur doit également faire un travail supplémentaire pour comprendre comment ce système fonctionne. Enfin, il y avait un lien implicite supplémentaire vers les composants du projet principal, que nous essayons d'éviter afin que la section message continue de fonctionner comme un module indépendant.

Je voulais résoudre le problème afin que le projet ne sache pas du tout quel type d'image a été choisi, comment la stocker, si elle avait besoin d'un chargement et d'un rendu spéciaux. De plus, nous avons déjà la possibilité de télécharger des images conventionnelles à partir d'Internet, seulement elles ne sont pas téléchargées via un service supplémentaire, mais simplement par URL . Et, en fait, il n'y a pas de différence entre les deux types d'images. Seuls certains sont stockés localement, tandis que d'autres sont stockés sur le serveur.

Nous avons donc eu une idée très simple: que faire si les ressources locales peuvent également être apprises à charger via URL ? Il semble qu'avec un seul clic des doigts de Thanos , cela résoudrait tous nos problèmes: vous n'avez rien à savoir sur AssetService , ajouter de nouveaux types de données et augmenter l'entropie en vain, apprendre à charger un nouveau type d'image, prendre soin de la mise en cache des données. Cela ressemble à un plan.

Tout ce dont nous avons besoin est une URL


Nous avons examiné cette idée et décidé de définir le format d' URL que nous utiliserons pour charger les ressources locales:

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

Nous utiliserons la valeur de la propriété localIdentifier de localIdentifier comme PHObject , et nous passerons les paramètres width et height pour charger les images de la taille souhaitée. Nous ajoutons également quelques paramètres supplémentaires comme crop , filter , rotate , ce qui vous permettra de travailler avec les informations de l'image traitée.

Pour gérer ces URL nous allons créer un AssetURLProtocol :

 class AssetURLProtocol: URLProtocol { } 

Sa tâche consiste à charger l'image via AssetService et à renvoyer les données déjà prêtes à l'emploi.

Tout cela nous permettra de déléguer presque complètement le travail du protocole URL Loading System et du URL Loading System .

À l'intérieur des messages, il sera possible de fonctionner avec les URL les plus courantes, uniquement dans un format différent. Il sera également possible de réutiliser le mécanisme existant de chargement des images, il est très simple de sérialiser dans la base de données et d'implémenter la mise en cache des données via URLCache standard.

Cela at-il fonctionné? Si, en lisant cet article, vous pouvez joindre une photo de la galerie au message sur l'application VKontakte, alors oui :)

image

Pour clarifier comment implémenter votre URLProtocol , je propose de considérer cela avec un exemple.

Nous nous sommes fixé la tâche: mettre en œuvre une application simple avec une liste dans laquelle vous devez afficher une liste d'instantanés de carte aux coordonnées données. Pour télécharger des instantanés, nous utiliserons le MKMapSnapshotter standard de MapKit et nous chargerons les données via le URLProtocol personnalisé. Le résultat pourrait ressembler à ceci:

image

Tout d'abord, nous implémentons le mécanisme de chargement des données par URL . Pour afficher l'instantané de la carte, nous devons connaître les coordonnées du point - sa latitude et sa longitude ( latitude , longitude ). Définissez le format d' URL personnalisé par lequel nous voulons charger les informations:

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

Maintenant, nous implémentons URLProtocol , qui traitera ces liens et générera le résultat souhaité. Créons la classe MapURLProtocol , que nous hériterons de la classe de base URLProtocol . Malgré son nom, URLProtocol est cependant une classe abstraite. Ne soyez pas gêné, nous utilisons ici d'autres concepts - URLProtocol représente exactement le protocole URL et n'a aucun rapport avec les termes de POO. Donc MapURLProtocol :

 class MapURLProtocol: URLProtocol { } 

Maintenant, nous redéfinissons certaines méthodes requises sans lesquelles le protocole URL ne fonctionnera pas:

1. canInit(with:)


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

La canInit(with:) est nécessaire pour indiquer les types de requêtes que notre protocole URL peut gérer. Pour cet exemple, supposons que le protocole traite uniquement les demandes avec un schéma de map dans l' URL . Avant de lancer une requête, l' URL Loading System passe par tous les protocoles enregistrés pour la session et appelle cette méthode. Le premier protocole enregistré, qui dans cette méthode retournera true , sera utilisé pour traiter la demande.

canonicalRequest(for:)


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

La méthode canonicalRequest(for:) est destinée à réduire la demande au format canonique. La documentation indique que la mise en œuvre du protocole lui-même décide ce qu'il faut considérer comme la définition de ce concept. Ici, vous pouvez normaliser le schéma, ajouter des en-têtes à la demande, si nécessaire, etc. La seule condition pour que cette méthode fonctionne est que pour chaque demande entrante, il devrait toujours y avoir le même résultat, y compris parce que cette méthode est également utilisée pour rechercher des réponses mises en cache. demandes dans URLCache .

3. startLoading()


La méthode startLoading() décrit toute la logique de chargement des données nécessaires. Dans cet exemple, vous devez analyser l' URL la demande et, en fonction des valeurs de ses paramètres de latitude et de longitude , vous tourner vers MKMapSnapshotter et charger l'instantané de carte souhaité.

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

Après avoir reçu les données, il est nécessaire d'arrêter correctement le protocole:

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

Tout d'abord, créez un objet de type URLResponse . Cet objet contient des métadonnées importantes pour répondre à une demande. Ensuite, nous exécutons trois méthodes importantes pour un objet de type URLProtocolClient . La propriété client de ce type contient chaque entité du protocole URL . Il agit comme un proxy entre le protocole URL et l' URL Loading System entière URL Loading System , qui, lorsque ces méthodes sont appelées, tire des conclusions sur ce qui doit être fait avec les données: cache, transmet les demandes à completionHandler , traite en quelque sorte l'arrêt du protocole, etc. et le nombre d'appels à ces méthodes peut varier en fonction de la mise en œuvre du protocole. Par exemple, nous pouvons télécharger des données à partir du réseau avec des lots et en informer périodiquement URLProtocolClient à ce sujet pour montrer la progression du chargement des données dans l'interface.

Si une erreur se produit dans le fonctionnement du protocole, il est également nécessaire de traiter correctement et d'informer URLProtocolClient à ce sujet:

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

C'est cette erreur qui sera ensuite envoyée à l' completionHandler la demande, où elle peut être traitée et un beau message affiché à l'utilisateur.

4. stopLoading()


La méthode stopLoading() est appelée lorsque l'opération de protocole a été terminée pour une raison quelconque. Cela peut être une réussite, une erreur ou une demande d'annulation. C'est un bon endroit pour libérer des ressources occupées ou supprimer des données temporaires.

 override func stopLoading() { } 

Ceci termine la mise en œuvre du protocole URL ; il peut être utilisé n'importe où dans l'application. Pour savoir où appliquer notre protocole, ajoutez quelques éléments supplémentaires.

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

Il s'agit d'une classe simple, le descendant de UIImageView , une implémentation similaire dont vous avez probablement dans n'importe quelle application. Ici, nous chargeons simplement l'image par l' URL dans la méthode render(url:) et l'écrivons dans la propriété image . La commodité est que vous pouvez télécharger absolument n'importe quelle image, soit par URL http / https , soit par notre URL personnalisée.

Pour exécuter les demandes de chargement d'images, vous aurez également besoin d'un objet de type URLSession :

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

La configuration de la session est particulièrement importante ici. Dans URLSessionConfiguration il existe une propriété importante pour nous - protocolClasses . Il s'agit d'une liste des types de protocoles URL qu'une session avec cette configuration peut gérer. Par défaut, la session prend en charge le traitement des protocoles http / https et si une prise en charge personnalisée est requise, ils doivent être spécifiés. Pour notre exemple, spécifiez MapURLProtocol .

Il ne reste plus qu'à implémenter View Controller, qui affichera des instantanés de carte. Son code source se trouve ici .

Voici le résultat:

image

Et la mise en cache?


Tout semble bien fonctionner - sauf un point important: lorsque nous faisons défiler la liste d'avant en arrière, des points blancs apparaissent à l'écran. Il semble que les instantanés ne soient en aucun cas mis en cache et pour chaque appel à la méthode render(url:) , nous MKMapSnapshotter données via MKMapSnapshotter . Cela prend du temps, et donc de tels écarts de chargement. Il convient de mettre en œuvre un mécanisme de mise en cache des données afin que les instantanés déjà créés ne soient pas téléchargés à nouveau. Ici, nous utilisons la puissance du URL Loading System , qui dispose déjà d'un mécanisme de mise en cache pour URLCache .

Considérez ce processus plus en détail et divisez le travail avec le cache en deux étapes importantes: la lecture et l'écriture.

La lecture


Pour lire correctement les données mises en cache, l' URL Loading System besoin d'aide pour obtenir des réponses à plusieurs questions importantes:

1. Quel URLCache utiliser?

Bien sûr, URLCache.shared est déjà terminé, mais l' URL Loading System ne peut pas toujours l'utiliser - après tout, le développeur peut vouloir créer et utiliser sa propre entité URLCache . Pour répondre à cette question, la URLSessionConfiguration session URLSessionConfiguration a une propriété urlCache . Il est utilisé pour lire et enregistrer les réponses aux demandes. Nous URLCache un URLCache à ces fins dans notre configuration existante.

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

2. Dois-je utiliser des données en cache ou les télécharger à nouveau?

La réponse à cette question dépend de la requête URLRequest nous sommes sur le point d'exécuter. Lors de la création d'une demande, nous avons la possibilité de spécifier une politique de cache dans l'argument cachePolicy en plus de l' URL .

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

La valeur par défaut est .useProtocolCachePolicy , qui est également écrite dans la documentation. Cela signifie que dans cette version, la tâche de trouver une réponse mise en cache à une demande et de déterminer sa pertinence incombe entièrement à la mise en œuvre du protocole URL . Mais il existe un moyen plus simple. Si vous définissez la valeur .returnCacheDataElseLoad , lors de la création de la prochaine entité URLProtocol URL Loading System cela prendra une partie du travail: il demandera à urlCache réponse mise en cache à la demande actuelle à l'aide de la cachedResponse(for:) . S'il y a des données en cache, un objet de type CachedURLResponse sera transféré immédiatement lorsque le URLProtocol initialisé et stocké dans la propriété cachedResponse :

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

CachedURLResponse est une classe simple qui contient des données ( Data ) et des méta-informations pour eux ( URLResponse ).

Nous pouvons seulement changer un startLoading méthode startLoading et vérifier la valeur de cette propriété à l'intérieur - et terminer immédiatement le protocole avec ces données:

 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


Pour trouver des données dans le cache, vous devez les y mettre. Le URL Loading System s'occupe également de ce travail. Tout ce qui nous est demandé est de lui dire que nous voulons mettre en cache les données lorsque le protocole cacheStoragePolicy aide du cacheStoragePolicy stratégie de cache cacheStoragePolicy . Il s'agit d'une énumération simple avec les valeurs suivantes:

 enum StoragePolicy { case allowed case allowedInMemoryOnly case notAllowed } 

Ils signifient que la mise en cache est autorisée en mémoire et sur disque, uniquement en mémoire ou est interdite. Dans notre exemple, nous indiquons que la mise en cache est autorisée en mémoire et sur disque, car pourquoi pas.

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

Ainsi, en suivant quelques étapes simples, nous avons pris en charge la possibilité de mettre en cache les instantanés de carte. Et maintenant, l'application fonctionne comme ceci:

image

Comme vous pouvez le voir, il n'y a plus de points blancs - les cartes sont chargées une fois puis simplement réutilisées à partir du cache.

Pas toujours facile


Lors de la mise en œuvre du protocole URL , nous avons rencontré une série de plantages.

Le premier était lié à l'implémentation interne de l'interaction du URL Loading System avec URLCache lors de la mise en cache des réponses aux demandes. La documentation indique : malgré la sécurité des URLCache d' URLCache , le fonctionnement des cachedResponse(for:) et storeCachedResponse(_:for:) pour lire / écrire les réponses aux requêtes peut conduire à une race d'états, donc ce point doit être pris en compte dans les sous-classes URLCache . Nous nous attendions à ce que l'utilisation d' URLCache.shared ce problème soit résolu, mais il s'est avéré être faux. Pour résoudre ce problème, nous utilisons un cache ImageURLCache distinct, un descendant d' URLCache , dans lequel nous URLCache les méthodes spécifiées de manière synchrone sur une file d'attente distincte. Comme bonus agréable, nous pouvons configurer séparément la capacité du cache en mémoire et sur le disque séparément des autres entités 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) } } 

Un autre problème a été reproduit uniquement sur les appareils avec iOS 9. Les méthodes pour démarrer et terminer le chargement du protocole URL peuvent être effectuées sur différents threads, ce qui peut entraîner des plantages rares mais désagréables. Pour résoudre le problème, nous enregistrons le thread actuel dans la méthode startLoading , puis startLoading le code de fin de téléchargement directement sur ce thread.

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

Quand un protocole URL peut-il être utile?


En conséquence, presque tous les utilisateurs de notre application iOS rencontrent d'une manière ou d'une autre des éléments qui fonctionnent via le protocole URL . En plus de télécharger des médias à partir de la galerie, diverses implémentations de protocoles URL nous aident à afficher des cartes et des sondages, ainsi qu'à afficher des avatars de chat composés de photos de leurs participants.

image

image

image

image

Comme toute solution, URLProtocol a ses avantages et ses inconvénients.

Inconvénients de URLProtocol


  • Manque de frappe stricte - lors de la création d'une URL paramètres de schéma et de lien sont spécifiés manuellement via des chaînes. Si vous faites une faute de frappe, le paramètre souhaité ne sera pas traité. Cela peut compliquer le débogage de l'application et la recherche d'erreurs dans son fonctionnement. Dans l'application VKontakte, nous utilisons des URLBuilder spéciaux qui forment l' URL finale en fonction des paramètres transmis. Cette décision n'est pas très belle et contredit quelque peu l'objectif de ne pas produire d'entités supplémentaires, mais il n'y a pas encore de meilleure idée. Mais nous savons que si vous avez besoin de créer une sorte d' URL personnalisée, il existe à coup sûr un URLBuilder spécial qui vous aidera à ne pas faire d'erreur.
  • URLProtocol non évidents - J'ai déjà décrit quelques scénarios qui pourraient provoquer le URLProtocol une application utilisant URLProtocol . Il y en a peut-être d'autres. Mais ces problèmes, comme d'habitude, sont résolus soit par une lecture plus réfléchie de la documentation, soit en étudiant en profondeur la trace de la pile et en trouvant la racine du problème.

Avantages d'URLProtocol


  • Faible connectivité des composants - la partie de l'application qui lance le chargement des données dont elle a besoin peut ne pas savoir du tout comment elle est organisée: quels composants sont utilisés pour cela, comment la mise en cache est organisée. Nous ne connaissons qu'un certain format URL- et n'interagissons qu'avec lui.
  • Simplicité de mise en œuvre - pour le bon fonctionnement du URLprotocole, il suffit de mettre en œuvre plusieurs méthodes simples et d'enregistrer le protocole. Après cela, il peut être utilisé n'importe où dans l'application.
  • — , , 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/fr467605/


All Articles