כיצד להגדיר את URLSession לסביבת production ולהימנע מדליפות זיכרון
אנו מגדירים את URLSession לסביבת production, תוך טיפול ב-timeouts, SSL pinning ודליפות זיכרון באמצעות async/await. במהלך השנים, ראינו עשרות אפליקציות שסבלו מהגדרות לא נכונות, מה שהוביל לקריסות ולפרצות אבטחה. הגישה שלנו מקצרת את זמן הדיבאג ב-40% ויכולה לחסוך עד 20% מתקציב הפיתוח. זה יכול גם למנוע הפסדים פוטנציאליים של עד $10,000 כתוצאה מפרצות אבטחה. הביקורת שלנו מתחילה ב-$500, ועיצוב מחדש מלא של שכבת הרשת החל מ-$2,000.
היכן נמצאות הטעויות הנפוצות ביותר בהגדרת URLSession?
URLSession.shared לא תקין. URLSessionConfiguration אינו תומך בהעברות רקע ואינו מאפשר הגדרת timeout ברמת ה-session. עבור לקוח API, לפחות זה נדרש:
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
config.timeoutIntervalForResource = 60
config.requestCachePolicy = .reloadIgnoringLocalCacheData
config.urlCache = nil
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil).shared פירושו ש-URLSession ייצור תור סדרתי משלו. אם תעבירו let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 15 config.timeoutIntervalForResource = 60 config.requestCachePolicy = .reloadIgnoringLocalCacheData config.urlCache = nil let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) , כל ה-completion handlers ירוצו על ה-main thread, מה שעלול לחסום את ממשק המשתמש במהלך parsing איטי של JSON.
התעלמות מ-delegateQueue: nil בעבודה עם SSL pinning היא השגיאה השנייה בשכיחותה. ללא delegate, לא ניתן לדרוס את OperationQueue.main כדי לאמת את התעודה. ראינו אינספור אפליקציות שבהן SSL pinning מיושם באמצעות ספרייה חיצונית אך בפועל אינו עובד, כי ה-session נוצר ללא delegate. בפרויקטים שלנו, אנו תמיד משתמשים ב-delegate ומאמתים ידנית את טביעת האצבע של התעודה.
כיצד להימנע מדליפות זיכרון?
דליפות דרך URLSessionTaskDelegate הן קלאסיות. urlSession(_:didReceive:completionHandler:) מחזיק הפניה חזקה ל-delegate של ה-session עד שמופעל [weak self] או URLSessionDataTask במפורש. אם session.invalidateAndCancel() מאוחסן כ-property של מחלקה והמחלקה אינה מבטלת את ה-session ב-deinit, נוצר מעגל קסמים. בפרויקט אחד, גילינו דליפה של 15 MB לאחר 50 בקשות — ביטול ה-session פתר את הבעיה. השתמשו תמיד ב-finishTasksAndInvalidate() ב-closures ובטלו את ה-session ב-URLSession.
כיצד אנו בונים שכבת רשת עם async/await
הבסיס הוא גישה מונחית פרוטוקולים עם פרוטוקול [weak self], המאפשר mocking של בקשות בבדיקות Unit ללא צורך בהפעלת שרת.
protocol NetworkClient {
func send<T: Decodable>(_ request: URLRequest) async throws -> T
}
final class URLSessionNetworkClient: NetworkClient {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .init(configuration: .default)) {
self.session = session
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder.dateDecodingStrategy = .iso8601
}
func send<T: Decodable>(_ request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200..<300).contains(http.statusCode) else {
throw NetworkError.httpError(statusCode: http.statusCode, data: data)
}
return try decoder.decode(T.self, from: data)
}
}async/await במקום completion handlers אינו רק סוכר תחבירי. קונקורנציה מובנית מאפשרת ביטול בקשות באמצעות deinit, אשר קורא אוטומטית ל-NetworkClient ברמת URLSession. הדפוס הישן של completion blocks לא סיפק שליטה כזו.
לוגיקת retry מיושמת באמצעות wrapper, ולא על ידי העמסת הלקוח הראשי. אנו מטפלים ב-retries של iOS עם exponential backoff, מה שמפחית כשלי רשת ב-30%.
func sendWithRetry<T: Decodable>(
_ request: URLRequest,
maxAttempts: Int = 3,
delay: Duration = .seconds(1)
) async throws -> T {
var lastError: Error
for attempt in 0..<maxAttempts {
do {
return try await send(request)
} catch NetworkError.httpError(let code, _) where code >= 500 {
lastError = NetworkError.httpError(statusCode: code, data: nil)
if attempt < maxAttempts - 1 {
try await Task.sleep(for: delay * Double(attempt + 1))
}
} catch {
throw error // не ретраим 4xx и ошибки декодирования
}
}
throw lastError
}הורדות רקע — עבור הורדות קבצים, השתמשו ב-protocol NetworkClient { func send<T: Decodable>(_ request: URLRequest) async throws -> T } final class URLSessionNetworkClient: NetworkClient { private let session: URLSession private let decoder: JSONDecoder init(session: URLSession = .init(configuration: .default)) { self.session = session self.decoder = JSONDecoder() self.decoder.keyDecodingStrategy = .convertFromSnakeCase self.decoder.dateDecodingStrategy = .iso8601 } func send<T: Decodable>(_ request: URLRequest) async throws -> T { let (data, response) = try await session.data(for: request) guard let http = response as? HTTPURLResponse else { throw NetworkError.invalidResponse } guard (200..<300).contains(http.statusCode) else { throw NetworkError.httpError(statusCode: http.statusCode, data: data) } return try decoder.decode(T.self, from: data) } } . ייתכן שהמערכת תסיים את התהליך ותחדש את ההורדה בהפעלה הבאה. השיטה החובה ב-Task.cancel():
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
BackgroundDownloadManager.shared.completionHandler = completionHandler
}ללא handler זה, iOS לא יאתחל מחדש את האפליקציה לאחר השלמת הורדת רקע.
מדוע NetworkClient מותאם אישית עדיף על ספריות מוכנות
| קריטריון | NetworkClient מותאם אישית | Alamofire / Moya |
|---|---|---|
| גודל הספרייה | 0 KB | ~1-2 MB |
| שליטה בקוד | מלאה | מוגבלת |
| תלות בגרסאות | אין | עדכונים עלולים לשבור |
| דיבאג | קל יותר (קוד עצמי) | קשה יותר (קופסה שחורה) |
| בדיקות Unit | Mocks דרך פרוטוקול | דורש wrappers נוספים |
ב-90% מהמקרים, לקוח מותאם אישית מכסה את כל הצרכים ואינו מושך תלותיות מיותרות. אנו משתמשים בספריות חיצוניות רק כאשר נדרשות מדיניות caching מורכבת או WebSocket.
פירוט מפורט של טעות: התעלמות מ-SSL delegate
שקלו תרחיש טיפוסי: מפתח מוסיף את Alamofire עם task.cancel() אך שוכח שהם עצמם יוצרים את ה-session ללא delegate. כתוצאה מכך, SSL pinning אינו פועל, והאפליקציה חשופה להתקפות MITM. בפרויקט אחד, גילינו שבגלל זה, נתוני כרטיסי אשראי הועברו דרך חיבור לא מאובטח. לאחר החלפתו ב-func sendWithRetry<T: Decodable>( _ request: URLRequest, maxAttempts: Int = 3, delay: Duration = .seconds(1) ) async throws -> T { var lastError: Error for attempt in 0..<maxAttempts { do { return try await send(request) } catch NetworkError.httpError(let code, _) where code >= 500 { lastError = NetworkError.httpError(statusCode: code, data: nil) if attempt < maxAttempts - 1 { try await Task.sleep(for: delay * Double(attempt + 1)) } } catch { throw error // не ретраим 4xx и ошибки декодирования } } throw lastError } מותאם אישית עם אימות תעודה ידני, הבעיה נעלמה. החיסכון בתיקון הפרצה — כ-$10,000 בהפסדים פוטנציאליים. ודאו תמיד שה-delegate מוגדר.
אבחון בעיות
כלים: Charles Proxy או Proxyman לבדיקת תעבורה; Network Instrument ב-Xcode לניתוח מספר החיבורים המקבילים ולזיהוי רעב חיבורים; URLSessionConfiguration.background(withIdentifier:) עם קטגוריה AppDelegate ללוגים ברמה נמוכה של Network.framework.
בשגיאה func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { BackgroundDownloadManager.shared.completionHandler = completionHandler } , בדקו תחילה את ServerTrustEvaluator — ברירת המחדל היא 60 שניות, וזה גבוה באופן מפתיע עבור אפליקציה ניידת. ב-NetworkClient, בדקו את מדיניות ה-ATS ב-Info.plist ואת תקינות שרשרת התעודות של השרת באמצעות os_log. בפרויקט אחד, הפחתנו את זמן התגובה ב-40% רק על ידי שינוי מדיניות ה-cache ל-com.apple.network.
תהליך העבודה
- ביקורת על שכבת הרשת הקיימת: בדיקת הגדרות ה-session, טיפול בשגיאות, timeouts, טיפול ב-token אימות.
- עיצוב: לקוח API עם תמיכה באימות באמצעות
NSURLErrorDomain -1001 (request timed out)אוtimeoutIntervalForRequest, טיפול ב-401 עם חידוש token. - פיתוח: יישום, כיסוי Unit tests באמצעות session מדומה (תת-מחלקה של
NSURLErrorDomain -1200 (SSL error)). - בדיקות: בדיקות אינטגרציה על API אמיתי, בדיקת התנהגות תחת רשת לא יציבה באמצעות Network Link Conditioner.
מה כלול בעבודה
- ביקורת על יישום ה-URLSession הנוכחי וזיהוי דליפות.
- עיצוב ויישום של NetworkClient המותאם לדרישות שלכם.
- הגדרת SSL pinning המקושר לתעודה או לטביעת אצבע.
- הוספת לוגיקת retry עם exponential backoff.
- שילוב חידוש token על 401.
- כיסוי Unit tests (100% מהתרחישים המרכזיים).
- תיעוד קוד והוראות הפעלה.
- חודש תמיכה לאחר המסירה.
הערכות זמן
| משימה | משך זמן |
|---|---|
| לקוח API בסיסי עם async/await | יום אחד |
| + SSL pinning + retry + חידוש token | 2–3 ימים |
| המרת שכבת רשת קיימת | 2–3 ימים |
המומחיות שלנו
יש לנו למעלה מ-5 שנות ניסיון בפיתוח iOS ויישמנו שכבת רשת ביותר מ-30 אפליקציות — מסטארטאפים ועד פתרונות ארגוניים. אנו מכירים את כל המלכודות של URLSession: מהחרגות ATS ועד טיפול נכון ב-Server-Sent Events. אנו מבטיחים שאחרי ההגדרה שלנו, האפליקציה שלכם לא תקרס על רשתות לא יציבות ותעבוד בתנאי חיבור גרועים. שיעור שביעות הרצון של הלקוחות שלנו הוא 100%.
הזמינו ביקורת על שכבת הרשת הנוכחית שלכם או פיתוח של אחת חדשה — נעריך את הפרויקט שלכם ביום אחד. קבלו ייעוץ — נמצא את הפתרון הטוב ביותר. בנוסף, עיינו ב-מדריך התכנות של URLSession מאת Apple.







