iOS Performance(パフォーマンス最適化支援)
概要
iOSアプリケーションのパフォーマンス最適化を包括的に支援するスキル。 Instruments等のツールを活用した計測から、具体的な改善施策の実装までをカバーする。
対象領域
- メモリ使用量: リーク検出、メモリフットプリント最適化
- CPU使用率: 処理の効率化、バックグラウンド処理最適化
- GPU使用率: レンダリング最適化、フレームレート改善
- 起動時間: Cold/Warm起動の高速化
- バッテリー消費: 電力効率の改善
- ネットワーク: 通信効率の最適化
実行条件
以下の状況でこのスキルを起動する:
- アプリの動作が遅いと感じる時
- メモリ使用量が高いと報告された時
- バッテリー消費が多いと指摘された時
- App Store Connect でパフォーマンス警告が出た時
- ユーザーからパフォーマンスに関するフィードバックがあった時
- 新機能追加後のパフォーマンス影響を確認したい時
- リリース前のパフォーマンス検証を行う時
プロセス
Phase 1: パフォーマンス計測
1.1 計測指標の定義
| 指標 | 目標値 | 計測方法 |
|---|---|---|
| 起動時間(Cold) | < 400ms | Instruments / MetricKit |
| 起動時間(Warm) | < 200ms | Instruments / MetricKit |
| メモリ使用量 | < 100MB(基本機能) | Instruments / Xcode Memory Gauge |
| CPU使用率(アイドル) | < 1% | Instruments |
| フレームレート | 60fps / 120fps | Instruments / CADisplayLink |
| バッテリー消費 | 低〜中 | Instruments / MetricKit |
1.2 Instruments の選択
詳細は references/instruments-guide.md を参照。
| 問題 | 推奨 Instrument |
|---|---|
| メモリリーク | Leaks |
| メモリ使用量 | Allocations |
| CPU使用率 | Time Profiler |
| UI性能 | Core Animation / Animation Hitches |
| 起動時間 | App Launch |
| バッテリー | Energy Log |
| ネットワーク | Network |
Phase 2: メモリ最適化
詳細は references/memory-optimization.md を参照。
メモリフットプリント削減
// 画像メモリ最適化
// Bad: 大きな画像をそのまま保持
let image = UIImage(named: "large_image")
// Good: 適切なサイズにリサイズ
func resizedImage(_ image: UIImage, targetSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
}
}
// Better: ImageIO でメモリ効率的に読み込み
func downsampledImage(at url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, imageSourceOptions) else {
return nil
}
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
return UIImage(cgImage: downsampledImage)
}
Phase 3: CPU最適化
詳細は references/cpu-optimization.md を参照。
Time Profiler による分析
// 重い処理の特定と最適化
// Bad: メインスレッドでの重い処理
func processData() {
let result = heavyComputation(data) // UI がブロックされる
updateUI(with: result)
}
// Good: バックグラウンドスレッドで処理
func processData() {
Task.detached(priority: .userInitiated) {
let result = heavyComputation(data)
await MainActor.run {
updateUI(with: result)
}
}
}
Phase 4: GPU/レンダリング最適化
詳細は references/rendering-optimization.md を参照。
4.1 フレームドロップの検出
// CADisplayLink でフレームレート監視
class FrameRateMonitor {
private var displayLink: CADisplayLink?
private var lastTimestamp: CFTimeInterval = 0
func start() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .common)
}
@objc private func tick(_ link: CADisplayLink) {
if lastTimestamp > 0 {
let duration = link.timestamp - lastTimestamp
let fps = 1.0 / duration
if fps < 55 { // 60fps を下回ったら警告
print("Frame drop detected: \(fps) fps")
}
}
lastTimestamp = link.timestamp
}
func stop() {
displayLink?.invalidate()
displayLink = nil
}
}
4.2 オフスクリーンレンダリングの回避
// Bad: オフスクリーンレンダリングを発生させる
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowOpacity = 0.3
// Good: 別レイヤーで影を描画
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
let shadowView = UIView(frame: view.frame)
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0, height: 2)
shadowView.layer.shadowOpacity = 0.3
shadowView.layer.shadowPath = UIBezierPath(
roundedRect: view.bounds,
cornerRadius: 10
).cgPath
// Better: SwiftUI の compositingGroup
view
.cornerRadius(10)
.compositingGroup()
.shadow(radius: 5)
Phase 5: 起動時間最適化
詳細は references/launch-optimization.md を参照。
5.1 起動フェーズの理解
Cold Launch:
1. dyld: ライブラリ読み込み
2. Runtime: クラス初期化、+load メソッド
3. UIKit: AppDelegate/SceneDelegate 初期化
4. Initial Frame: 最初の画面描画
Warm Launch:
- プロセスは生きているが、バックグラウンドから復帰
- UIの再構築が必要
5.2 起動時間計測
// アプリ起動時間の計測
class AppLaunchTimer {
static let shared = AppLaunchTimer()
private let processStartTime: CFAbsoluteTime
init() {
// プロセス起動時刻を取得
var kinfo = kinfo_proc()
var size = MemoryLayout<kinfo_proc>.stride
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
sysctl(&mib, u_int(mib.count), &kinfo, &size, nil, 0)
let startTime = kinfo.kp_proc.p_starttime
processStartTime = CFAbsoluteTime(startTime.tv_sec) + CFAbsoluteTime(startTime.tv_usec) / 1_000_000
}
func markFirstFrame() {
let now = CFAbsoluteTimeGetCurrent()
let launchTime = now - processStartTime
print("App launch time: \(launchTime * 1000) ms")
}
}
// AppDelegate で使用
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 最初のフレーム描画後に計測
DispatchQueue.main.async {
AppLaunchTimer.shared.markFirstFrame()
}
return true
}
5.3 起動最適化テクニック
// 1. 遅延初期化
class HeavyService {
static let shared = HeavyService() // lazy by default
private init() {
// 重い初期化処理
}
}
// 2. バックグラウンドでの初期化
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 起動に必須な処理のみメインスレッドで
setupCriticalServices()
// 非必須の処理はバックグラウンドで
DispatchQueue.global(qos: .utility).async {
self.setupAnalytics()
self.warmUpCache()
self.preloadData()
}
return true
}
// 3. 静的初期化の回避
// Bad: グローバル変数の重い初期化
let heavyFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ja_JP")
formatter.dateStyle = .full
return formatter
}()
// Good: 必要時に初期化
class DateFormatterProvider {
private var _formatter: DateFormatter?
var formatter: DateFormatter {
if _formatter == nil {
let f = DateFormatter()
f.locale = Locale(identifier: "ja_JP")
f.dateStyle = .full
_formatter = f
}
return _formatter!
}
}
Phase 6: バッテリー最適化
詳細は references/battery-optimization.md を参照。
6.1 バッテリー消費の主要因
| 要因 | 影響度 | 対策 |
|---|---|---|
| 位置情報 | 高 | 精度を下げる、バックグラウンド更新を制限 |
| ネットワーク | 高 | バッチ処理、適切なタイミングで通信 |
| CPU | 中 | 処理の効率化、アイドル時の処理削減 |
| GPU | 中 | 不要なアニメーション削減 |
| センサー | 低〜中 | 必要時のみ使用 |
6.2 位置情報の最適化
// Bad: 常に高精度
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
// Good: 用途に応じた精度
// ナビゲーション時
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
// 大まかな位置のみ必要な場合
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
// 重要な位置変化のみ
locationManager.startMonitoringSignificantLocationChanges()
6.3 ネットワーク最適化
// バッチ処理でネットワークアクセスをまとめる
class BatchNetworkManager {
private var pendingRequests: [URLRequest] = []
private var batchTimer: Timer?
func enqueue(_ request: URLRequest) {
pendingRequests.append(request)
// 5秒後にバッチ実行
batchTimer?.invalidate()
batchTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { [weak self] _ in
self?.executeBatch()
}
}
private func executeBatch() {
guard !pendingRequests.isEmpty else { return }
// バッチリクエストを実行
let requests = pendingRequests
pendingRequests = []
// 実行処理...
}
}
Phase 7: MetricKit によるモニタリング
import MetricKit
class MetricsManager: NSObject, MXMetricManagerSubscriber {
static let shared = MetricsManager()
private override init() {
super.init()
MXMetricManager.shared.add(self)
}
// iOS 13+: 日次レポート
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
// 起動時間
if let launchMetrics = payload.applicationLaunchMetrics {
analyzeLaunchMetrics(launchMetrics)
}
// メモリ
if let memoryMetrics = payload.memoryMetrics {
analyzeMemoryMetrics(memoryMetrics)
}
// CPU
if let cpuMetrics = payload.cpuMetrics {
analyzeCPUMetrics(cpuMetrics)
}
}
}
// iOS 14+: 診断レポート
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
// クラッシュ
if let crashDiagnostics = payload.crashDiagnostics {
analyzeCrashes(crashDiagnostics)
}
// ハング
if let hangDiagnostics = payload.hangDiagnostics {
analyzeHangs(hangDiagnostics)
}
}
}
private func analyzeLaunchMetrics(_ metrics: MXAppLaunchMetric) {
let resumeTime = metrics.histogrammedApplicationResumeTime
let launchTime = metrics.histogrammedTimeToFirstDraw
// 閾値を超えたらアラート
// ...
}
}
ガードレール
必須遵守事項
- 計測前後の比較: 最適化前後で必ず計測し効果を検証
- 複数デバイスでの検証: 最低スペックのサポートデバイスでテスト
- リリース構成でのテスト: Debug ではなく Release でプロファイリング
- 継続的モニタリング: MetricKit でリリース後も監視
禁止事項
- 可読性の犠牲: 極端な最適化で保守性を損なわない
- 機能の削減: パフォーマンスのためにUXを損なわない
警告事項
- シミュレータでの計測: 実機と性能特性が異なる
- デバッグビルド: オーバーヘッドが大きい
- サンプルサイズ: 複数回計測して平均を取る
- 環境要因: バッテリー残量、温度、他アプリの影響
参照
- references/instruments-guide.md: Instruments活用ガイド
- references/memory-optimization.md: メモリ最適化詳細
- references/cpu-optimization.md: CPU最適化詳細
- references/rendering-optimization.md: レンダリング最適化詳細
- references/launch-optimization.md: 起動時間最適化詳細
- references/battery-optimization.md: バッテリー最適化詳細