# Instruments ile iOS Performance Profiling: Tam Rehber
Kullanıcılar uygulamanızın yavaş olduğunu hissettiklerinde, App Store'dan silip gidiyorlar. Performance sadece teknik bir metrik değil — kullanıcı deneyiminin temelidir. Bu rehberde Xcode Instruments'ın tüm gücünü keşfedecek ve production-ready profiling becerilerini kazanacaksınız.
İçindekiler
- Instruments'a Giriş
- Time Profiler
- Allocations
- Leaks Instrument
- Network Profiling
- Energy Log
- Core Animation
- System Trace
- Custom Instruments
- CI/CD Entegrasyonu
- Production Monitoring
- Checklist & Best Practices
1. Instruments'a Giriş
Instruments, Apple'ın en güçlü profiling aracıdır. Xcode → Product → Profile (⌘I) ile başlatılır.
Temel Kavramlar
Kavram | Açıklama | Ne Zaman Kullanılır |
|---|---|---|
**Time Profiler** | CPU kullanım analizi | UI donması, yavaş hesaplama |
**Allocations** | Bellek tahsis takibi | Yüksek bellek kullanımı |
**Leaks** | Bellek sızıntısı tespiti | Retain cycle şüphesi |
**Network** | HTTP trafik analizi | Yavaş API, fazla istek |
**Energy Log** | Pil tüketimi | Background drain |
**System Trace** | Thread ve dispatch analizi | Deadlock, priority inversion |
**Core Animation** | FPS ve render analizi | Scroll kasması, animasyon |
İlk Profiling Oturumu
swift
1// ÖNEMLİ: Release konfigürasyonunda profile edin!2// Debug mode optimizer kapalıdır, yanıltıcı sonuçlar verir.3 4// Scheme ayarı:5// Product → Scheme → Edit Scheme → Profile → Build Configuration: Release6 7// Signpost ile özel bölümleri işaretleyin8import os9 10let logger = Logger(subsystem: "com.app.performance", category: "profiling")11let signposter = OSSignposter(logger: logger)12 13func loadData() {14 let signpostID = signposter.makeSignpostID()15 let state = signposter.beginInterval("DataLoad", id: signpostID)16 17 // Veri yükleme işlemi18 fetchFromNetwork { data in19 signposter.endInterval("DataLoad", state)20 }21}Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
2. Time Profiler Mastery
Time Profiler, CPU'nun nerede vakit geçirdiğini gösterir. Ağır hesaplamalar, gereksiz main thread işleri ve bottleneck'ler burada yakalanır.
Temel Kullanım
swift
1// KÖTÜ: Main thread'de ağır iş2func processImages(_ images: [UIImage]) -> [UIImage] {3 return images.map { image in4 // Bu satır Time Profiler'da kırmızı yanar5 applyFilter(to: image)6 }7}8 9// İYİ: Background thread + signpost10func processImagesOptimized(_ images: [UIImage]) async -> [UIImage] {11 let signposter = OSSignposter()12 let state = signposter.beginInterval("ImageProcessing")13 14 let result = await withTaskGroup(of: (Int, UIImage).self) { group in15 for (index, image) in images.enumerated() {16 group.addTask {17 let processed = await self.applyFilterAsync(to: image)18 return (index, processed)19 }20 }21 22 var results = [(Int, UIImage)]()23 for await result in group {24 results.append(result)25 }26 return results.sorted { $0.0 < $1.0 }.map { $0.1 }27 }28 29 signposter.endInterval("ImageProcessing", state)30 return result31}Call Tree Okuma Stratejisi
Strateji | Ayar | Amaç |
|---|---|---|
**Separate by Thread** | ✅ | Thread bazında analiz |
**Invert Call Tree** | ✅ | En pahalı fonksiyonu üstte göster |
**Hide System Libraries** | ✅ | Sadece kendi kodunu gör |
**Flatten Recursion** | ✅ | Recursive call'ları birleştir |
**Top Functions** | ✅ | Self + children toplam |
Hang Detection
swift
1// iOS 16+ otomatik hang detection2// Instruments → App Hang template3 4// Manual hang detection5final class HangDetector {6 private let watchdogQueue = DispatchQueue(label: "hangDetector")7 private var lastPing = Date()8 private let threshold: TimeInterval = 0.5 // 500ms9 10 func start() {11 schedulePing()12 scheduleCheck()13 }14 15 private func schedulePing() {16 DispatchQueue.main.async { [weak self] in17 self?.lastPing = Date()18 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {19 self?.schedulePing()20 }21 }22 }23 24 private func scheduleCheck() {25 watchdogQueue.asyncAfter(deadline: .now() + 0.2) { [weak self] in26 guard let self else { return }27 let interval = Date().timeIntervalSince(self.lastPing)28 if interval > self.threshold {29 self.reportHang(duration: interval)30 }31 self.scheduleCheck()32 }33 }34 35 private func reportHang(duration: TimeInterval) {36 os_log(.fault, "Main thread hang: %.2f seconds", duration)37 }38}3. Allocations Deep Dive
Memory allocation pattern'leri anlamak, OOM crash'lerini önlemenin anahtarıdır.
Generation Analysis
swift
1// Allocations → Mark Generation ile bellek artışını izleyin2 3// Her ekran geçişinde generation mark koyun:4// 1. Ana ekran → Mark Generation A5// 2. Detay ekrana git → Mark Generation B6// 3. Geri dön → Mark Generation C7// Generation C'de Growth varsa → bellek sızıntısı!8 9// Yaygın sızıntı: Closure capture10class DetailViewController: UIViewController {11 var onDismiss: (() -> Void)?12 private var timer: Timer?13 14 override func viewDidLoad() {15 super.viewDidLoad()16 17 // KÖTÜ: self'i strong capture ediyor18 // timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in19 // self.updateUI()20 // }21 22 // İYİ: weak capture23 timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in24 self?.updateUI()25 }26 }27 28 deinit {29 timer?.invalidate()30 print("DetailVC deallocated ✅")31 }32 33 private func updateUI() {34 // UI güncelle35 }36}Transient vs Persistent Allocations
Tip | Açıklama | Sorun Belirtisi |
|---|---|---|
**Persistent** | Hâlâ bellekte | Growth > 0 = potansiyel leak |
**Transient** | Oluşturulup serbest bırakılmış | Çok fazla = allocation churn |
**Dirty Memory** | Değiştirilmiş sayfa | Sistem bunu sıkıştıramaz |
**Compressed** | Sıkıştırılmış bellek | Erişimde decompress gerekir |
Autorelease Pool Optimizasyonu
swift
1// Döngüde çok nesne oluşturuyorsanız2func processLargeDataset(_ items: [Data]) -> [ProcessedItem] {3 var results: [ProcessedItem] = []4 results.reserveCapacity(items.count) // Önceden kapasite ayır5 6 for chunk in items.chunked(into: 100) {7 autoreleasepool {8 let processed = chunk.map { ProcessedItem(data: $0) }9 results.append(contentsOf: processed)10 }11 // Her 100 öğeden sonra autorelease pool temizlenir12 }13 return results14}15 16extension Array {17 func chunked(into size: Int) -> [[Element]] {18 stride(from: 0, to: count, by: size).map {19 Array(self[$0..<Swift.min($0 + size, count)])20 }21 }22}4. Leaks Instrument
Retain cycle'lar en yaygın bellek sızıntısı nedenidir.
Retain Cycle Tespit Kalıpları
swift
1// Pattern 1: Delegate retain cycle2protocol DataManagerDelegate: AnyObject { // AnyObject zorunlu!3 func didUpdateData()4}5 6class DataManager {7 weak var delegate: DataManagerDelegate? // WEAK zorunlu!8}9 10// Pattern 2: Closure retain cycle11class NetworkService {12 var completionHandler: ((Result<Data, Error>) -> Void)?13 14 func fetch(url: URL) {15 URLSession.shared.dataTask(with: url) { [weak self] data, _, error in16 guard let self else { return }17 if let data {18 self.completionHandler?(.success(data))19 }20 // İşlem bitti, closure'ı temizle21 self.completionHandler = nil22 }.resume()23 }24}25 26// Pattern 3: NotificationCenter (iOS 9+ otomatik temizler ama explicit daha güvenli)27class ObserverVC: UIViewController {28 private var observers: [NSObjectProtocol] = []29 30 override func viewDidLoad() {31 super.viewDidLoad()32 let obs = NotificationCenter.default.addObserver(33 forName: .dataDidChange, object: nil, queue: .main34 ) { [weak self] notification in35 self?.handleDataChange(notification)36 }37 observers.append(obs)38 }39 40 deinit {41 observers.forEach { NotificationCenter.default.removeObserver($0) }42 }43}5. Network Profiling
swift
1// URLSession metrikleri toplama2class NetworkMetricsCollector: NSObject, URLSessionTaskDelegate {3 func urlSession(4 _ session: URLSession,5 task: URLSessionTask,6 didFinishCollecting metrics: URLSessionTaskMetrics7 ) {8 for metric in metrics.transactionMetrics {9 let dns = metric.domainLookupEndDate?.timeIntervalSince(10 metric.domainLookupStartDate ?? Date()11 ) ?? 012 let connect = metric.connectEndDate?.timeIntervalSince(13 metric.connectStartDate ?? Date()14 ) ?? 015 let ttfb = metric.responseStartDate?.timeIntervalSince(16 metric.requestStartDate ?? Date()17 ) ?? 018 19 os_log(20 "Network: DNS=%.3fs Connect=%.3fs TTFB=%.3fs Total=%.3fs",21 dns, connect, ttfb,22 metric.responseEndDate?.timeIntervalSince(23 metric.fetchStartDate ?? Date()24 ) ?? 025 )26 }27 }28}29 30// Optimized URLSession configuration31let config = URLSessionConfiguration.default32config.httpMaximumConnectionsPerHost = 633config.timeoutIntervalForRequest = 1534config.waitsForConnectivity = true35config.urlCache = URLCache(36 memoryCapacity: 20 * 1024 * 1024, // 20MB memory37 diskCapacity: 100 * 1024 * 1024 // 100MB disk38)6. Energy Log & Pil Optimizasyonu
Bileşen | Yüksek Tüketim | Optimizasyon |
|---|---|---|
**CPU** | >20% sürekli | Background task'ları QoS.utility ile |
**Network** | Sık küçük istekler | Batch request, URLSession background |
**GPS** | Continuous significant | significantLocationChange kullan |
**Display** | 120Hz sürekli | CADisplayLink adaptive |
**Bluetooth** | Continuous scan | İhtiyaç olunca scan, timeout koy |
swift
1// Adaptive frame rate2class AdaptiveDisplayLink {3 private var displayLink: CADisplayLink?4 private var isAnimating = false5 6 func start(preferredFPS: Int = 60) {7 displayLink = CADisplayLink(target: self, selector: #selector(update))8 if #available(iOS 15.0, *) {9 displayLink?.preferredFrameRateRange = CAFrameRateRange(10 minimum: 30, maximum: Float(preferredFPS), preferred: Float(preferredFPS)11 )12 }13 displayLink?.add(to: .main, forMode: .common)14 }15 16 @objc private func update(_ link: CADisplayLink) {17 // Render update18 }19 20 func stop() {21 displayLink?.invalidate()22 displayLink = nil23 }24}7. Core Animation Profiling
swift
1// Scroll performance optimizasyonu2class OptimizedCell: UICollectionViewCell {3 // 1. Opaque views kullan4 override func awakeFromNib() {5 super.awakeFromNib()6 layer.shouldRasterize = true7 layer.rasterizationScale = UIScreen.main.scale8 contentView.isOpaque = true9 backgroundColor = .systemBackground10 }11 12 // 2. Image'ları background'da hazırla13 func configure(with imageURL: URL) {14 Task.detached(priority: .userInitiated) {15 let (data, _) = try await URLSession.shared.data(from: imageURL)16 guard let image = UIImage(data: data) else { return }17 18 // Downscale to cell size19 let renderer = UIGraphicsImageRenderer(size: CGSize(width: 100, height: 100))20 let resized = renderer.image { _ in21 image.draw(in: CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))22 }23 24 await MainActor.run {25 self.imageView.image = resized26 }27 }28 }29 30 private let imageView = UIImageView()31}FPS Hedefleri
Senaryo | Hedef FPS | Kabul Edilebilir |
|---|---|---|
Scroll | 60 (120 ProMotion) | >50 |
Animasyon | 60 | >45 |
Geçiş | 60 | >30 |
İdle | Düşük | <10 |
8. System Trace & Thread Analizi
swift
1// Priority inversion önleme2actor DataProcessor {3 private let processingQueue = DispatchQueue(4 label: "processing",5 qos: .userInitiated, // Doğru QoS seviyesi6 attributes: .concurrent7 )8 9 func process(_ items: [DataItem]) async -> [Result] {10 await withCheckedContinuation { continuation in11 processingQueue.async {12 let results = items.map { self.transform($0) }13 continuation.resume(returning: results)14 }15 }16 }17 18 private func transform(_ item: DataItem) -> Result {19 // CPU-intensive dönüşüm20 Result(item: item)21 }22}9. Custom Instruments
swift
1// os_signpost ile custom trace noktaları2import os3 4extension OSSignposter {5 static let appFlow = OSSignposter(6 subsystem: "com.app.flow",7 category: "UserFlow"8 )9}10 11// Kullanım12func userTappedPurchase() {13 let id = OSSignposter.appFlow.makeSignpostID()14 let state = OSSignposter.appFlow.beginInterval("Purchase", id: id)15 16 Task {17 do {18 let product = try await fetchProduct()19 OSSignposter.appFlow.emitEvent("ProductFetched", id: id, "SKU: \(product.sku)")20 21 let result = try await purchase(product)22 OSSignposter.appFlow.endInterval("Purchase", state, "Success: \(result.transactionId)")23 } catch {24 OSSignposter.appFlow.endInterval("Purchase", state, "Error: \(error.localizedDescription)")25 }26 }27}10. CI/CD Performance Regression
swift
1// XCTest performance tests2final class PerformanceTests: XCTestCase {3 func testScrollPerformance() throws {4 let app = XCUIApplication()5 app.launch()6 7 let options = XCTMeasureOptions()8 options.iterationCount = 59 10 measure(metrics: [11 XCTCPUMetric(application: app),12 XCTMemoryMetric(application: app),13 XCTClockMetric()14 ], options: options) {15 let list = app.collectionViews.firstMatch16 list.swipeUp(velocity: .fast)17 list.swipeDown(velocity: .fast)18 }19 }20 21 func testLaunchPerformance() throws {22 measure(metrics: [XCTApplicationLaunchMetric()]) {23 XCUIApplication().launch()24 }25 }26}11. Production Monitoring
swift
1// MetricKit ile production metrikleri2import MetricKit3 4class MetricSubscriber: NSObject, MXMetricManagerSubscriber {5 func didReceive(_ payloads: [MXMetricPayload]) {6 for payload in payloads {7 // Launch time8 if let launch = payload.applicationLaunchMetrics {9 let resumeTime = launch.histogrammedApplicationResumeTime10 logMetric("app_resume", histogram: resumeTime)11 }12 13 // Memory14 if let memory = payload.memoryMetrics {15 let peak = memory.peakMemoryUsage16 logMetric("peak_memory", measurement: peak)17 }18 19 // Disk20 if let disk = payload.diskIOMetrics {21 logMetric("disk_writes", measurement: disk.cumulativeLogicalWrites)22 }23 }24 }25 26 func didReceive(_ payloads: [MXDiagnosticPayload]) {27 for payload in payloads {28 if let hangs = payload.hangDiagnostics {29 for hang in hangs {30 logDiagnostic("hang", callStack: hang.callStackTree)31 }32 }33 }34 }35 36 private func logMetric(_ name: String, histogram: MXHistogram<some Unit>? = nil,37 measurement: Measurement<some Unit>? = nil) {38 // Analytics'e gönder39 }40 41 private func logDiagnostic(_ type: String, callStack: MXCallStackTree) {42 // Crash reporting'e gönder43 }44}12. Performance Checklist
Launch Time
- Cold launch < 400ms
- Warm launch < 200ms
- Lazy initialization kullan
- +initialize ve +load'dan kaçın
Memory
- Peak memory < 150MB (iPhone)
- Leak-free navigation flow
- Image downsampling aktif
- Cache eviction politikası var
Rendering
- 60 FPS scroll (ProMotion: 120)
- Off-screen render yok
- Blending minimize
- shouldRasterize uygun yerde
Network
- HTTP/2 aktif
- Response caching var
- Batch requests kullanılıyor
- Background download/upload
Energy
- Background'da CPU < 5%
- Location accuracy uygun seviyede
- Timer coalescing aktif
- Idle state düşük tüketim
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

