省電力設計の本質:通信・位置情報・ディスプレイをどう絞るか

「Androidアプリ開発の極意」(木田学ほか著、テックファーム監修、技術評論社、2017年)を読み進めながら整理したメモがベースになっている。刊行から年数が経っている本だが、バッテリー消費を抑えるための設計判断そのものは古びていない部分が多い。 はじめに アプリが「重い」「電池が減る」と言われるとき、原因の多くはCPU負荷そのものより、通信・位置情報・ディスプレイの3つの使い方に集約される。個別の最適化テクニックを覚える前に、「なぜその実装がバッテリーを食うのか」という設計原理を押さえておくと、新しいAPIが出てきたときにも応用が効く。 基本方針はシンプルで、避けられるなら避ける、必要なら節約する設計にするの一言に尽きる。以下、この3領域それぞれで何が問題になり、Androidがどう対処してきたかを整理する。 通信:タイミングをずらし、量を減らす 決まった時間に一斉通信しない 複数のアプリが同じタイミングで定期通信を行うと、端末は本来スリープしていられる時間まで叩き起こされることになる。これに対するAndroidの答えがAlarmManagerのアラームバッチング機構だ。 AlarmManager.set()(setExact()ではない通常版)は、API 19(Android 4.4)以降、指定時刻ちょうどの起動が保証されない仕様に変わっている。システムが複数アプリのアラームをまとめて処理し、意図的に遅延・分散させることで端末のウェイクアップ回数そのものを減らす仕組みだ。厳密な時刻指定が必要なカレンダーの通知などはsetExact()を使う設計になっており、「本当に正確な時刻が必要か」を自問するだけで、使うAPIが変わる。 電源状態・接続状態に応じて挙動を変える 満充電/充電中/放電中/充電なし、バッテリー残量は取得できるので、これに応じて通信頻度を変える設計が可能 そもそもネットワークに繋がっていなければ、到達しないとわかっている通信を試みるだけ無駄になる。接続状態はAndroidでは容易に取得できる 3G等の従量課金回線では、通信量そのものを絞る設計も選択肢に入る(ただし経路をつど判定するコストとのトレードオフは意識しておきたい) バックグラウンド通信制限(データセーバー) Android N以降には「データセーバー」機能があり、有効化されているとフォアグラウンドでも通信を抑える方向に働く。アプリ側はこの挙動があり得ることを前提に設計しておく必要がある。 通信データ量そのものを減らす GZIP圧縮は最低限やっておく データはまとめて取得し、通信回数自体を減らす キャッシュを活用する レジュームダウンロード(範囲指定でのデータ取得)を使えば、中断位置から再取得して結合できる ポーリング vs プッシュ 定期的に通信して更新を確認する「ポーリング」に対し、サーバー側から必要なときだけ通知する「プッシュ」はバッテリー面で有利になりやすい。AndroidではFCM(Firebase Cloud Messaging)が定番で、以下の制約を踏まえて設計する。 項目 内容 データメッセージのペイロード上限 4096バイト(4KB) 通知メッセージのペイロード上限 2KB メッセージの有効期限(デフォルト) 4週間(約28日) ペイロードには最小限のデータ(IDなど)だけを載せ、本体データはAPI経由で取得する設計が定石になる。ペイロードに実データを詰め込みすぎると、将来的にサイズ上限に引っかかって通知が届かなくなるか、最悪クラッシュにつながる。 リトライ間隔を空ける 通信失敗時のリトライを短い間隔で繰り返すと、サーバー負荷とクライアントのバッテリー消費の両方を悪化させる。指数バックオフなど、間隔を徐々に空ける設計が基本になる。 通信量の主導権をユーザーに渡す 通信量に関わる挙動(画質、同期頻度など)をユーザー自身が選べるようにするのも有効な設計の一つ。 ディスプレイ:点灯そのものを減らす ディスプレイは単純に「光っている時間」がそのままバッテリー消費に直結する。不要な点滅を避けるのは当然として、注意すべきはWakeLockの扱いだ。 WakeLockは画面スリープを防ぐ機能で、バッテリーの敵として扱われがちだが、ダウンロード処理中など「画面がオフになると処理自体が失敗する」場面では必須になる。**「使わない」ではなく「必要な範囲だけ、必要な時間だけ保持して確実に解放する」**設計判断が求められる。 位置情報:APIの選び方で消費が変わる LocationServices(Google Play Services必須)を使う方が、LocationManager単体よりバッテリー効率が良い傾向にある LocationManagerを使う場合、プロバイダをNETWORK_PROVIDERに指定するとWiFi経由の測位のみになり、GPSを使うより省電力になる 精度が本当に必要な場面かどうかを見極め、過剰に高精度なプロバイダを選ばないことが基本方針になる。 実装をまとめて任せる:JobScheduler ここまでの対策(電源状態に応じた実行判断、リトライ間隔、実行タイミングの分散など)を個別に手組みするのではなく、JobScheduler一本に任せるという選択肢がある。バッテリー状態・実行遅延・リトライ間隔などを条件として設定でき、標準APIとして安定している。特別な理由がない限り、これを軸に設計するのが妥当だろう。 なお、Android N以降ではブロードキャスト配信の仕組み自体にも変更が入っている。CONNECTIVITY_ACTION(ネットワーク変更)のような特定のブロードキャストを受信対象アプリが一斉に受け取ることでパフォーマンスが悪化する問題への対処で、対象イベントは複数あるが、ここでは深追いしない。 Expo/React Nativeでは何に当たるか ここまでの話はAndroidネイティブAPIの話だが、Expo管理下のReact Nativeアプリでは、それぞれ対応するモジュールに置き換わる。「素のAndroid APIを直接叩けない」制約はあるが、内部的には同じ仕組みに乗っているケースが多い。 Androidネイティブの概念 Expoでの対応 備考 AlarmManager / JobScheduler expo-background-task SDK 53でexpo-background-fetchを置き換え。Android側は内部的にWorkManager(JobSchedulerベース)を利用 FCM(プッシュ通知) expo-notifications リモート通知の受信・権限管理をラップ。ペイロードサイズ等の制約はFCM側の仕様がそのまま適用される WakeLock expo-keep-awake useKeepAwake()フックまたは命令的APIで画面スリープを抑制。必要な範囲だけ有効化する設計方針は変わらない 接続状態の判定 expo-network オンライン/オフライン、接続種別(WiFi/セルラー等)の取得に使う バッテリー状態の取得 expo-battery 充電状態・残量の取得。電源状態に応じた通信頻度の出し分けに使える LocationManager / LocationServices expo-location 精度指定(Accuracy)でGPS/WiFi測位のどちらに寄せるかを制御できる 注意点は主に2つ。 ...

2026年9月9日 · 1 min

Androidアプリ開発の極意 4,5章ユーザー体験と非同期処理

前の記事同様に Androidアプリ開発の極意 | 技術評論社の「ユーザーストレス軽減」まわりの実装Tipsと、並行処理の選択肢をざっくり整理してExpo周りと絡めたもの。細かい実装には踏み込まず、要点だけ。 はじめに アプリが固まる原因の大半は、メインスレッド(UIスレッド)を長時間ブロックしていること。非同期処理の設計は結局「ユーザーを待たせない」という一点のためにある。 二部構成で一部は本を読んだ感想+α、二部がExpoだとどうなるかって話。 基本的にAIで出して、それを読んで調べたりしたもの。 第1部:Androidの話 Android(Java/Kotlin)側だけで完結する話。UXまわりの実装Tipsと、非同期処理の手段がsynchronizedからVirtual Threadsまでどう変遷してきたかを、Expoの話を挟まずに整理する。 UXまわりの小さな判断 時間のかかる処理にはProgressBarで進捗を見せる SharedPreferencesは永続化のためディスクI/Oが発生する セルラー通信時は重い処理の前に一言断りを入れると親切 エラーはその場で表示しつつ、Crashlytics/ACRAのような開発者向けツールで後から追える形が理想 NDKは高速化と引き換えにデバッグの複雑さが増す。基本的には奥の手 非同期処理はなぜ増え続けているのか synchronizedは「同時に入らない」ことしか保証しない。順番までは保証しない。 Threadを直接生成した場合も同様で、生成順と実行順は一致しない。順番を保証したいならExecutors.newSingleThreadExecutor()のような直列化の仕組みを使う。 ExecutorServiceは、スレッドを直接管理せず、プールとキューで管理する仕組み。スレッド数の制御や再利用をアプリ側のロジックから切り離せる。 AsyncTaskはAPI 30で非推奨になった。ライフサイクル管理が弱く、Activity/Fragment破棄後もタスクが動き続けてリークやクラッシュを招くのが理由。 今の主流はKotlin Coroutines。スレッドをブロックせず中断できる軽量な並行処理で、排他制御が必要ならMutexを使う。UIに紐づく短い処理はCoroutines、画面を離れても続けるべき処理はWorkManagerと使い分ける。 JVM側ではJDK 21でVirtual Threads(JEP 444)が正式機能になった。I/Oバウンドな処理を大量にさばくのに向くが、CPUバウンドには向かない。ただしこれはJDK/Java SE側の機能で、Android Runtime(ART)は非対応。Androidアプリでそのまま使えるわけではない。 Akkaはアクター単位でメッセージパッシングする設計思想。2022年にライセンスがApache 2.0からBSL(Business Source License)に変わり、2024年10月以降はproduction利用にライセンスキーが必須になった。個人・OSS・startupは無料キー、商用は原則サブスクという体系。汎用ツールというより大規模分散システム向けの専門ツールという位置づけに変わっている。 比較表(Android) 手段 排他制御 順序保証 位置づけ synchronized あり なし 低レベルの排他制御プリミティブ Thread直接生成 なし なし 基本的には避ける ExecutorService 設計次第 直列化すれば可 CPUバウンド処理で有効 AsyncTask なし なし API 30で非推奨 Kotlin Coroutines Mutexで対応 設計次第 Android/Kotlinの主流 Virtual Threads 従来通り 設計次第 JVM側、I/Oバウンド向け(ART非対応) Akka アクターモデル 条件付き 大規模分散システム向け 「排他制御」と「順序保証」は別物。ここを混同すると設計は破綻する。AsyncTaskはAPI 30で非推奨、今はCoroutines(短命処理)とWorkManager(永続処理)を使い分けるのが主流。Virtual Threads(JDK 21)はJVM側の話でARTは非対応。 ...

2026年8月18日 · 1 min

Androidアプリ開発の極意 2,3章あたり ANR、UIスレッド

Androidアプリ開発の極意 | 技術評論社 この本読んでる。2017年に出版してて、内容には古いものがいくつかある。 そこはAIの使いどころ。とりあえず学習したことを軽くメモして、中身について古い場合は指摘を入れたり、今は何が主流なのかといったことを補完するような指示を出した。 サポート付きの読書っぽいことをしながら読み進めて、文章をまとめた。 概要 React Native/Expoでアプリを書いていると、「なぜかカクつくが、クラッシュログには何も残っていない」という現象に何度もぶつかる。 原因を追っていくと、大体はUIスレッドを塞いでいるか、メモリを食い潰しているかのどちらかという同じ根っこに行き着くことが多い気がする。 この記事では、Androidアプリ開発の極意 | 技術評論社の3章UIスレッド/ANRの基礎から、React NativeにおけるJS-ネイティブのスレッド分離、コルーチンとasync/awaitの正体、Android/iOS/RNの非同期処理対応、そして見落とされがちなメモリ管理(OOM・LMK・Javaのメモリリーク・useEffectのクリーンアップ)までを、実務で踏んだり踏みそうな地雷も交えて整理した。 なお、私は仕事でしばしReact Native Expoを利用しているため、なるべくそちらに沿った内容に寄せて本文を作成する。 それによって本の内容とは離れることがしばしあるが、そこは予め留意してほしい。 全体像 まずスレッドの分離構造をざっくり図にする。 [Android/ネイティブ] UIスレッド(メインスレッド) ── 描画・入力処理を担当 │ │ 5秒ブロック → ANRダイアログ ▼ ワーカースレッド/コルーチン(Dispatchers.IO等) ── 重い処理はここへ逃がす [React Native/Expo] JSスレッド ── ビジネスロジック・状態更新・レンダリング計算 │ │ (ブリッジ/JSI経由) ▼ ネイティブUIスレッド ── 実際の描画 │ (任意) Worklet用の別JSコンテキスト ── react-native-worklets-core / Reanimated ポイントは、Androidのネイティブ層には「ANR」という可視化された強制終了の仕組みがあるが、React NativeのJSスレッドが詰まっても同じ仕組みは働かないという非対称性にある。以下、順番に見ていく。 UIスレッド/ANRの基礎 Androidでは、UIスレッド(メインスレッド)が入力イベントに応答できない状態が一定時間続くと、システムが強制的にANR(Application Not Responding)ダイアログを出す。Android公式ドキュメントによれば、入力イベント(キー押下や画面タップなど)に5秒以内に応答しない場合にANRがトリガーされる。これはフォアグラウンドの入力ディスパッチに関する条件で、他にもブロードキャストレシーバーやサービスの応答遅延など複数のANR条件が存在する。 一方、React Native/Expoでは事情が異なる。RNのアーキテクチャはJSスレッドとネイティブUIスレッドが分離されている。JSスレッドが重い処理で詰まっても、ネイティブ側のUIスレッド自体がブロックされているわけではないため、Android OSが検知する「入力ディスパッチのタイムアウト」という意味でのANRダイアログは出にくい。ただし、体感としては「タップしても反応がない」「アニメーションがカクつく」という、ユーザーから見ればANRと区別のつかない現象が起きる。ANRダイアログが出ないぶん、むしろ発見が遅れて厄介とも言える。 補足: 「RNならANRが原理的に起きない」という理解は不正確。JSスレッドが完全にブロックされている間、そのJSスレッドに紐づくタッチイベント処理やRedux的な状態更新も止まるため、ユーザー体験としては同種の「固まり」が発生する。OS側の検知メカニズムが働かないだけで、問題自体は消えていない。 重い処理の逃がし方:InteractionManager(旧API)とWorkletsは別物 RN側で「重い処理をUI/JSスレッドから逃がす」ための手段として、従来は InteractionManager がよく使われていたが、2026年8月リリースのReact Native 0.87で InteractionManager は削除済みであり、公式は代替として requestIdleCallback を案内している。0.82〜0.86の時点では非推奨警告が出るようになっていたため、既存プロジェクトでこの警告を見た人も多いはずだ。以降は「過去にどう動いていたか」の説明として読んでほしい。 ...

2026年8月16日 · 2 min

WSL2でExpo + E2Eテスト(MaestroとDetox)を試みて完全に詰んだ話

TL;DR WSL2環境でExpo(React Native)のE2EテストをMaestroとDetoxで試みたが、どちらもWSL2とWindowsエミュレータの構造的な問題で動かなかった。 かなり過言ではあるが、あえて感情的になるならば、Mobile開発においてMac以外は人権がない。というかあまりにもMac環境以外がだるすぎる。 環境 OS: Windows + WSL2(Ubuntu) Expo SDK 54 / React Native 0.81.5 New Architecture有効 Androidエミュレータ: Windows側で動作(Medium Phone API 36) ADB: Windows側のものをWSL2から参照 Maestroを試みる インストール curl -Ls "https://get.maestro.mobile.dev" | bash export PATH="$HOME/.maestro/bin:$PATH" ここで最初の罠。maestro --helpを叩くとAI系の全く別のCLIツールが応答した。同名の別アプリが先にPATHに入っていたため。$HOME/.maestro/binをPATHの先頭に置くことで解決。 フローの準備 # .maestro/add_and_complete_task.yml appId: com.example.myapp --- - launchApp - tapOn: text: "追加" - inputText: "テストタスク" - tapOn: text: "追加する" - assertVisible: text: "NOW" 実行して即死 You have 0 devices connected, which is not enough to run 1 shards. エミュレータはWindows側で動いており、adb devicesにはemulator-5554が見えている。しかしMaestroはWSL2側でデバイスを探すため認識できない。 --udid=emulator-5554を指定しても: Device emulator-5554 was requested, but it is not connected. maestro start-device --platform=androidを試みると: ...

2026年2月23日 · 2 min