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