Yang Anda butuhkan hanyalah URL

gambar

Pengguna VKontakte bertukar 10 miliar pesan setiap hari. Mereka saling mengirim foto, komik, meme, dan lampiran lainnya. Kami akan memberi tahu Anda cara kami membuat aplikasi iOS untuk mengunggah gambar menggunakan URLProtocol , dan langkah demi langkah kami akan mencari tahu bagaimana menerapkannya sendiri.

Sekitar satu setengah tahun yang lalu, pengembangan bagian pesan baru dalam aplikasi VK untuk iOS sedang berjalan lancar. Ini adalah bagian pertama yang seluruhnya ditulis dalam Swift. Itu terletak di modul terpisah vkm (Pesan VK), yang tidak tahu apa-apa tentang perangkat aplikasi utama. Ia bahkan dapat dijalankan dalam proyek terpisah - fungsionalitas dasar membaca dan mengirim pesan akan terus berfungsi. Dalam aplikasi utama, pengontrol pesan ditambahkan melalui Pengontrol Tampilan Kontainer yang sesuai untuk ditampilkan, misalnya, daftar percakapan atau pesan dalam percakapan.

Pesan adalah salah satu bagian paling populer dari aplikasi seluler VKontakte, jadi penting agar ia berfungsi seperti jam. Dalam proyek messages , kami berjuang untuk setiap baris kode. Kami selalu sangat menyukai betapa rapi pesan-pesan tersebut dibangun ke dalam aplikasi, dan kami berusaha untuk memastikan bahwa semuanya tetap sama.

Secara bertahap mengisi bagian dengan fungsi-fungsi baru, kami mendekati tugas berikut: kami harus memastikan bahwa foto yang dilampirkan ke pesan pertama kali ditampilkan dalam konsep, dan setelah mengirimnya, dalam daftar umum pesan. Kami hanya bisa menambahkan modul untuk bekerja dengan PHImageManager , tetapi kondisi tambahan membuat tugas lebih sulit.

gambar


Saat memilih foto, pengguna dapat memprosesnya: menerapkan filter, memutar, memotong, dll. Dalam aplikasi VK, fungsi tersebut diimplementasikan dalam komponen AssetService terpisah. Sekarang perlu belajar untuk bekerja dengannya dari proyek pesan.

Nah, tugasnya cukup sederhana, kami akan melakukannya. Ini kira-kira solusi rata-rata, karena ada banyak variasi. Kami mengambil protokol, membuangnya dalam pesan dan mulai mengisinya dengan metode. Kami menambah AssetService, mengadaptasi protokol dan menambahkan implementasi cache kami! untuk viskositas. Kemudian kami menempatkan implementasi dalam pesan, menambahkannya ke beberapa layanan atau manajer yang akan bekerja dengan semua ini, dan mulai menggunakannya. Pada saat yang sama, seorang pengembang baru masih datang dan, sambil mencoba mencari tahu semuanya, ia mengutuk dengan berbisik ... (yah, Anda mengerti). Pada saat bersamaan, keringat muncul di dahinya.

gambar


Keputusan ini tidak sesuai dengan keinginan kita . Entitas baru muncul yang perlu diketahui komponen pesan saat bekerja dengan gambar dari AssetService . Pengembang juga perlu melakukan pekerjaan ekstra untuk mengetahui cara kerja sistem ini. Akhirnya, ada tautan implisit tambahan ke komponen proyek utama, yang kami coba hindari sehingga bagian pesan terus berfungsi sebagai modul independen.

Saya ingin menyelesaikan masalah sehingga proyek tidak tahu sama sekali tentang jenis gambar apa yang dipilih, bagaimana cara menyimpannya, apakah itu perlu memuat dan merender khusus. Selain itu, kami sudah memiliki kemampuan untuk mengunduh gambar konvensional dari Internet, hanya saja mereka tidak diunduh melalui layanan tambahan, tetapi hanya dengan URL . Dan, pada kenyataannya, tidak ada perbedaan antara kedua jenis gambar. Hanya beberapa yang disimpan secara lokal, sementara yang lain disimpan di server.

Jadi kami datang dengan ide yang sangat sederhana: bagaimana jika aset lokal juga dapat dipelajari untuk memuat melalui URL ? Tampaknya dengan satu klik jari Thanos , itu akan menyelesaikan semua masalah kami: Anda tidak perlu tahu apa-apa tentang AssetService , menambahkan tipe data baru dan menambah entropi dengan sia-sia, belajar memuat jenis gambar baru, berhati-hati dalam menyimpan data caching. Kedengarannya seperti rencana.

Yang kita butuhkan hanyalah URL


Kami mempertimbangkan gagasan ini dan memutuskan untuk menentukan format URL yang akan kami gunakan untuk memuat aset lokal:

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

Kami akan menggunakan nilai properti localIdentifier dari localIdentifier sebagai PHObject , dan kami akan melewati parameter width dan height untuk memuat gambar dari ukuran yang diinginkan. Kami juga menambahkan beberapa parameter lagi seperti crop , filter , rotate , yang akan memungkinkan Anda untuk bekerja dengan informasi dari gambar yang diproses.

Untuk menangani URL ini URL kami akan membuat AssetURLProtocol :

 class AssetURLProtocol: URLProtocol { } 

Tugasnya adalah memuat gambar melalui AssetService dan mengembalikan kembali data yang sudah siap digunakan.

Semua ini akan memungkinkan kami untuk mendelegasikan hampir sepenuhnya pekerjaan protokol URL Loading System dan URL Loading System .

Di dalam pesan itu akan memungkinkan untuk beroperasi dengan URL paling umum, hanya dalam format yang berbeda. Ini juga akan mungkin untuk menggunakan kembali mekanisme yang ada untuk memuat gambar, sangat sederhana untuk membuat serial dalam database, dan mengimplementasikan caching data melalui URLCache standar.

Apakah itu berhasil? Jika, membaca artikel ini, Anda dapat melampirkan foto dari galeri ke pesan di aplikasi VKontakte, maka ya :)

gambar

Untuk memperjelas cara menerapkan URLProtocol Anda, saya mengusulkan untuk mempertimbangkan ini dengan sebuah contoh.

Kami menetapkan sendiri tugas: untuk mengimplementasikan aplikasi sederhana dengan daftar di mana Anda perlu menampilkan daftar foto snapshot pada koordinat yang diberikan. Untuk mengunduh snapshots, kami akan menggunakan MKMapSnapshotter standar dari MapKit , dan kami akan memuat data melalui URLProtocol kustom. Hasilnya mungkin terlihat seperti ini:

gambar

Pertama, kami menerapkan mekanisme untuk memuat data dengan URL . Untuk menampilkan snapshot peta, kita perlu mengetahui koordinat titik - garis lintang dan garis bujur ( latitude , longitude ). Tetapkan format URL khusus tempat kami ingin memuat informasi:

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

Sekarang kami menerapkan URLProtocol , yang akan memproses tautan tersebut dan menghasilkan hasil yang diinginkan. Mari kita membuat kelas MapURLProtocol , yang akan kita warisi dari kelas dasar URLProtocol . Terlepas dari namanya, URLProtocol adalah kelas abstrak. Jangan malu, di sini kami menggunakan konsep lain - URLProtocol mewakili protokol URL dengan tepat dan tidak ada hubungannya dengan ketentuan OOP. Jadi MapURLProtocol :

 class MapURLProtocol: URLProtocol { } 

Sekarang kami mendefinisikan kembali beberapa metode yang diperlukan yang tanpanya protokol URL tidak akan berfungsi:

1. canInit(with:)


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

Metode canInit(with:) diperlukan untuk menunjukkan jenis permintaan apa yang dapat ditangani oleh protokol URL kami. Untuk contoh ini, anggap protokol hanya akan memproses permintaan dengan skema map di URL . Sebelum memulai permintaan apa pun, URL Loading System memeriksa semua protokol yang terdaftar untuk sesi ini dan memanggil metode ini. Protokol terdaftar pertama, yang dalam metode ini akan mengembalikan true , akan digunakan untuk memproses permintaan.

canonicalRequest(for:)


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

Metode canonicalRequest(for:) dimaksudkan untuk mengurangi permintaan ke formulir kanonik. Dokumentasi mengatakan bahwa implementasi protokol itu sendiri memutuskan apa yang harus dipertimbangkan sebagai definisi dari konsep ini. Di sini Anda dapat menormalkan skema, menambahkan header ke permintaan, jika perlu, dll. Satu-satunya persyaratan agar metode ini berfungsi adalah bahwa untuk setiap permintaan yang masuk harus selalu ada hasil yang sama, termasuk karena metode ini juga digunakan untuk mencari jawaban yang di-cache permintaan di URLCache .

3. startLoading()


Metode startLoading() menjelaskan semua logika untuk memuat data yang diperlukan. Dalam contoh ini, Anda perlu mem-parsing URL permintaan dan, berdasarkan nilai-nilai parameter latitude dan longitude , MKMapSnapshotter dan muat snapshot peta yang diinginkan.

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

Setelah menerima data, protokol perlu dimatikan dengan benar:

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

Pertama-tama, buat objek bertipe URLResponse . Objek ini berisi metadata penting untuk menanggapi permintaan. Kemudian kami menjalankan tiga metode penting untuk objek bertipe URLProtocolClient . Properti client jenis ini berisi setiap entitas dari protokol URL . Ini bertindak sebagai proksi antara protokol URL dan seluruh URL Loading System , yang, saat memanggil metode ini, menarik kesimpulan tentang apa yang perlu dilakukan dengan data: cache, mengirim permintaan ke completionHandler , entah bagaimana proses penutupan protokol, dll. dan jumlah panggilan ke metode ini dapat bervariasi tergantung pada implementasi protokol. Misalnya, kita dapat mengunduh data dari jaringan dengan batch dan memberi tahu URLProtocolClient tentang ini secara berkala untuk menunjukkan perkembangan pemuatan data di antarmuka.

Jika terjadi kesalahan dalam pengoperasian protokol, perlu juga memproses dan memberi tahu URLProtocolClient tentang ini dengan benar:

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

Kesalahan inilah yang kemudian akan dikirim ke completionHandler permintaan, di mana ia dapat diproses dan pesan yang indah ditampilkan kepada pengguna.

4. stopLoading()


Metode stopLoading() dipanggil ketika operasi protokol selesai karena beberapa alasan. Ini bisa berupa penyelesaian yang berhasil, atau penyelesaian kesalahan atau pembatalan permintaan. Ini adalah tempat yang baik untuk membebaskan sumber daya yang diduduki atau menghapus data sementara.

 override func stopLoading() { } 

Ini melengkapi implementasi protokol URL , dapat digunakan di mana saja dalam aplikasi. Untuk menjadi tempat menerapkan protokol kami, tambahkan beberapa hal lagi.

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

Ini adalah kelas sederhana, turunan dari UIImageView , implementasi serupa yang mungkin Anda miliki di aplikasi apa pun. Di sini kita cukup memuat gambar dengan URL di metode render(url:) dan menulisnya ke properti image . Kemudahannya adalah Anda dapat mengunggah gambar apa pun, baik dengan URL http / https , atau dengan URL khusus kami.

Untuk menjalankan permintaan untuk memuat gambar, Anda juga akan memerlukan objek tipe URLSession :

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

Konfigurasi sesi sangat penting di sini. Di URLSessionConfiguration ada satu properti penting bagi kami - protocolClasses . Ini adalah daftar jenis protokol URL yang dapat ditangani oleh sesi dengan konfigurasi ini. Secara default, sesi mendukung pemrosesan protokol http / https , dan jika dukungan khusus diperlukan, mereka harus ditentukan. Sebagai contoh kami, tentukan MapURLProtocol .

Yang masih harus dilakukan adalah mengimplementasikan View Controller, yang akan menampilkan snapshot peta. Kode sumbernya dapat ditemukan di sini .

Inilah hasilnya:

gambar

Bagaimana dengan caching?


Segalanya tampak bekerja dengan baik - kecuali satu poin penting: ketika kita menggulir daftar bolak-balik, bintik-bintik putih muncul di layar. Tampaknya snapshot tidak di-cache dengan cara apa pun dan untuk setiap panggilan ke metode render(url:) , kami MKMapSnapshotter data melalui MKMapSnapshotter . Ini membutuhkan waktu, dan oleh karena itu kesenjangan dalam memuat. Perlu menerapkan mekanisme caching data sehingga snapshot yang sudah dibuat tidak diunduh lagi. Di sini kami menggunakan kekuatan URL Loading System , yang sudah memiliki mekanisme caching untuk URLCache disediakan untuk ini.

Pertimbangkan proses ini secara lebih rinci dan bagi pekerjaan dengan cache menjadi dua tahap penting: membaca dan menulis.

Membaca


Untuk membaca data yang di-cache dengan benar, URL Loading System perlu bantuan untuk mendapatkan jawaban atas beberapa pertanyaan penting:

1. URLCache apa yang digunakan?

Tentu saja, sudah ada URLCache.shared sudah selesai, tetapi URL Loading System tidak selalu dapat menggunakannya - lagipula, pengembang mungkin ingin membuat dan menggunakan entitas URLCache miliknya sendiri. Untuk menjawab pertanyaan ini, URLSessionConfiguration sesi URLSessionConfiguration memiliki properti urlCache . Ini digunakan untuk membaca dan merekam respons terhadap permintaan. Kami akan URLCache beberapa URLCache untuk keperluan ini dalam konfigurasi yang ada.

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

2. Apakah saya perlu menggunakan data cache atau mengunduh lagi?

Jawaban untuk pertanyaan ini tergantung pada permintaan URLRequest kita jalankan. Saat membuat permintaan, kami memiliki kesempatan untuk menentukan kebijakan cache dalam argumen cachePolicy selain URL .

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

Nilai defaultnya adalah .useProtocolCachePolicy , yang juga ditulis dalam dokumentasi. Ini berarti bahwa dalam versi ini, tugas untuk menemukan respons yang di-cache ke suatu permintaan dan menentukan relevansinya sepenuhnya terletak pada implementasi protokol URL . Tetapi ada cara yang lebih mudah. Jika Anda menetapkan nilai .returnCacheDataElseLoad , maka saat membuat entitas berikutnya, URLProtocol URL Loading System akan mengambil beberapa pekerjaan: ia akan meminta urlCache respons yang di-cache ke permintaan saat ini menggunakan metode cachedResponse(for:) . Jika ada data yang di-cache, maka objek tipe CachedURLResponse akan ditransfer segera ketika URLProtocol diinisialisasi dan disimpan di properti cachedResponse :

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

CachedURLResponse adalah kelas sederhana yang berisi data ( Data ) dan meta-informasi untuk mereka ( URLResponse ).

Kami hanya dapat mengubah startLoading metode startLoading dan memeriksa nilai properti ini di dalamnya - dan segera mengakhiri protokol dengan data ini:

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

Rekam


Untuk menemukan data dalam cache, Anda harus meletakkannya di sana. URL Loading System juga menangani pekerjaan ini. Semua yang diperlukan dari kami adalah untuk memberitahunya bahwa kami ingin melakukan cache data ketika protokol cacheStoragePolicy menggunakan cacheStoragePolicy kebijakan cache cacheStoragePolicy . Ini adalah penghitungan sederhana dengan nilai-nilai berikut:

 enum StoragePolicy { case allowed case allowedInMemoryOnly case notAllowed } 

Itu berarti bahwa caching diperbolehkan dalam memori dan pada disk, hanya dalam memori atau dilarang. Dalam contoh kami, kami menunjukkan bahwa caching diperbolehkan dalam memori dan pada disk, karena mengapa tidak.

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

Jadi, dengan mengikuti beberapa langkah sederhana, kami mendukung kemampuan untuk me-cache snapshot peta. Dan sekarang aplikasi berfungsi seperti ini:

gambar

Seperti yang Anda lihat, tidak ada lagi bintik putih - kartu dimuat satu kali dan kemudian digunakan kembali dari cache.

Tidak selalu mudah


Saat menerapkan protokol URL , kami mengalami serangkaian gangguan.

Yang pertama terkait dengan implementasi internal interaksi URL Loading System dengan URLCache ketika caching tanggapan terhadap permintaan. Dokumentasi menyatakan : meskipun ada keamanan URLCache , pengoperasian cachedResponse(for:) dan storeCachedResponse(_:for:) metode untuk membaca / menulis tanggapan terhadap permintaan dapat mengarah pada perlombaan status, oleh karena itu, poin ini harus diperhitungkan dalam subclass dari URLCache . Kami berharap menggunakan URLCache.shared masalah ini akan terpecahkan, tetapi ternyata salah. Untuk memperbaikinya, kami menggunakan cache ImageURLCache terpisah, turunan dari URLCache , di mana kami menjalankan metode yang ditentukan secara sinkron pada antrian terpisah. Sebagai bonus yang menyenangkan, kami dapat mengonfigurasi kapasitas cache secara terpisah dalam memori dan pada disk secara terpisah dari entitas URLCache lainnya.

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

Masalah lain hanya direproduksi pada perangkat dengan iOS 9. Metode untuk memulai dan mengakhiri pemuatan protokol URL dapat dilakukan pada utas yang berbeda, yang dapat menyebabkan crash yang jarang namun tidak menyenangkan. Untuk menyelesaikan masalah, kami menyimpan utas saat ini dalam metode startLoading dan kemudian menjalankan kode penyelesaian unduhan langsung di utas ini.

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

Kapan protokol URL berguna?


Akibatnya, hampir setiap pengguna aplikasi iOS kami dengan satu atau lain cara menemukan elemen yang bekerja melalui protokol URL . Selain mengunduh media dari galeri, berbagai implementasi protokol URL membantu kami menampilkan peta dan polling, serta menunjukkan avatar obrolan yang terdiri dari foto-foto peserta mereka.

gambar

gambar

gambar

gambar

Seperti solusi apa pun, URLProtocol memiliki kelebihan dan kekurangan.

Kerugian dari URLProtocol


  • Kurang mengetik ketat - saat membuat URL skema dan parameter tautan ditentukan secara manual melalui string. Jika Anda salah ketik, parameter yang diinginkan tidak akan diproses. Ini dapat mempersulit proses debug aplikasi dan mencari kesalahan dalam operasinya. Dalam aplikasi VKontakte, kami menggunakan URLBuilder khusus yang membentuk URL final berdasarkan parameter yang diteruskan. Keputusan ini tidak terlalu indah dan agak bertentangan dengan tujuan tidak menghasilkan entitas tambahan, tetapi belum ada ide yang lebih baik. Tetapi kami tahu bahwa jika Anda perlu membuat semacam URL khusus, maka pasti ada URLBuilder khusus untuk itu yang akan membantu Anda untuk tidak membuat kesalahan.
  • Crash tidak jelas - Saya sudah menjelaskan beberapa skenario yang dapat menyebabkan aplikasi menggunakan URLProtocol mogok. Mungkin ada yang lain. , , , 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/id467605/


All Articles