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