google-code-prettify

2015-06-23

Android4.0以前と4.1以降では、親のViewをクリックした時に子のViewがどう見えるのかが異なる

Android4.0以前と4.1以降では、親のViewをクリックした時に子のViewがどう見えるのかが異なるので注意が必要。

以下のようなViewがあったとする。



Clickできる箇所は、外側の白い部分(下図のA)と、上の水色の部分(下図のB)。残りはnon clickable。すべてのViewのbackgroundDrawableは、stateful(pressedステートだと色が暗くなる)とする。



Android 4.1以降では、子にもpressedステートが伝わる


Android4.1以降でAをクリックした場合は、Aの領域すべてが暗くなる(子にもpressedステートが伝わる)。ただしclickableなBには伝わらない。



Android4.1以降でBをクリックした場合は、Bの領域が暗くなる(Bの中にあるnon clickableな子にもpressedステートが伝わっている)。



Android 4.0以前では、子にpressedステートが伝わらない


一方、Android4.0でAをクリックした場合。子にpressedステートが伝わっていないので、子の色が変わらない。



Android4.0でBをクリックした場合。同様にBの中にある子の色は変わっていない。



なぜ動作が違うか


おそらく関係しているのはView.javaのこのcommit

変更後は、ACTION_DOWNでsetPressed()が呼ばれるようになった。setPressed()が呼ばれるとdispatchSetPressed()が呼ばれ、そこから子のViewのsetPressed()が呼ばれる。




2015-06-10

Activityの再生成にまつわる処理を書くベストな方法

AndroidでActivityの再生成にまつわる問題をちゃんと解決しようとすると、非常に煩雑なコードになる。

その煩雑さを解消するクラスを作った。今の所、この方法がベストな方法だと思う。
Retain.java

以下、Activityの再生成に関する典型的な対処法とその問題点。

Activityの再生成のパターンは2つある


Activityは、以下の場合に再生成される。
  1. Configuration changeの時(画面の回転など)。プロセスは生きたまま。
  2. バックグラウンドにいる間にOSによってプロセスごと殺された後、アプリに戻ってきた時。
再生成後の新しいinstanceにデータを渡したいときにどう渡すかが問題となる。

方法1. onSaveInstanceStateでbundleに入れる

  • processが死んでいた場合でも、データが残っている。
  • Bundleに入る型しか保持できない(Integer, Boolean, String, Serializableなど)。
    例えば、通信処理スレッドを再生成後のinstanceに引き渡す、ということは直接はできない。
  • onSaveInstanceState()が呼ばれた後には、もうbundleに入れることができない。

方法2. staticな領域に覚えておく

  • processが死んでいない場合は、データは残っている。
  • processが死んでいた場合は、データは残っていない。
  • Activityが2つ存在するときに、間違って別のActivityにデータを渡さないようにしなくてはならない。

方法3. retain用のFragmentを使う

setRetainInstance(true)なFragmentを用意して、そのretain用Fragmentにデータを覚えさせておく方法。 http://developer.android.com/guide/topics/resources/runtime-changes.html
  • processが死んでいない場合は、データは残っている。
  • processが死んでいた場合は、データは残っていない。
  • Activityに紐付いたデータ(ThreadにおけるThread Local Storageのように)なので、別のActivityに間違ってデータを渡してしまうことはない。
  • Activityがいなくなると、自動でFragmentも消してくれる。
方法3の問題点
  • あくまでActivityに結びついた情報なので、Activityに含まれるFragmentごとに別々のretain用fragmentをもたせる、と言ったことは非常にしにくい。
  • retain用Fragmentを削除するタイミングが難しい。下手なタイミングでFragmentTransactionをすると、IllegalStateExceptionが出る。
  • FragmentActivity, Fragmentを使わなくてはならない。
  • コードが読みにくい

解決方法

  • processが死んだ場合でも、できるだけ情報を残したい、
  • でも、bundleに入る情報以外も保持したい
  • 3の方法は嫌だ
となると結局1と2のハイブリッドな方法で行くしか無い。 1,2の問題点を解消しつつ、1も2も統一的な方法で扱えるためのクラスを作った。 Retain.java

使い方

private Retain<somedata> retain; // SomeDataは、ParcelableまたはSerializableであること

@Override
protected void onCreate(Bundle savedInstanceState) {
    retain = Retain.forSerializable()
    someData = retain.onCreate(savedInstanceState);
    if (someData == null) {
        someData = new SomeData()
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    retain.onSaveInstanceState(outState, someData);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    retain.onDestroy(this);
}

これだけで、someDataの情報はできるだけ保持されるし、画面回転等にも対処できる。

2015-05-20

AndroidのcompileSdkVersionとかbuildToolsVersionとか

build.gradleに指定する、compileSdkVersionやtargetSdkVersionなどの説明。

compileSdkVersion

Javaのソースをコンパイルするときに使うSDKのバージョン。新しいSDKではAPIやandroid.R.id.xxxの定義が追加されていたりする。

例えば、ViewのrequestUnfufferedDispatchはAPIレベル21で追加されているので、このAPIを使う場合にはcompileSdkVersionは21以上にする必要がある。

buildToolsVersion

ビルドに使うツールチェイン(aapt/dxなど)のバージョン。上位互換なので、最新バージョンを追っていってもあまり問題はない。

SDKバージョンと連動していて、あるバージョンのSDKがリリースされると、一緒に対応する.0.0のバージョンのbuildToolsがリリースされる。例えばAPI 20がリリースされたときには、20.0.0のbuildToolsがリリースされた。

compileSdkVersionの値以上にする必要がある。

minSdkVersion

このバージョン未満の端末には、アプリをインストールできなくなる。アプリの動作には影響はない。

targetSdkVersion

targetという意味が分かりにくいが、どのバージョンの端末を念頭に置いて開発されたかを表す。この値を変えると、アプリの見た目や挙動が変わるので注意が必要。

例えば、targetSdkVersionが19(Kitkat)の場合、
  • 端末バージョンが19以下の場合、特に影響はない。
  • 端末バージョンが20(Lollipop)以上の場合には、19互換モードでアプリが動く。アプリの見た目は19の見た目になり、20で新たに導入された挙動はOFFになる。

targetSdkVersionで挙動が変わる例:
  • 19以上の端末でtargetSdkVersionを19以上にすると、AlarmManagerのset()でセットしたアラームのなるタイミングは若干不正確になる(バッテリ消費を抑えるため)
  • 14以上の端末でtargetSdkVersionを14以上にすると、hardware accelerationがデフォルトでONになる。
  • 10以上の端末でtargetSdkVersionを10以上にすると、UIスレッドでネットワークアクセスできなくなる。
公式ドキュメントのtargetSdkVersionには、テストした端末のバージョンを書けと書いてあるが、まさにその通り。20の端末でしかテストしていないのにtargetSdkVersionを21にすると、21の端末で動かない可能性がある。




2014-05-05

dx.jarをfastdx.jarに入れ替えて、Androidのビルドを50%くらい速くする

Androidのアプリのビルドは非常に遅い。原因の一つはGradleが重いことなのだが、実はDEXとPRE-DEXのフェーズも無駄な処理を行なっているため、非常に時間がかかる。
そこで、DEXとPRE-DEXを行なっているdx.jarの高速版のfastdx.jarを作成した。全体のビルド時間は、私の環境では50%くらい高速化された。

fastdx.jarのダウンロードはこちら

オリジナルのdx.jarが遅い原因は、大きく2つある。

  1. 無駄に.classから.dexの作成を行なっている(キャッシュしていない)
  2. 複数のdexのマージの処理に無駄がある

無駄に.classから.dexの作成を行なっている(キャッシュしていない)


オリジナルのdx.jarでは、.jarに含まれていた.classが一つでも変更された場合、.jarに含まれていた全.classファイルを再度.dexにコンバートし直している。
例えば、.jarの中身が以下の3つの.classを含んでいたとする。

a.class
b.class
c.class

a.classが変更された場合、dx.jarはa.dexのみならず、b.dex, c.dexも再度生成する。このコンバートに結構時間がかかる。

fastdx.jarでは、コンバート済みの.dexをキャッシュすることで、無駄に.dexへのコンバートが発生しないようにした。

複数のdexのマージの処理に無駄がある


複数のdexをマージする際、例えばa.dexからd.dexまでをマージする際、オリジナルのdx.jarは以下のようにdexをひとつずつマージする。
a.dex + b.dex = ab.dex
ab.dex + c.dex = abc.dex
abc.dex + d.dex = abdc.dex
dexファイルをいちいち生成、再読み込み、を行うため、非常に時間がかかる。

fastdx.jarでは、マージを一気に行う。
a.dex + b.dex + c.dex + d.dex = abcd.dex

このため無駄なdexの生成と読み込みが無くなり、高速化されている。



2014-04-24

AndroidのGradleでのビルドが遅いのをちょっと速くする

Gradleを使ってAndroidアプリのビルドをしていると、あまりのビルドの遅さについついGradleに移行したことを後悔してしまいそうになる。
遅い原因を調べて、2つほどビルドを速くするポイントを発見した。
  • 実はGradleデーモンが使われていない問題
  • 実はlibrary projectはReleaseとDebugの両方がビルドされている問題
以下、IntelliJ 13.1.2での症状&対処法だが、Android Studioでも同じ方法で速くなると思われる

前者は、Android Studio 0.6.0で直ったようです。


実はGradleデーモンが使われていない問題


.gradle/gradle.propertiesに
org.gradle.daemon=true
と書いておけばGradleデーモンが使われるので立ち上がりが速くなる、というのはあちこちに書いてあるが、実はGradleデーモンが使われていない可能性がある。

その理由は2つ。
  • IntelliJはGradleデーモンがいない場合にはGradleデーモンを起動するが、アイドルタイムアウトが60秒(固定値)。そのため、次にビルドするときにはGradleデーモンが既に死んでいる。
  • 手動でGradleデーモンを立ち上げたとしても、IntelliJが起動しようとするGradleとあまりに引数が違うと、新たに別のGradleデーモンが(timeout60秒で)起動する。
Gradleデーモンの引数やアイドルタイムアウトは、ps all wwで確認できる。
0  1000 10170     1  20   0 2124512 1254752 futex_ Sl pts/3    11:15 /usr/lib/jvm/java-6-oracle/bin/java -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8 -cp /home/sakurai/.gradle/wrapper/dists/gradle-1.11-all/7qd8qq8te5j4f5q9aaei3gh3lj/gradle-1.11/lib/gradle-launcher-1.11.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.11 /home/sakurai/.gradle/daemon 10800000 7e2c5ad5-a731-455d-a37b-699b5c251ea6 -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8
IntelliJでのビルド直後にps all wwで、timeoutの値をチェックしてみると良い。数値が60000の場合には、60秒で死ぬデーモンが使われている。タイムアウトが長いデーモンを使うには、手動でGradleを立ち上げるしか無い。

また、Gradleの引数が一致していない場合には、IntelliJのSettings->Gradle->Gradle VM Optionsの値を、手動で起動したGradleと合わせておく必要がある。私の場合には、以下を設定してある。
-XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m

実はlibrary projectはReleaseとDebugの両方がビルドされている問題


Build VariantsをDebugにしていると、library projectはReleaseとDebugの両方がビルドされている(library以外は関係ない)。

現状、Debugビルドをしていても、必ずReleaseビルドのlibraryが使われてしまうというバグがある。
Gradle plugin does not propagate debug/release to dependencies

このバグの副作用として、アプリをDebugビルドした場合に、library projectはReleaseとDebugの両方がビルドされてしまうようだ。

Gradleの出力例: (DebugもReleaseもビルドされている)
:ActionBarSherlock:preReleaseBuild
:ActionBarSherlock:checkReleaseManifest
...
:ActionBarSherlock:preBuild
:ActionBarSherlock:preDebugBuild

どうせReleaseビルドしか使われないのだから、最初からlibraryはReleaseビルドしかしないようにしてしまえばよい。

IntelliJのウィンドウの左下のBuild Variantをクリックし、libraryプロジェクトのBuild Variantは全てReleaseに変えておく。

2013-11-19

ある特定の文字列をTAGにすると、Log.xが出力されない

Androidのandroid.util.Logで、ある特定の文字列をTAGにした場合には、ログが出力されない。

ログが出力されないTAGは、以下の通り。
  • HTC_RIL
  • RILで始まる文字列
  • AT
  • GSM
  • STK
  • CDMA
  • PHONE
  • SMS
電話番号を扱うアプリを作っていてTAGを"PHONE"にしていたところ、ログに何も出力されずに悩んでしまった。

参考:
logd_write.c

2013-06-18

Androidアプリのテストに役立つサービス

Androidアプリのテストに役立つサービス。

Samsung Remote Test Lab

サムスンの端末をリモートで操作でき、アプリのインストールやテストが行える。サムスンの機種がいろいろそろっている。無料。

Apkudo

apkをアップロードすると、多数(250くらい)の端末上でのmonkeyテストを自動で行なって結果を表示してくれるサービス。無料。日本の端末はあまり用意されていない。

Remote TestKit

端末をリモートで操作でき、アプリのインストールやテストが行える。日本の端末が豊富に用意されているので、日本向けのアプリを出す際には便利なサービス。
以前とある端末でのみ落ちるバグがあり、その原因を調べるときに同じ作者のリモート・スマホ・レンタルにお世話になった。

Linux版のクライアントが無いため私は試せていないが、デバッガでリモートデバッグができたり、adb shellが動作したりもするらしい。
1時間945円ほど。