AsyncTaskLoaderにはAsyncTaskのpublishProgress()のような途中経過を通知するメソッドが標準では用意されていません。
そこでブロードキャストを利用してこれを実装します。
Context#sendBroadcast()を使ってもいいのですが、これだと自分のアプリ外にもブロードキャストが送信されてしまうので、LocalBroadcastManagerを利用します。
AsyncTaskLoaderでの処理 AsyncTaskLoader側ではloadInBackground()内で、ブロードキャストの送信を行うだけです。
この際、途中経過のデータはIntentに埋め込んで送信する必要があります。
@Override public String loadInBackground(){ //非同期処理 Intent intent = new Intent(MainActivity.ACTION_PROGRESS) .putExtra("key", "hoge"); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } MainActivity.ACTION_PROGRESSはインテントフィルタを表す文字列です。
Activity側の実装 //インテントフィルタの定義 public static final String ACTION_PROGRESS = "jp.gcreate.sample.asynctaskloadersample.ACTION_PROGRESS"; //ブロードキャストレシーバーの作成 private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String progress = intent.getStringExtra("key"); mTextView.setText(progress); } }; @Override protected void onStart() {
super.onStart(); //ブロードキャストレシーバーの登録 LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this); manager.registerReceiver(mReceiver, new IntentFilter(ACTION_PROGRESS)); } @Override protected void onStop() { super.onStop(); //ブロードキャストレシーバーの解除 LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); } ブロードキャストレシーバーを作成して、ここでAsyncTaskLoaderから送られてくるProgressを受け取りとUIの更新処理を実装します。
後はonStart()でブロードキャストレシーバーの登録、onStop()で解除を行ってやればOKです。
細かいサンプルはGitHubにおいてます。
基本的にはここに書いてあるとおりにやればいいだけの話です。
準備 /app/build.gradleのdependenciesにjunitを追加します。
testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.9.5' この際に注意が必要なのは、androidTestCompileとtestCompileは別物であるということです。
何が別物かというと、テストコードを配置するディレクトリがそれぞれ違います。
その名の通りandroidTestCompileはandroidTestディレクトリに配置したテストコードのコンパイル時にだけ使うライブラリです。同じくtestCompileはtestディレクトリに配置したテストコードのみに使われるライブラリになります。
なお、androidTestディレクトリは自動的に作成されていますが、testディレクトリは自分で作らなければなりません。(ディレクトリは/app/src/test/java/パッケージ名にしてやればOK)
androidTestとtestの切り替え Build Variantsウィンドウを開くと、Test Artifactという欄があります。
Android Instrumentation Testsを選択していると、androidTestディレクトリ以下にあるテストコードが有効化されます。有効化されるというのが適切なのかは分かりませんが、Android Studioからコンパイル対象のソースコードであると認識されます。
Unit Testsに切り替えると、testディレクトリが有効化されます。試しに切り替えてみると、androidTestディレクトリの色が変わって、テストコードのアイコンに赤いJアイコンが出るようになると思います。
IDE上からテストを実行しようと思うと、このBuild Variantsをいちいち切り替えないといけないのが面倒くさいかもしれません。
しかし、ターミナルからGradleを使って実行する場合は、このTest Artifactsの切り替えはしなくてもいいみたいです。Gradleからテストを実行する場合、./gradlew connectedAndroidTestがAndroid Instrumentation Testsを、./gradlew testがUnit Testsを選択してテストを実行するのと同じになります。この場合のテスト結果は/app/build/reports/testsの中に出力されます。
テストコードはViewやActivityなどのUIに関するテストをandroidTestディレクトリに、純粋なJavaコードのテストはtestディレクトリに置くように工夫すべきでしょう。そうしてやれば、ユニットテストにかかる時間を削減できて幸せになれると思います。
Android Studio 1.2でEspresso2.1を使ったUIテストをやってみました。
テストの実行に実機(エミュレータ)が必要なのが面倒くさいですが、実機無しでテストが実行できるようにする方が面倒くさい(というかやり方がわからない)ので、テストができるだけマシだと考えることにしました。
テストを実行する際に、開発者オプションでアニメーションの無効化をしておかないと、アニメーションのせいで同じテストが失敗することがあるのは注意が必要かもしれません。
しかしながら、Android Support Libraryに入っているおかげでテストを実行するまでのハードルが低いのはうれしいところです。
また、EspressoによるUIテストの書き方も非常にシンプルで分かりやすいと思います。
準備 詳しい手順はここに書いてあるとおりです。Get started – android-test-kit
まずは/app/build.gradleにテストで利用するライブラリを追加します。
dependencies {
// Testing-only dependencies
androidTestCompile 'com.android.support.test:runner:0.2'
androidTestCompile 'com.android.support.test:rules:0.2'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'
} /app/build.gradleにtestInstrumentationRunnerを追記します。場所はdefaultConfigの中です。
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
更に以下のおまじないも追加します。
packagingOptions { exclude 'LICENSE.txt' } 最終的な/app/build.gradle 適宜読み替えて使ってください。
apply plugin: 'com.android.application'
android {
compileSdkVersion 22 buildToolsVersion "22.0.1" defaultConfig { applicationId "jp.gcreate.product.developersettingsshortcut"
minSdkVersion 15 targetSdkVersion 22
versionCode 1
versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } packagingOptions {
exclude 'LICENSE.txt' } } dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.1.1'
compile 'com.jakewharton:butterknife:6.1.0'
// Testing-only dependencies androidTestCompile 'com.android.support.test:runner:0.2'
androidTestCompile 'com.android.support.test:rules:0.2'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1' } 一時的なバグ? Warning:Conflict with dependency 'com.
LinearLayoutをネストしすぎたりするなど、Viewの階層を深くするとアプリのパフォーマンスに良くないという話はよく聞くと思います。
それと似たような話で、画面を何回描画しているかを確認して、アプリのパフォーマンスに役立てることができきます。今回はそれの紹介です。
確認の仕方 端末の開発者オプションで「GPUオーバードローをデバッグ」を有効にします。
これを有効にすると、目に悪そうな色で画面が表示されるようになります。
この各色が、GPUによって何回上書き描画されているのかを示しています。
青色:1回 緑色:2回 薄赤:3回 濃赤:4回以上 この状態で画面が真っ赤っ赤だと、描画方法を改善した方がいいぞということになります。
対策 例えばFrameLayoutでbackgroundDrawableを持ったViewを何個も重ねていくと、見えているのは一番上のものだけなのに、見えない下の要素まで描画するため上書き回数が増えて赤色になってしまいます。
そのため不要なbackgroundDrawableを描画しないようにすることが、この問題の対策になります。
例えばActivityでgetWindow().setBackgroundDrawable(null)とするだけでも画面の赤色が薄くなると思います。(ただし、これをやるとListViewやGridViewなど、スクロールをともなうViewの描画がおかしくなります)
重ねて描画せざるをえない場合は、canvas.clipRectを使って重なって見えない部分を描画しないようにすることで対応できるようです。
効果 ムダな描画回数を減らすことにつながるので、その分アプリの動きが軽快になるでしょう。
さらにバッテリーにも優しくなると思います。
ただし、アプリのもっさり感解消のための施策としては、優先度は低いのかなと思います。ちまたに出ているアプリでも、割と真っ赤なアプリが多いですし、赤くとも動作がもっさりしているものは少ない印象です。
やらないよりやった方がマシでしょうが、ここを気にするより、メモリの使用量を抑えるといったチューニングの方が、アプリのパフォーマンスにとって効果が高いような気がします。
Android Performance この話はUDACITYのAndroid Performanceという動画を見て知りました。
英語オンリーかつ字幕すらありませんが、大体雰囲気で分かるんじゃないかなと思います。
Android Performance – UDACITY
「このアプリのデザインを参考にしたいんだけど、どうやって作ってるのか知りたい」というときに便利かもしれないコマンドです。
調べたい画面を表示させた状態で、ターミナルからadb shell dumpsys activity topと入力すると、現在表示中のView階層などが表示されます。
View階層だけを調べたいなら、hierarchyviewerを使った方がグラフィカルに見えて便利なのですが、hierarchyviewerはroot権限がないと起動しないので、実機で調べたい画面を表示して解析することができません。
その点、このadb shell dumpsys activity topはroot権限を必要としないので、実機でちょっと調べたいという時に便利だと思います。
どこからどこまでがActionBarの領域で、どこがコンテンツの領域なのかが非常に分かりづらいのですが、Viewに割り振られているIDも一緒に表示されるのである程度把握できると思います。
このIDが表示されるのを利用して、View階層の中でIDの衝突が起こっていないかなんてことを調べるのにも便利かもしれません。
ちなみにadb shell dumpsys activityと最後のtopを省略すると、Activity Managerの情報がズラズラと表示されます。
Broad castがどうなってるかとか、Content Providerがどんなのが動いているかとか、どんなServiceが動いているかとか、スタックがどうなってるかとかが出力されます。
実機でコマンドを打つだけで調べられるので、手軽で便利だと思います。
カスタムViewを作った場合、BaseSaveStateを拡張してViewの状態をカスタムView自身で復元できるようにできます。
この際に注意すべきことが3点あります。
Activityを保持しないを有効にしてチェックする カスタムViewの復元機能を実装したら、必ず開発者オプションのActivityを保持しないを有効にしてちゃんどう動くかどうか確認しましょう。
自分ではちゃんと実装したつもりでも、これを有効にした状態で画面回転させるとアプリが落ちる場合があります。
フィールド名のタイポに注意 BaseSaveStateを拡張したクラスには、必ずpublic static final Parcelable.Creator<BaseSaveStateを拡張したクラス名> CREATORというフィールドが必要です。
このフィールドの名前はCREATORでなければなりません。
CREATERとタイポすると動きません。動かない上にエラーメッセージはjava.lang.RuntimeException: Unable to start activity ComponentInfo{jp.gcreate.sample.savestatecustomview/jp.gcreate.sample.savestatecustomview.MainActivity2Activity}: java.lang.RuntimeException: Parcel android.os.Parcel@18c09797: Unmarshalling unknown type code 2131296303 at offset 264のように、「フィールド名が違います」と教えてくれません。
writeToParcelで書き出す順番 writeToParcelで書き出す順番とコンストラクタで読み出す順番は同じ順番にしなければなりません。
書き出す順番と読み出す順番が異なるとうまく復元することができません。
順番を同じにすることと一緒に忘れていけないのは、最初にsuperを呼び出すことです。
public ImageState(Parcel source) {
super(source);
savedUri = source.readParcelable(Uri.class.getClassLoader());
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(savedUri, flags);
}
``` コンストラクタで`super(source)`を最初に呼び出す、`writeToParcel`の最初で`super.writeToParcel(dest, flags)`を呼び出すことも忘れてはいけません。 単純なことですが、エラーメッセージからどこが悪いのか把握しづらいので、知らないとドはまりするので注意しましょう。 ## サンプル public class UriImageView extends ImageView{
private Uri mUri;
public UriImageView(Context context, AttributeSet attrs) {
super(context, attrs);
setImage();
}
private void setImage() {
if(mUri == null){
setImageDrawable(getContext().getResources().getDrawable(android.R.drawable.btn_star, getContext().getTheme()));
}else{
setImageURI(mUri);
}
}
public void setUri(Uri uri) {
mUri = uri;
setImage();
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.
Logcatでアプリのデバッグをする際に、自分のアプリからのログ出力だけ見たいなんてときありますよね。
そんなときにどうやってフィルタリングをするかという話です。
キーワードやログレベルでのフィルタリングはここでできます。
ちなみにログレベルでのフィルタリングは、指定したレベル以下のものを出力するというフィルタ設定になります。
例えばここでInfoを選ぶとInfo以下のものだけが出力されるようになり、VerboseとDebugレベルのログは表示されなくなります。
ログレベルは下図で囲った部分です。ログレベル/タグという形式で出力されています。
特定のアプリからのログ出力だけ見たい場合は、Show only selected applicationのところをクリックして、Edit Filter Configrationを選びます。
ここでPackage Nameのところで調べたいアプリのパッケージ名を指定してやると、自分のアプリからの出力しか表示されなくなります。
こういったフィルタリングの設定をうまく使えば、アプリ開発も捗ると思います。
一方でLogcatにこれらのフィルタ設定をしているときには注意しなければならないことがあります。それは、アプリが落ちた時のスタックトレースまでフィルタされて表示されないことがあるということです。
「アプリが落ちたのにLogcatに何も表示されない・・・」と混乱しないようにしましょう。
前回の続きでAsyncTaskLoaderを使ったサンプルを作って、Loaderの動きを確認していたのですが、1つの問題点にぶち当たりました。
initLoaderでLoaderを動かす分にはとてもスッキリしたのですが、restartLoaderを使うと非同期処理がイメージ通りに動きませんでした。
それは以前の非同期処理が終わらないと、restartLoaderで新しく動かす非同期処理が始まらないということです。
私が作ったサンプルでは、指定した数字までカウントアップを行う非同期処理をしています。しかし非同期処理中にrestartLoaderを呼び出すと、今動いている非同期処理が終わらないと新しい非同期処理が動いてくれないのです。
restartLoaderを呼んだら今動いている非同期処理には停止してもらい、すぐに新しい非同期処理が始まって欲しいです。使いもしない非同期処理の終了を待つのは時間の無駄ですし、使いもしない処理にリソースを割くのももったいないです。 ## ソースコードを読んで分かったこと
現在進行形で格闘しているので、まとまっていないですがこんな感じ。
onStopLoadingはActivityがonStopになったときに呼ばれる(画面回転時は除く) キャンセルの処理(restartLoader実行時)は、まずonCancelLoadが呼ばれる AsyncTaskLoader.onCancelLoadでLoaderの状態に合わせてキャンセル処理を行う 実際のキャンセル処理はcancelLoadInBackgroundメソッドで行われる しかしAsyncTaskLoader.cancelLoadInBackgroundでは何もしていない すなわち実際にloadInBackgroundの処理を止めるのは自分で実装しなければならない AsyncTaskLoader.onCancelLoadを経ていれば、loadInBackgroundの処理結果は最終的にonCanceledに通知される LoaderManagerがうまいこと管理してくれているので、restartLoader呼んだ数だけ非同期処理が乱立するわけではない(それでもいくつか並行して走るけれども) そもそもLoaderManagerは何している? LoaderManagerはrestartLoaderが呼ばれた時に何をしているのかも、同時進行で読み解いています。
LoaderManagerはLoaderをmLoadersとmInactiveLoadersという2つのリストで管理しています。
mLoadersでは現在実行中のLoaderを、mInactiveLoadersでは以前実行されていたLoaderを管理しています。mInactiveLoadersはLoaderを破棄するためのもののようです。おそらく。
restartLoaderをすると、LoaderManagerはLoaderの状態によってあれやこれやしながら新しいLoaderを作成します。現在実行中のLoaderがあればキャンセル処理を行いますが、新しいタスクはmPendingLoaderに登録します。
mPendingLoaderが何者かというと、その名が表すように次に実行される非同期処理のタスク(Loader)です。このmPendingLoaderがいつ実行されるのかというと、今実行されているタスクのloadInBackgroundが終了した時です。 そのため実行中のタスクが終わらないと、restartLoaderで作られた新しいタスクが始まらないのです。
AsyncTaskLoader上のキャンセル処理 Loaderの非同期処理が実行されているときにキャンセルがかかると、以下の場合にonCanceledが呼ばれます。
Loaderが非同期処理実行中の間に、Activity等でinitLoader().forceLoad()をしたとき Activity等でrestartLoaderを呼んだ時(非同期処理が実行中かは問わない) AsyncTaskLoader.onCancelLoad()でキャンセル関連の処理が行われているためsuper.onCancelLoad()を呼ぶ必要があります。
ただしやってるのはLoaderの管理情報の更新だけで、実行中のloadInBackgroundを止めるような処理は何もしていません。
具体的には、、現在実行中の非同期処理があるか確認(mTask != null)し、タスクがなければキャンセル対象がないので何もしません。
ある場合には、キャンセル処理中の非同期処理があるかを確認します(mCancellingTask != null)。
mCancellingTaskがある場合、onCancelLoadが呼び出されたLoaderがPendingTaskなら破棄します(mTask.waiting == true)。これは実行待ち状態のLoaderをキャンセルすることを意味しています。実行待ちのタスクはまだ開始されてないから破棄するだけでいいわけです。
mCancellingTaskがない場合は、onCancelLoadが呼び出されたLoaderがPendingTaskか確認します。上と同じことをやっていますが、mCancellingTaskがない場合、このLoaderをキャンセルされたタスクとして退避させる必要があるので条件分岐されてます。
で、PendingTaskであればLoaderをそのまま破棄します。まだ非同期処理が始まっていないのでそのまま破棄するだけでいいからです。
PendingTaskでないのであれば、このLoaderは現在稼働中の非同期処理ということになります。そこでこれをキャンセルし、mCancellingTaskへと退避します。その上でcancelLoadInBackgroundを呼び出します。
そのため、Loaderをキャンセルするための処理は、cancelLoadInBackgroundで実装すればいいことになります。
cancelLoadInBackgroundで何をすればいいか Loaderをキャンセルするための処理を実装するといっても、具体的にどう実装すればいいかというとよく分かりません。
このメソッドの中からloadInBackgroundの処理を停止させることはできないでしょう。むしろこのメソッドは、メインスレッド(Activityとか)からLoaderを停止させるためのメソッドのような気がします。
しかし直接ActivityからこのcancelLoadInBackgroundを呼ぶと、LoaderManagerの管理下から外れた動きをすることになって、変なことになりそうな気がします。
結局のところ、loadInBackgroundの中でisLoadInBackgroundCancelled(Loaderがキャンセルされたらtrueになる)をチェックして非同期処理を途中で止めるように実装するしかなさそうです。
新たな謎 今気づいたんですが、ActivityからLoaderのforceLoadを呼んだ後でrestartLoaderすると、前の非同期処理完了を待つことなくrestartLoaderした処理が走っていることに気づきました。前の処理は走ったままなので、2つの非同期処理が並列で走ってますけども。
この違いはいったいどこからやってくるのか・・・。
ちなみにサンプルはGitHubで公開中です。
重い腰をあげてLoader触ってみました。
これまでもAsyncTaskはやめろ、Loader(AsyncTaskLoader)使えっていう話は知ってはいたんですが、Loader使い方よく分からんって敬遠してたんですよね。
とりあえずAsyncTaskLoaderを使ってみてわかったこと、感じたことを書いてみたいと思います。
参考にしたところ
AsyncTaskLoader – Android Developers 非同期処理 – mixi-inc/AndroidTraning サンプルはGitHubに上げてます。
未だによく分かってないところもあるんですが(キャンセル処理についてはまだ手を付けていない)、とりあえず現状で分かったことを書いてまとめます。
ちなみにソースコード読んで動きを把握したいなら、サポートパッケージではなくandroid.app.LoaderManager、android.content.Loaderを使った方がいいと思います。
LoaderManagerの動きを知る getLoaderManager.initLoaderを呼ぶより前に、LoaderManager.enableDebugLogging(true);を実行すると、LoaderManagerがログを出力してくれるようになるので便利。
LoaderManagerがLoaderの状態を管理しているので、ActivityやAsyncTaskLoaderは非同期処理がどんな状態にあるのか気にしなくて済むのがいいですね。
ただしちゃんと動くようにするためには、Loder側でどういう状態の時にどのメソッドが呼び出されるのかを理解しておく必要があります。そのためにはLoaderManagerの動きを知っておかないとわけが分からないというわけです。
ついでに言うと、メソッド名から想定したイメージと実際の動きの間が、私の感覚と違っていて余計に混乱したというのもあります。
getLoaderManager().initLoader getLoaderManager(もしくはgetSupportLoaderManager).initLoaderは指定したIDのLoaderがなければLoaderを作成、既に存在していればActivityへのCallbackを設定します。
Loaderを初期化するメソッドというより、コールバックを更新するものと思った方が理解しやすい気がします。私はずっとこのメソッドでLoader作って非同期処理を開始するものだとばかり思っていて、ずっと混乱していました。
指定されたIDのLoaderがまだ存在しない場合は、ActivityのonCreateLoaderにコールバックを行い、ここでLoaderを作ります。
Loaderを作るのはActivityのお仕事です。initLoaderだけなら、onCreateLoaderが呼ばれるのはActivityがonCreateされたとき(画面回転時は除く)だけです。
Loader.onReset 名前からして再稼働させた時に呼ばれるのかと思って混乱しました。いまだによく分かっていません。
onResetが呼ばれたLoaderは再利用されることはない・・・であってると思うんですけど、自信がありません。
LoaderManagerは、LoaderのIDごとに現在動いているLoader、以前に使ってたLoaderを管理しているだけなのようです。だから以前使ったLoaderのインスタンスを再活用したりはしてないと思います。
Loaderで使ってたリソースを解放してねってタイミングのようです。
バックグラウンド処理を引き継げる とりあえずやってみて感じたのは、AsyncTaskと違ってバックグラウンドの処理を引き継げることがいいなと感じました。
AsyncTaskだと画面回転したらまた最初からやり直しになってたものが、そのままバックグラウンド処理は続いてくれるし、結果もそのまま受け取れるのが素敵。
Loader側でキャッシュ機構を持たせることで、ムダな非同期処理を防ぐことができるのもいいなと思います。
Loader側が非同期処理だけに専念できる AsyncTaskと違って呼び出し元のActivity(Fragment)が生きてるかどうかを確認しなくていいのが想像以上にやりやすいです。
AsyncTaskだとonPostExecuteで処理結果をUIに反映します。バックグラウンド処理をしている最中に画面回転が生じるとUI更新しようとするものの、対象のActivityは既にお亡くなりになっているせいでアプリが落ちてました。
LoaderではUIの更新について何1つ考える必要がないので、とてもスッキリします。
でも途中経過を伝えられない AsyncTaskLoaderには途中経過を通知するメソッドが標準で用意されていません。そのためバックグラウンド処理の途中経過を表示することができません。同じAsyncTaskがつくのに別物と
対策としてはAsyncTaskLoaderにActivityへの参照を持たせて通知するみたいなやり方がありましたけど、そんなことするとLoader使う意味が無い気がします。せっかくの非同期処理とUI処理を分離するための機構が台無しです。
LocalBroadcastで対応するっていう策も見つけましたが、こっちの方がまだましかなと思います。
しかし、いっそのことプログレス表示しないという選択肢もありなのかもしれません。データが全部で10件あって、そのうち◯件処理済みとか表示するのではなく、データ読み込み中ですよって表示するだけ。そういう方向での切り替えをした方が通知のための仕組みを実装するより安全な気がします。
Android Studioでコーディングしていて、「このメソッドどういう処理するんだっけ?」と思った時に便利なQUICK DOCUMENTATIONがあります。
F1押すと出てきますが、標準だとFloating Modeで表示されて邪魔です(設定によって違うかもしれませんけど)。
これは右上の歯車マークをクリックして、Floating Modeを押して解除してやれば他の枠に収まってくれます。
私の場合毎回右側にDocumentationが収まるのですが、右側は個人的に見づらい。このDocumentationはドラッグすることで表示位置を移動することができます。
個人的には右下に置いておくのが好みです。
こうすることでQuick Documentation機能がぐっと使いやすくなります。
これでめでたしめでたしなんですが、このままではDocumentationの表示位置は現在開いているプロジェクト固有の設定にしかなりません。
つまり、別のプロジェクトを作成してQuick Documentationを利用した時に、再びFloating Modeで開かれてしまいます。プロジェクトごとに毎回この設定をするのは面倒なので、これを標準のレイアウトとして設定してしまいましょう。
方法はAndroid StudioのWindowメニュー→Store Current Layout as Defaultを選べばOKです。
これで毎回設定せずともすみます。