1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Android開発30日間マスターシリーズ - Day28: デバッグ技法とパフォーマンス最適化 - Android Studio Profilerとメモリリーク対策

Posted at

はじめに

今日のテーマは、アプリの品質を飛躍的に向上させるためのデバッグ技法パフォーマンス最適化です。バグを見つけて修正するだけでなく、アプリの動作をよりスムーズにし、バッテリー消費を抑えるための方法を学びます。


1. 効果的なデバッグ技法

1.1 構造化ログの活用

単純なLog.d()の代わりに、Timberライブラリを使用して構造化されたログを実装しましょう:

// build.gradle.kts (Module: app)
dependencies {
    implementation("com.jakewharton.timber:timber:5.0.1")
}

// Application クラス
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        } else {
            // プロダクションでは Crashlytics などに送信
            Timber.plant(CrashlyticsTree())
        }
    }
}

// 使用例
class ChatViewModel : ViewModel() {
    fun sendMessage(message: String) {
        Timber.tag("ChatViewModel").d("Sending message: length=${message.length}")
        
        viewModelScope.launch {
            try {
                val response = repository.sendMessage(message)
                Timber.tag("ChatViewModel").i("Response received: ${response.id}")
            } catch (e: Exception) {
                Timber.tag("ChatViewModel").e(e, "Failed to send message")
            }
        }
    }
}

1.2 デバッグビルド用の開発者オプション

デバッグビルド限定の機能を追加して開発効率を向上:

// BuildConfig を活用したデバッグ機能
class DebugUtils {
    companion object {
        fun showDebugInfo(context: Context, info: String) {
            if (BuildConfig.DEBUG) {
                Toast.makeText(context, "DEBUG: $info", Toast.LENGTH_SHORT).show()
            }
        }
        
        fun logNetworkRequest(url: String, method: String) {
            if (BuildConfig.DEBUG) {
                Timber.tag("NETWORK").d("$method $url")
            }
        }
    }
}

// デバッグメニューの実装
class MainActivity : AppCompatActivity() {
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        if (BuildConfig.DEBUG) {
            menuInflater.inflate(R.menu.debug_menu, menu)
        }
        return super.onCreateOptionsMenu(menu)
    }
    
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_clear_cache -> {
                clearApplicationCache()
                true
            }
            R.id.action_mock_data -> {
                loadMockData()
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }
}

1.3 効果的なブレークポイントの使用

条件付きブレークポイントと式の評価を活用:

// 条件付きブレークポイントの例
fun processMessages(messages: List<Message>) {
    messages.forEach { message ->
        // ブレークポイント条件: message.content.contains("error")
        if (message.isImportant) {
            handleImportantMessage(message)
        }
        // 式の評価: message.timestamp - System.currentTimeMillis()
        processMessage(message)
    }
}

2. Android Studio Profilerの活用

2.1 CPU Profilerによるボトルネック解析

CPU Profilerを使用してパフォーマンスのボトルネックを特定:

// パフォーマンスの問題があるコード例
class MessageProcessor {
    // 重い処理の例
    fun processMessages(messages: List<Message>): List<ProcessedMessage> {
        // 問題: UI スレッドでの重い処理
        return messages.map { message ->
            // 複雑な文字列操作
            val processedContent = message.content
                .split(" ")
                .joinToString(" ") { word ->
                    if (isSpecialWord(word)) {
                        processSpecialWord(word) // 重い処理
                    } else {
                        word
                    }
                }
            
            ProcessedMessage(
                id = message.id,
                content = processedContent,
                timestamp = System.currentTimeMillis()
            )
        }
    }
    
    // 最適化後
    suspend fun processMessagesOptimized(messages: List<Message>): List<ProcessedMessage> = 
        withContext(Dispatchers.Default) {
            messages.asSequence() // 遅延評価でメモリ効率向上
                .map { message ->
                    async { // 並列処理
                        processMessage(message)
                    }
                }
                .toList()
                .awaitAll()
        }
}

2.2 Memory Profilerの詳細活用

Memory Profilerを使ったメモリリークの特定と対策:

// メモリリークの例とその修正
class BadExample : AppCompatActivity() {
    companion object {
        // 危険: static変数でActivityを保持
        private var instance: BadExample? = null
    }
    
    private val handler = Handler(Looper.getMainLooper())
    private val runnable = Runnable {
        // 危険: Activityが破棄された後も実行される可能性
        updateUI()
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        instance = this // メモリリークの原因
        
        // 遅延実行(危険)
        handler.postDelayed(runnable, 10000)
    }
}

// 修正版
class GoodExample : AppCompatActivity() {
    private var handler: Handler? = null
    private var runnable: Runnable? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        handler = Handler(Looper.getMainLooper())
        runnable = Runnable {
            if (!isDestroyed && !isFinishing) {
                updateUI()
            }
        }
        
        handler?.postDelayed(runnable!!, 10000)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // リソースの確実なクリーンアップ
        handler?.removeCallbacks(runnable!!)
        handler = null
        runnable = null
    }
}

2.3 Network Profilerによる通信最適化

ネットワーク使用量の最適化:

class OptimizedNetworkManager {
    private val cache = LruCache<String, CachedResponse>(50)
    
    // リクエストの最適化
    suspend fun fetchData(url: String, forceRefresh: Boolean = false): ApiResponse {
        // キャッシュチェック
        if (!forceRefresh) {
            cache.get(url)?.let { cached ->
                if (cached.isValid()) {
                    Timber.d("Using cached response for $url")
                    return cached.response
                }
            }
        }
        
        return try {
            val response = apiService.getData(url)
            // レスポンスをキャッシュ
            cache.put(url, CachedResponse(response, System.currentTimeMillis()))
            response
        } catch (e: Exception) {
            // オフライン時はキャッシュから返す
            cache.get(url)?.response ?: throw e
        }
    }
    
    data class CachedResponse(
        val response: ApiResponse,
        val timestamp: Long
    ) {
        fun isValid(): Boolean = 
            System.currentTimeMillis() - timestamp < TimeUnit.MINUTES.toMillis(5)
    }
}

3. 一般的なメモリリークパターンと対策

3.1 ViewModelでのメモリリーク対策

class ChatViewModel(
    private val repository: ChatRepository
) : ViewModel() {
    private val _messages = MutableStateFlow<List<Message>>(emptyList())
    val messages = _messages.asStateFlow()
    
    private var streamingJob: Job? = null
    
    fun startStreaming() {
        // 前のジョブをキャンセル
        streamingJob?.cancel()
        
        streamingJob = viewModelScope.launch {
            repository.streamMessages()
                .catch { exception ->
                    Timber.e(exception, "Streaming failed")
                }
                .collect { message ->
                    _messages.update { currentMessages ->
                        currentMessages + message
                    }
                }
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        // 明示的にジョブをキャンセル
        streamingJob?.cancel()
        Timber.d("ViewModel cleared")
    }
}

3.2 RecyclerViewの最適化

class OptimizedMessageAdapter : ListAdapter<Message, MessageViewHolder>(DiffCallback) {
    
    // DiffUtil による効率的な更新
    private object DiffCallback : DiffUtil.ItemCallback<Message>() {
        override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean =
            oldItem.id == newItem.id
            
        override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean =
            oldItem == newItem
    }
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_message, parent, false)
        return MessageViewHolder(view)
    }
    
    override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
    
    override fun onViewRecycled(holder: MessageViewHolder) {
        super.onViewRecycled(holder)
        // ViewHolder がリサイクルされる時のクリーンアップ
        holder.cleanup()
    }
}

class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private var loadImageJob: Job? = null
    
    fun bind(message: Message) {
        // 前の画像読み込みジョブをキャンセル
        loadImageJob?.cancel()
        
        if (message.imageUrl != null) {
            loadImageJob = CoroutineScope(Dispatchers.Main).launch {
                // 画像の非同期読み込み
                loadImageSafely(message.imageUrl)
            }
        }
    }
    
    fun cleanup() {
        loadImageJob?.cancel()
    }
}

4. パフォーマンス監視の自動化

4.1 カスタムパフォーマンス指標

class PerformanceMonitor {
    companion object {
        private const val TAG = "PerformanceMonitor"
        
        inline fun <T> measureExecutionTime(
            operationName: String,
            operation: () -> T
        ): T {
            val startTime = System.currentTimeMillis()
            val result = operation()
            val endTime = System.currentTimeMillis()
            
            val duration = endTime - startTime
            Timber.tag(TAG).d("$operationName took ${duration}ms")
            
            // 閾値を超えた場合の警告
            if (duration > 100) {
                Timber.tag(TAG).w("$operationName is slow: ${duration}ms")
            }
            
            return result
        }
        
        fun trackMemoryUsage(context: String) {
            val runtime = Runtime.getRuntime()
            val usedMemoryMB = (runtime.totalMemory() - runtime.freeMemory()) / 1048576L
            val maxMemoryMB = runtime.maxMemory() / 1048576L
            
            Timber.tag(TAG).d("$context - Memory: $usedMemoryMB MB / $maxMemoryMB MB")
            
            // メモリ使用率が80%を超えた場合
            if (usedMemoryMB.toDouble() / maxMemoryMB > 0.8) {
                Timber.tag(TAG).w("High memory usage detected: ${usedMemoryMB}MB")
                System.gc() // ガベージコレクションを提案
            }
        }
    }
}

// 使用例
class ChatRepository {
    suspend fun processLargeDataset(data: List<RawData>): List<ProcessedData> {
        return PerformanceMonitor.measureExecutionTime("processLargeDataset") {
            PerformanceMonitor.trackMemoryUsage("Before processing")
            
            val result = data.chunked(100) // バッチ処理
                .flatMap { batch ->
                    processBatch(batch)
                }
            
            PerformanceMonitor.trackMemoryUsage("After processing")
            result
        }
    }
}

4.2 LaunchMode とタスク管理

// AndroidManifest.xml での適切な設定
/*
<activity 
    android:name=".MainActivity"
    android:launchMode="singleTop"
    android:exported="true">
    ...
</activity>
*/

class MainActivity : AppCompatActivity() {
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        // singleTop での新しいインテント処理
        handleNewIntent(intent)
    }
    
    private fun handleNewIntent(intent: Intent?) {
        intent?.let {
            when (it.action) {
                Intent.ACTION_SEND -> {
                    handleSharedContent(it)
                }
            }
        }
    }
}

5. 本番環境での監視

5.1 Firebase Performance Monitoring

// build.gradle.kts (Module: app)
dependencies {
    implementation("com.google.firebase:firebase-perf:20.4.1")
}

class NetworkManager {
    private val firebasePerformance = FirebasePerformance.getInstance()
    
    suspend fun apiCall(url: String): ApiResponse {
        val trace = firebasePerformance.newTrace("api_call_$url")
        trace.start()
        
        try {
            val response = httpClient.get(url)
            trace.putAttribute("status_code", response.status.toString())
            trace.putAttribute("response_size", response.contentLength().toString())
            return response
        } catch (e: Exception) {
            trace.putAttribute("error", e.message ?: "unknown")
            throw e
        } finally {
            trace.stop()
        }
    }
}

6. まとめ

デバッグのベストプラクティス:

  • 構造化ログ: Timberを使用した適切なログレベル管理
  • 条件付きブレークポイント: 効率的なデバッグセッション
  • デバッグビルド限定機能: 開発者向けツールの実装

パフォーマンス最適化:

  • CPU最適化: 非同期処理とバックグラウンドスレッドの活用
  • メモリ最適化:適切なライフサイクル管理とリソース解放
  • ネットワーク最適化: キャッシュ機能と効率的なデータ取得

監視と計測:

  • 自動パフォーマンス測定: カスタム指標での継続的な監視
  • 本番環境監視: Firebase Performance Monitoring による実ユーザー体験の把握

これらの技術を適用することで、より安定で高速なアプリケーションを構築できます。次回は、いよいよアプリをリリースするための最終準備、Google Play Storeへの登録プロセスを学びます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?