Android Data Binding Tips

これはAndroid Advent Calendar 2016の10日目の記事です。

はじめに

Data Binding使ってますか?便利ですよね。私も個人でも仕事でも使っています。

今回の記事ではData BindingのTipsをいくつか紹介したいと思います。

カスタムバインディングを定義する

Data Bindingはデータオブジェクトの値をViewに反映するためのものですが、標準で用意されているバインディングだけではちょっと物足りません。例えば、GlideやPicassoを使ってImageViewに画像を表示するようなユースケースでは、カスタムバインディングを定義する必要があります。

Data Bindingでは以下のようにBindingAdapterを使ってカスタムバインディングを定義します。

public class CustomBinder {
    @BindingAdapter("app:imageUrl")
    public static void imageUrl(ImageView imageView, String url) {
        Glide.with(imageView.getContext()).load(url).into(imageView);
    }
}

上記のようなカスタムバインディングを定義した上で、XMLで以下のようなバインディングを行います。

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:imageUrl="@{article.thumbnail()}"/>

今回はImageViewに対して画像URLをバインドする方法を紹介しましたが、これを応用すれば基本的には何でもバインドすることが出来ます。が、あまりにアクロバティックなバインディングを行うと後からプロジェクトにジョインした人が困るので、用法用量を守って使いましょう。

ちなみにカスタムバインディングの悪用事例は以下の記事で紹介されています。

includeでデータオブジェクトを渡す

レイアウトの再利用を目的として別のレイアウトファイルをincludeすることがあると思います。Data Bindingではincludeでデータオブジェクトを渡すことが出来ます。

まずは、includeされる側で以下のようにデータオブジェクトの定義を書きます。

<layout>
    <data>
        <variable
            name="article"
            type="com.yuyakaido.android.flow.domain.entity.Article"/>
    </data>

    (省略)
</layout>

次に、includeする側でデータオブジェクトを渡す処理を書きます。

<include
    layout="@layout/include_item_article"
    app:article="@{article}"/>

おわりに

今回は以下の2つのTipsを紹介しました。

どちらもある程度な規模のアプリを開発する上で必要になってくるかなと思います。

それでは、良いData Bindingライフを!

KotlinとData Bindingを組み合わせた場合の問題点と解決法

これはKotlin Advent Calendar 2016の3日目の記事です。

はじめに

KotlinとData Binding、両方とも便利ですよね。

この記事ではこれらを組み合わせて使った場合に発生するいくつかの問題点と解決法を紹介したいと思います。ググるといくつか情報が出てきますが、断片的であったり新しい解決法が登場していたりするので、情報を集約して解決法を列挙する形で書いています。

この記事では、

  1. Kotlinから自動生成されたクラスが参照できない
  2. BindingAdapterを使ったカスタムバインディングが使えない

という2つの問題を扱います。

1. Kotlinから自動生成されたクラスが参照できない

これはData Bindingに限った話ではなく、KotlinとAnnotation Processingを組み合わせた場合に発生します。

Data Bindingを使う場合、XMLで記述したレイアウトをLayoutタグで囲むことでFooBindingのようなクラスが生成されます。しかし、このFooBindingをKotlinから参照すると正常にビルドが出来ず、以下のようなビルドエラーが表示されると思います。

Error:(7, 35) Unresolved reference: databinding
Error:(19, 27) Unresolved reference: ItemArticleBinding
Error:(40, 35) Unresolved reference: ItemArticleBinding

この記事で詳しくは説明しませんが、KotlinとJavaコンパイル順序が関係しています。以下の記事で詳しい解説があるので、興味のある方はご覧ください。

この問題を解決する方法として、2016年12月現在では2つの方法があります。

generateStubsを有効にする

build.gradleに以下の記述を追加することでKotlinとJavaコンパイル順序に関連した問題が解消されて、KotlinからFooBindingのような自動生成されたクラスが参照できるようになると思います。

kapt {
    generateStubs = true
}

kotlin-kaptを使う

もう1つの解決方法として、build.gradleに以下の記述を追加することも解決できます。

apply plugin: 'kotlin-kapt'

しかし、公式ブログには以下のような記述があり、kotlin-kaptはまだ実験段階のようです。この記事を書くにあたって簡単なサンプルを実装した限りは正常に動作しましたが、まだ問題が残っている可能性もありそうです。

The new annotation processing still has known issues and may not be compatible with all annotation processors. You should enable it only if you’ve run into problems with the default kapt annotation processing implementation.

2. BindingAdapterを使ったカスタムバインディングが使えない

Data BindingではTextViewにテキストをバインドするといった標準で用意されているバインディングに加えて、カスタムバインディングを実装することができます。よくある例としては、ImageViewにGlideやPicassoなどを使ってURLをバインディングするものだと思います。これをJavaで実装すると以下のようになります。

public class CustomBinder {
    @BindingAdapter("app:imageUrl")
    public static void imageUrl(ImageView imageView, String url) {
        Glide.with(imageView.getContext()).load(url).into(imageView);
    }
}

JavaではstaticメソッドにBindingAdapterを付与することでカスタムバインディングを実装することができます。これをKotlinで実装すると以下のようになります。

object CustomBinder {
    @BindingAdapter("app:imageUrl")
    fun imageUrl(imageView: ImageView, url: String?) {
        Glide.with(imageView.context).load(url).into(imageView)
    }
}

これでいけるかと思いきや、実行すると以下のようなエラーが発生します。

java.lang.IllegalStateException: Required DataBindingComponent is null in class ItemArticleBinding. A BindingAdapter in com.yuyakaido.android.flow.presentation.binder.CustomBinder.Companion is not static and requires an object to use, retrieved from the DataBindingComponent. If you don't use an inflation method taking a DataBindingComponent, use DataBindingUtil.setDefaultComponent or make all BindingAdapter methods static.

一言で要約すると、カスタムバインディングはstaticメソッドとして実装してね、という感じです。

この問題を解決する方法はいくつも考えられますが、大きくは2つあります。

JvmStaticを付与する

Kotlinにはstaticというキーワードはありませんが、以下のようにJvmStaticを付与すると、Javaからはstaticメソッドとして見えるようになります。

object CustomBinder {
    @JvmStatic
    @BindingAdapter("app:imageUrl")
    fun imageUrl(imageView: ImageView, url: String?) {
        Glide.with(imageView.context).load(url).into(imageView)
    }
}

これでめでたくカスタムバインディングが動作するはずです。

カスタムバインディングを拡張関数として実装する

もっとKotlinらしく書くとしたら、カスタムバインディングを拡張関数として実装してもいいかもしれません。

@BindingAdapter("android:imageUrl")
fun ImageView.imageUrl(url: String?) {
    Glide.with(context).load(url).into(this)
}

上記のようにImageViewにimageUrlという拡張関数を実装すると、以下のようにカスタムバインディングであることを意識せずにバインドできるようになります。

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:imageUrl="@{article.thumbnail()}"/>

さいごに

この記事ではKotlinとData Bindingを組み合わせて使う場合の問題点と解決策を紹介しました。

  • Kotlinから自動生成されたクラスが参照できない
    • generateStubsを有効にする
    • kotlin-kaptを使う
  • BindingAdapterを使ったカスタムバインディングが使えない

また、この記事で紹介したコードは以下のリポジトリで公開しています。興味がある方は覗いてみてください。

それでは、良いKotlin×Data Bindingライフを!

繰り返しのある予定表示に対応したカレンダーを作った

これはKotlin Advent Calendar 2016の3日目の記事です。

はじめに

2014年のAndroid Advent Calendarは25日目を担当したので、今年は無難な日付に収まることにしました。今回は繰り返しのある予定表示に対応したカレンダーライブラリを作ったので、その紹介をさせていただきます。

リポジトリ

繰り返しのある予定について

予定の繰り返しをどのように表現するかはRFC2445にて定義されており、一般にRRuleと呼ばれています。RRuleは予定の繰り返し周期や繰り返し曜日などを表現することができ、本ライブラリはそのRRuleを用いた予定の繰り返しに対応しています。例えば、毎週月曜日という繰り返し条件は以下のRRuleで表現されます。

FREQ=WEEKLY;BYDAY=MO

もう少し具体的な例を考えてみます。 予定の開始日時が2015-09-05T10:00:00.000Zで、RRuleがFREQ=WEEKLYとし、予定の展開範囲を2015-09-01T00:00:00.000Zから2015-09-30T23:59:59.999Zとします。この場合は、以下の4つの予定に展開されます。

  • 2015-09-05T10:00:00.000Z
  • 2015-09-12T10:00:00.000Z
  • 2015-09-19T10:00:00.000Z
  • 2015-09-26T10:00:00.000Z

機能紹介

土日の色付け

以下のように土曜日は青、日曜日は赤で表示されます。

f:id:yuyakaido:20171210092914p:plain

予定の表示

予定がある日には以下のようにドットが表示されます。最大2つまで表示され、3つ以上ある場合には右下にプラスマークが付きます。

f:id:yuyakaido:20171210092938p:plain

予定の色分け

予定には任意の色を設定することが出来ます。デフォルトでは赤、オレンジ、黄、緑、ティファニーブルー、ライトブルー、青、紫、ピンク、藍のテーマカラーを用意してあります。詳しくはThemeをご参照ください。

f:id:yuyakaido:20171210092949p:plain

テーマカラー

本ライブラリでは日付セルをタップするとその日付がテーマカラーにもとづいてハイライトされます。ライブラリの初期化時にテーマカラーを渡すことで切り替えが可能です。

f:id:yuyakaido:20171210093001p:plain

繰り返しのある予定の表示

本ライブラリではRRuleをもとに自動的に予定が展開されて表示されます。繰り返しの周期は毎日(FREQ=DAILY)毎週(FREQ=WEEKLY)毎月(FREQ=MONTHLY)毎年(FREQ=YEARLY)に対応しています。例えば、開始時間が2015-08-01T00:00:00.000Z毎週繰り返し(FREQ=WEEKLY)というRRuleを持った予定と開始時間が2015-08-02T00:00:00.000Z隔週繰り返し(FREQ=WEEKLY;INTERVAL=2)という2つの予定を設定すると以下のように表示されます。

f:id:yuyakaido:20171210093013p:plain

連日予定の帯表示

複数日にわたる予定の場合には、以下のように帯表示となります。予定が被っている場合には重なって表示されます。

f:id:yuyakaido:20171210093028p:plain

使い方紹介

カレンダーの表示

カレンダー本体はCouplesCalendarFragmentで実装されています。カレンダーを表示するだけであれば以下のようにFragmentTransactionでCouplesCalendarFragmentを表示するだけでOKです。

CouplesCalendarFragment fragment = CouplesCalendarFragment.newInstance();
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.main_activity_fragment_container, fragment);
transaction.commit();

予定の表示

以下のように任意のオブジェクトにCouplesCalendarEventを付与することでライブラリで表示可能な予定オブジェクトとなります。CouplesCalendarFragmentのsetEventsに予定オブジェクトのリストを渡すことで予定が表示されます。

public class SampleEvent implements CouplesCalendarEvent {

    private Date mStartAt;
    private Date mEndAt;
    private String mRRule;
    private int mEventColor;

    public void setStartAt(Date startAt) {
        mStartAt = startAt;
    }

    @Override
    public Date getStartAt() {
        return mStartAt;
    }

    public void setEndAt(Date endAt) {
        mEndAt = endAt;
    }

    @Override
    public Date getEndAt() {
        return mEndAt;
    }

    public void setRecurrenceRule(String recurrenceRule) {
        mRRule = recurrenceRule;
    }

    @Override
    public String getRecurrenceRule() {
        return mRRule;
    }

    public void setEventColor(int eventColor) {
        mEventColor = eventColor;
    }

    @Override
    public int getEventColor() {
        return mEventColor;
    }

}

CouplesCalendarEventには以下の4つの情報を取得するためにメソッドが定義されています。

  • 予定の開始時間
  • 予定の終了時間
  • 予定の繰り返し条件
  • 予定の色

このうちでカレンダーに予定を表示するためには最低限以下の情報が必要になります。

  • 予定の開始時間
  • 予定の終了時間

予定の色を設定しなかった場合はデフォルトカラーが設定され、繰り返し条件(RRule)に何も設定しなかった場合は繰り返しのない予定として扱われます。

RxJavaを使ったAndroidアプリにおける非同期処理

これはRxJava Advent Calendar 2015の2日目の記事です。

はじめに

最近何かと話題なRxJavaとRxAndroidを使って、Androidアプリの非同期処理を実装する方法を紹介します。RxJavaとRxAndroidだけでAndroidの非同期処理を実装可能ですが、RxLifecycleというライブラリも同時に使うことで何かと面倒なActivityやFragmentのライフサイクルのハンドリングを簡単に行うことが可能になります。今回はRxJava、RxAndroid、RxLifecycleの3つを使う方法についてまとめました。

RxJavaとRxAndroidとRxLifecycleの関係

RxJavaとRxAndroidはそれぞれReactive ExtensionsのJava向け実装とAndroid向け実装です。RxLifecycleはAndroidアプリ開発においてライフサイクルを持ったオブジェクト(ActivityやFragment)を管理するためのものです。RxLifecycleを使わなくてもこの記事と同じことが実現出来ますが、ライフサイクルの管理を自前で行わなければならないため、特に理由がない場合は一緒に導入することをお勧めします。

それぞれのリポジトリはこちらです。

AsyncTaskを使った非同期処理

まず、はじめにAndroid標準のAsyncTaskを使った非同期処理を見てみます。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(20000);
                return null;
            }
        }.execute();
    }
}

このコードはかなり極端な例ですが、上記のコードを実行して画面遷移や画面回転等のActivity破棄/再生成を伴う操作をするとMainActivityがリークするという問題を抱えています。なぜかと言うと、Javaの匿名クラスはアウタークラスへの暗黙の参照を持っているからで、上記の例ではAsyncTaskは最低でも20秒は裏で動き続けるため、AsyncTaskから参照されているMainActivityがGCで回収されずにリークが発生してしまいます。

RxJavaを使った非同期処理

public class MainActivity extends RxAppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Album.getAllAlbumsObservable().compose(this.<List<Album>>bindToLifecycle())
            .subscribe(new Action1<List<Album>>() {
                @Override
                public void call(List<Album> albums) {
                    // Do something
                }
            });
    }
}

今回のポイントは以下の2点です。

  • RxAppCompatActivityを継承していること
  • bindToLifecycleを呼び出していること

まず、RxAppCompatActivityについてですが、これはRxLifecycleが提供しているクラスで、ライフサイクルを管理するための実装が含まれています。次に、RxAppCompatActivityのbindToLifecycleを呼び出すことでライフサイクルを自動で管理してくれます。RxLifecycleを使わない場合は購読したObservableをどこかのタイミングで明示的に購読解除する必要がありますが、上記のように実装することで明示的な購読解除が必要なくなります。つまり、RxLifecycleを使えば購読解除をしてないことによるActivityやFragmentのリークを防ぐことが可能になります。

おわりに

非同期処理のためにRxJavaとRxAndroidを使うのは本来の使い方からは少しズレていますが、標準のAsyncTaskを雑に利用すると色々と問題があることを考えるとこの使い方もアリなのかなと思います。

Androidアプリを高速化しよう -ANR編-

これはAndroid Advent Calendar 2014の30日目の記事です。

はじめに

25日目では目に見えないボトルネックを探す手法を紹介しましたが、今回はAndroidアプリ開発を行う上で避けては通れないANRとその対処法について書きます。

環境

この記事で紹介するソースコードの動作確認は以下の環境で行いました。

また、この記事で紹介するコードはこちらで公開しています。

ANR

ANRとはメインスレッド上で重たい処理を行った際に表示される警告で、ユーザーには「〜は応答していません」というダイアログとして表示されます。ここでいう重たい処理というのは具体的には以下の通りです。

  • メインスレッド上で5秒以上かかる処理を実行する
  • BroadcastReceiverが10秒以内で終了しない

実験

では、実際にANRを発生させてみます。

アプリに適当なボタンを配置してそれを押すと以下のコードが実行されるようにします。

try {
    Thread.sleep(10000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

アプリを実行し、ボタンを押してしばらくすると以下のようにANRが発生します。

f:id:yuyakaido:20141230154001p:plain

対処法

上記のサンプルでは原因が明らかですが、実際はコードがもっと複雑で一見するとどこが原因となっているのかを特定するのが困難である場合がほとんどだと思います。ですが、実はANRは端末内の以下のファイルに逐一記録されています。

/data/anr/traces.txt

上記のファイルを確認することでANRの原因を特定することが可能となります。

adbを用いて端末内のANRログを取り出します。

adb pull /data/anr/traces.txt ~/

ANRログはこのファイルの先頭に逐一追加されていきます。取り出したファイルを開いてみると、以下のように先程の実験で発生させたANRが記録されているはずです。

----- pid 1219 at 2014-12-30 06:54:15 -----
Cmd line: com.yuyakaido.androidadventcalendar2014

DALVIK THREADS:
(mutexes: tll=0 tsl=0 tscl=0 ghl=0)

"main" prio=5 tid=1 TIMED_WAIT
  | group="main" sCount=1 dsCount=0 obj=0xa62904b0 self=0xb9109510
  | sysTid=1219 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1216953280
  | schedstat=( 81308922 43683273 389 ) utm=6 stm=1 core=0
  at java.lang.VMThread.sleep(Native Method)
  at java.lang.Thread.sleep(Thread.java:1031)
  at java.lang.Thread.sleep(Thread.java:1013)
  at com.yuyakaido.androidadventcalendar2014.MainActivity.performApplicationNotRespondingTest(MainActivity.java:71)
  at com.yuyakaido.androidadventcalendar2014.MainActivity.access$400(MainActivity.java:17)
  at com.yuyakaido.androidadventcalendar2014.MainActivity$5.onClick(MainActivity.java:63)
  at android.view.View.performClick(View.java:4084)
  at android.view.View$PerformClick.run(View.java:16966)
  at android.os.Handler.handleCallback(Handler.java:615)
  at android.os.Handler.dispatchMessage(Handler.java:92)
  at android.os.Looper.loop(Looper.java:137)
  at android.app.ActivityThread.main(ActivityThread.java:4745)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
  at dalvik.system.NativeStart.main(Native Method)

このようにANRログを参照することでANRの原因を特定することが出来ます。原因が分かってしまえば後は煮るなり焼くなりしてしまいましょう。

さいごに

この年末はAndroidではなく、ひたすらSwiftでiOSアプリを開発しています。決してAndroidアプリ開発に嫌気がさしたわけではないですよ?

それではAndroid開発者の皆様、良いお年を!

Androidアプリを高速化しよう

これはAndroid Advent Calendar 2014の25日目の記事です。

はじめに

Androidアプリの開発をしていたのがきっかけで彼女が出来たyuyakaidoです。昨日のkaneshinさんの記事の冒頭にあるように僕はマルチスレッド初心者なので常にシングルスレッドで動作しています。勿論クリスマスイブも。

今回はAndroidアプリのボトルネックを探すための手法を紹介していこうと思います。

目次

  • StrictMode
    • パフォーマンスに影響を及ぼすコードの検出
  • Traceview
    • パフォーマンス計測ツール
  • その他
    • Viewのネストについて
    • Viewの塗り潰しについて

環境

この記事で紹介するソースコードの動作確認は以下環境で行いました。

StrictMode

StrictModeはアプリのパフォーマンスに影響を及ぼす可能性のあるコードを検出してくれるものです。StrictModeはポリシーという形で何を検出するかを自由に設定することが出来ます。

基礎編

まず、StrictModeを有効にするためにApplicationのonCreate、もしくはActivityのonCreateに以下コードを追加します。

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());

では、上記ポリシーに違反するコードを書いてみましょう。

String fileName = "sample.txt";
String fileContent = "Hello Android Advent Calendar!!";

File file = new File(Environment.getExternalStorageDirectory(), fileName);

try {
    FileOutputStream fileOutputStream = new FileOutputStream(file);
    fileOutputStream.write(fileContent.getBytes());
    fileOutputStream.close();
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

上記コードを実行すると以下ログが表示されるはずです。

12-20 21:08:33.894    5064-5064/com.yuyakaido.androidadventcalendar2014 D/StrictMode﹕ StrictMode policy violation; ~duration=24 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=31 violation=2
            at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1089)
            (省略)
12-20 21:08:33.894    5064-5064/com.yuyakaido.androidadventcalendar2014 D/StrictMode﹕ StrictMode policy violation; ~duration=8 ms: android.os.StrictMode$StrictModeDiskWriteViolation: policy=31 violation=1
            at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk(StrictMode.java:1063)
            (省略)

上記ログからディスクI/Oに関するポリシー違反が検出されたことが分かります。

実践編

ActiveAndroidでデータをDBに保存して、その保存したデータをDBから取り出す場合を考えてみます。

まず、保存対象となるモデルクラスを用意します。

@Table(name = "Items")
public class Item extends Model {
    @Column(name = "Name")
    public String name;
}

次に、以下コードを適当な箇所に記述します。

Item item = new Item();
item.name = "NAME";
item.save();
item = new Select().from(Item.class).executeSingle();

上記コードを実行すると以下ログが出力されるはずです。

12-20 21:18:56.758    5350-5350/com.yuyakaido.androidadventcalendar2014 D/StrictMode﹕ StrictMode policy violation; ~duration=32 ms: android.os.StrictMode$StrictModeDiskWriteViolation: policy=31 violation=1
            at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk(StrictMode.java:1063)
            at android.database.sqlite.SQLiteStatement.acquireAndLock(SQLiteStatement.java:226)
            at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:84)
            at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:2020)
            at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1960)
            at android.database.sqlite.SQLiteDatabase.endTransaction(SQLiteDatabase.java:736)
            at android.database.sqlite.SQLiteStatement.releaseAndUnlock(SQLiteStatement.java:273)
            at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:115)
            at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1839)
            at android.database.sqlite.SQLiteDatabase.insert(SQLiteDatabase.java:1712)
            at com.activeandroid.Model.save(Model.java:155)
            at com.yuyakaido.androidadventcalendar2014.MainActivity.ActiveAndroidTest(MainActivity.java:27)
            at com.yuyakaido.androidadventcalendar2014.MainActivity.onCreate(MainActivity.java:21)
            at android.app.Activity.performCreate(Activity.java:4470)
            at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1053)
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1934)
            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1995)
            at android.app.ActivityThread.access$600(ActivityThread.java:128)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1161)
            at android.os.Handler.dispatchMessage(Handler.java:99)
            at android.os.Looper.loop(Looper.java:137)
            at android.app.ActivityThread.main(ActivityThread.java:4514)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:511)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:980)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:747)
            at dalvik.system.NativeStart.main(Native Method)
12-20 21:18:56.758    5350-5350/com.yuyakaido.androidadventcalendar2014 D/StrictMode﹕ StrictMode policy violation; ~duration=10 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=31 violation=2
            at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1089)
            at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1678)
            at android.database.sqlite.SQLiteDatabase.rawQuery(SQLiteDatabase.java:1659)
            at com.activeandroid.util.SQLiteUtils.rawQuery(SQLiteUtils.java:106)
            at com.activeandroid.util.SQLiteUtils.rawQuerySingle(SQLiteUtils.java:122)
            at com.activeandroid.query.From.executeSingle(From.java:311)
            at com.yuyakaido.androidadventcalendar2014.MainActivity.ActiveAndroidTest(MainActivity.java:28)
            at com.yuyakaido.androidadventcalendar2014.MainActivity.onCreate(MainActivity.java:21)
            at android.app.Activity.performCreate(Activity.java:4470)
            at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1053)
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1934)
            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1995)
            at android.app.ActivityThread.access$600(ActivityThread.java:128)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1161)
            at android.os.Handler.dispatchMessage(Handler.java:99)
            at android.os.Looper.loop(Looper.java:137)
            at android.app.ActivityThread.main(ActivityThread.java:4514)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:511)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:980)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:747)
            at dalvik.system.NativeStart.main(Native Method)

上記スタックトレースを見ると、DBに書き込む時とDBから読み込む時にポリシー違反が発生していることが分かると思います。

Traceview

TraceviewはAndroid SDKに付属しているパフォーマンス計測のためのツールで、開発中のアプリ内で実行されるメソッドの実行時間の計測や時系列の表示、メソッドのコールスタックの可視化といったアプリの高速化の役に立つであろう様々な計測を行うことが出来ます。

以前はソースコードに計測開始・計測終了を指定するメソッドを埋め込まなければならず、さらに計測結果のファイルが端末内に保存されるので取り出しが面倒でしたが、Android Studioでは計測開始・計測終了時にボタンを押すだけで良くなり、簡単にパフォーマンス計測を行うことが可能になりました。

基礎編

適当な箇所に以下コードを記述します。

try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Android Studioで以下操作を行うことで簡単にパフォーマンス計測を行うことが出来ます。

f:id:yuyakaido:20141221001350p:plain

  1. Android DDMSを開く
  2. パフォーマンス計測を行うプロセスを選択
  3. 計測開始
  4. 計測終了

上記操作を行うと以下のような計測結果が表示されるはずです。

f:id:yuyakaido:20141221003359p:plain

上半分にはUIスレッド上の処理が時系列順に並んでいます。下半分には全メソッドの実行時間や総実行時間に対する各メソッドの実行時間の割合などが表示されています。

実践編

Traceviewはメソッドのコールスタックを追うことが出来ます。

適当な箇所に以下コードを記述します。

ActiveAndroid.beginTransaction();
try {
    for (int i = 0; i < 100; i++) {
        Item item = new Item();
        item.name = "NAME";
        item.save();
    }
    ActiveAndroid.setTransactionSuccessful();
} finally {
    ActiveAndroid.endTransaction();
}

上記コードのパフォーマンス計測を行った結果を以下に示します。

f:id:yuyakaido:20141221011747p:plain

処理が開始された時点を矢印で示しています。今回の検証コードではModelのsaveが100回呼ばれているので、Modelのsaveがいくつも並んでいます。

このようにTraceviewを使えばメソッドのコールスタックを辿ることが出来ます。このコールスタックで自身のコードに行き当たればその部分が根本原因であると特定することが出来ます。

その他

Viewのネストについて

15日目で話題に挙がっているViewのネストはパフォーマンスを低下させる一因になります。一般にLinearLayoutよりもRelativeLayoutを使った方がViewのネストが浅くなります。とはいえ、既存のものを書き換えるのはコスパが悪いので、そこまで神経質に全てを書き換える必要はないと思います。新規で作るレイアウトに関してはネストの深さを意識してみると良いのではないでしょうか。

Viewの塗り潰しについて

ここで言及されているように、無駄にViewの塗りつぶしをするのはパフォーマンスを低下させる一因になります。

おわりに

この記事で紹介したソースコードここで公開しています。

ミクシィのインターンシップに参加してきました

今年の夏休みに株式会社ミクシィインターンシップに参加してきました。とても貴重な経験だったので振り返りもかねてブログにまとめておこうと思います。

期間

2013/08/01 - 2013/09/13

週5日フルタイムでした。一度も寝坊しなかったのは奇跡かもしれない。日数だけ聞くと長いですが、とても楽しくてあっという間でした。

選考

自分の場合、選考フローは以下の通りでした。

書類選考(ES) → 1次面接(エンジニア) → 2次面接(希望部署のオーナー) → 採用

面接の回数は人によって違っていたようですが、2回という人が多かった気がします。また、遠方の方はスカイプを使って面接が行われたそうです。今後インターンシップに参加する人のための参考になるかどうかは分かりませんが、一応詳細を書いておきます。

1次面接

1次面接は現場のエンジニアの方との面接でした。

軽く自己紹介をしたあとは、技術的な質問をいくつかされました。質問は特にマニアックな内容というわけではなく、基本的なデータ構造やアルゴリズムが理解出来ていれば問題なく答えられる内容でした。質問にちゃんと答えられる事は勿論大切ですが、自分が1次面接を通過出来た要因を挙げるとすれば、今までに作った作品を実際に見せて説明した事が大きかったのではないかと思います。あと、OSの自作をしているというのはウケが良かった気がします。

2次面接

2次面接は希望部署のオーナーの方との面接でした。

1次面接と比較すると、その部署を希望した理由やどんな仕事がしたいかといった面接らしい面接でした。

内容

今年のミクシィインターンシップは他の企業のインターンシップとは一味違っていて、社内の開発プロジェクトに参加させてもらう形のインターンシップでした。基本的に各部署に1人ずつ配属される感じです。クライアントアプリ開発から社内インフラの運用まで色々な部署で募集をしていたようです。

自分はユーザーサービス本部のメッセージユニットという部署に配属でした。この部署で「Coscam」というAndroidアプリの開発に参加させてもらいました。このアプリは一言で言うと、コスプレイヤー向けの名刺作成アプリで、名刺を作成するだけでなくアプリから直接注文する事も出来ます。このアプリは自分がインターンシップとして参加する以前から既に開発が始まっており、自分は途中から参加する形になりました。

主な仕事内容は、ミーティングへの参加・バグの修正・新規機能の追加などです。インターンシップ参加だからこれは触っちゃダメとかこのソースは見ちゃダメとかはまったくなく、自由に色々とやらせてもらう事が出来ました。企画段階には関わる事が出来ませんでしたが、開発・リリース・保守の一連の流れを経験する事が出来たのはとても良かったです。

エンジニアにはMacBook Proが貸与され、基本的にそれで開発を行います。実はミクシィインターンシップに参加するまで一度もMacを使った事がなく、最初の1週間は何かある度に隣の方に聞いたり調べたりの連続でした。初めは慣れてないせいもあり、使いにくいと思っていたのですが、いつの間にか自分用のMacBook Airを買ってしまっていました。怖い。

感想

実はインターンシップに参加すると単位が貰えるからという不純な理由でインターンシップに参加したのですが、いま振り返ってみると単位などどうでもよくなるくらい得るものが多かったです。例えば、今まで一人で開発する事が多かったので、自分で好き勝手に設計してコーディングも好き勝手にやっていたのですが、ちゃんとした設計手法を学ぶ事が出来ました。また、Gitの使い方も習得する事が出来ました。挙げればキリがないので、この辺に留めておきますが、とにかく毎日勉強になる事ばかりでした。あ、単位はしっかり貰いますけどね。

開発の中でGitを使っていたのですが、インターンシップに参加するまでまともにGitを使った事が無かったので、慣れるまで大変だったのが記憶に残っています。3日目辺りに作業していたブランチとは別のブランチからPull Requestを投げた上にローカルリポジトリの削除をやらかして、半日分の作業を消し飛ばすというヘマをやらかした時は死にたくなりました。

他の企業と比較は出来ないですが、ミクシィは福利厚生がしっかりしている印象を受けました。フリードリンクがあったり、社内でお菓子が調達出来たり、エンジニア用のお高い机と椅子がいくつもあったりという感じです。また、社内に蛍光灯の光を遮るための葉っぱがいくつも生えているのが印象的でした。あと、机の間には仕切りがなく開放的な感じでした。

最終日にサプライズで色々とプレゼントを貰いました。ありがとうございます。色紙とかKindle PaperWhiteとかオクトキャットシールとかParseシールとかカントリーマアムとか。内定者インターンの方とランチに行った時に、内定者のオリエンテーションミクシィのロゴが印字されたHHKBのMのキートップが貰えた、という話を聞いてから欲しくて堪らなかったのですが、最終日にその話を出したらなんとキートップを頂く事が出来ました!めっちゃ嬉しい!!

f:id:yuyakaido:20140213224559j:plain

最後に

ミクシィインターンシップに参加して本当に良かったと思います。とても楽しく仕事をすることが出来ました。ありがとうございました。