おでーぶでおでーぶ

いろいろ書く。いろいろ。

Android Architecture Components の Lifecycle モジュールの APT を追う

Google I/O 2017 で Android Architecture Components が公開されました。

developer.android.com

簡単に Lifecycle の Observer を作成できます。どうやって実現しているのでしょうか?

試しに簡単な LifecycleObserver を作ってみましょう。

class MyActivity extends LifecycleActivity {
  static class MyLifecycleObserver implements LifecycleObserver {
    private static final String TAG = MyLifecycleObserver.class.getSimpleName();
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    void onResume() {
      Log.d(TAG, "onResume");
    }
  }

  void onCreate() {
    getLifecycle().addObserver(new MyLifecycleObserver());
  }
}

APT で生成された実装は以下です。

// 同一パッケージ
public class MyActivity_MyLifecycleObserver_LifecycleAdapter implements GenericLifecycleObserver {
  final MyActivity.MyLifecycleObserver mReceiver;

  MyActivity_MyLifecycleObserver_LifecycleAdapter(MyActivity.MyLifecycleObserver receiver) {
    this.mReceiver = receiver;
  }

  @Override
  public void onStateChanged(LifecycleOwner owner, Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_RESUME) {
      mReceiver.onResume();
    }
    if (event == Lifecycle.Event.ON_CREATE) {
      mReceiver.onCreate();
    }
  }

  public Object getReceiver() {
    return mReceiver;
  }
}

上記はとてもシンプルですね。分かりやすいです。では LifecycleRegistry#addObserver を追っていきます。

// in : LifecycleRegistry
@Override
public void addObserver(LifecycleObserver observer) {
  ObserverWithState observerWithState = new ObserverWithState(observer);
  mObserverSet.putIfAbsent(observer, observerWithState);
  observerWithState.sync();
}

なるほどなるほど。では ObserverWithState の中へ。

// in : LifecycleRegistry
class ObserverWithState {
  private State mObserverCurrentState = INITIALIZED;
  private GenericLifecycleObserver mCallback;

  ObserverWithState(LifecycleObserver observer) {
    mCallback = Lifecycling.getCallback(observer);
  }
}

GenericLifecycleObserver は自分では実装していませんね。APT で作成したクラスから取るのでしょう。では Lifecycling#getCallback へ。

// in : LifecycleRegistry
@NonNull
static GenericLifecycleObserver getCallback(Object object) {
  if (object instanceof GenericLifecycleObserver) {
    return (GenericLifecycleObserver) object;
  }
  ...

違いますね。次です。

// in : LifecycleRegistry
//noinspection TryWithIdenticalCatches
try {
  final Class<?> klass = object.getClass();
  Constructor<? extends GenericLifecycleObserver> cachedConstructor = sCallbackCache.get(
  klass);
  if (cachedConstructor != null) {
    return cachedConstructor.newInstance(object);
  }
  cachedConstructor = getGeneratedAdapterConstructor(klass);
  if (cachedConstructor != null) {
    sCallbackCache.put(klass, cachedConstructor);
    if (!cachedConstructor.isAccessible()) {
      cachedConstructor.setAccessible(true);
    }
    return cachedConstructor.newInstance(object);
  } else {
    sCallbackCache.put(klass, sREFLECTIVE);
  }
  return new ReflectiveGenericLifecycleObserver(object);
} catch (IllegalAccessException e) {
  throw new RuntimeException(e);
} catch (InstantiationException e) {
  throw new RuntimeException(e);
} catch (InvocationTargetException e) {
  throw new RuntimeException(e);
}

コンストラクタをキャッシュして早くしてるだけなので、一回キャッシュ周りの処理を消しましょう。

// in : LifecycleRegistry
  final Class<?> klass = object.getClass();
  cachedConstructor = getGeneratedAdapterConstructor(klass);
  if (cachedConstructor != null) {
    if (!cachedConstructor.isAccessible()) {
      cachedConstructor.setAccessible(true);
    }
    return cachedConstructor.newInstance(object);
  }
  return new ReflectiveGenericLifecycleObserver(object);

つまり getGeneratedAdapterConstructor を見ればいいわけですね。

// in : LifecycleRegistry
@Nullable
private static Constructor<? extends GenericLifecycleObserver> getGeneratedAdapterConstructor(
        Class<?> klass) {
    final String fullPackage = klass.getPackage().getName();

    String name = klass.getCanonicalName();
    // anonymous class bug:35073837
    if (name == null) {
        return null;
    }
    final String adapterName = getAdapterName(fullPackage.isEmpty() ? name :
            name.substring(fullPackage.length() + 1));
    try {
        @SuppressWarnings("unchecked")
        final Class<? extends GenericLifecycleObserver> aClass =
                (Class<? extends GenericLifecycleObserver>) Class.forName(
                        fullPackage.isEmpty() ? adapterName : fullPackage + "." + adapterName);
        return aClass.getDeclaredConstructor(klass);
    } catch (ClassNotFoundException e) {
        final Class<?> superclass = klass.getSuperclass();
        if (superclass != null) {
            return getGeneratedAdapterConstructor(superclass);
        }
    } catch (NoSuchMethodException e) {
        // this should not happen
        throw new RuntimeException(e);
    }
    return null;
}

static String getAdapterName(String className) {
    return className.replace(".", "_") + "_LifecycleAdapter";
}

bug とか書いてあって不穏ですが、canonical name は匿名クラスだと null になります。問題ありません。さて canonical name がある場合、パッケージ名等から LifecycleAdapter 名を作成します。最初の APT で生成されたクラスを作り、そのコンストラクタを返します。

// in : LifecycleRegistry
  final Class<?> klass = object.getClass();
  cachedConstructor = getGeneratedAdapterConstructor(klass);
  if (cachedConstructor != null) {
    if (!cachedConstructor.isAccessible()) {
      cachedConstructor.setAccessible(true);
    }
    return cachedConstructor.newInstance(object);
  }
  return new ReflectiveGenericLifecycleObserver(object);

コンストラクタがあれば、最初に渡した Observer を引数として GenericLifecycleObserver を、今回は MyActivity_MyLifecycleObserver_LifecycleAdapter を作成します。非常にシンプルですね。

canonical name がなかった場合は getGeneratedAdapterConstructor が null を返していましたね。つまり ReflectiveGenericLifecycleObserver なるものにたどり着きます。

f:id:jmatsu:20170522141928j:plain

その中身は黒魔術の塊・・・といいたいんですが、Reflection ではクラスを作れないので、本来は APT でクラスにしておく部分を Runtime で処理しています。

ということで、匿名クラスで LifecycleObserver を作成してしまうと初回のみコストが高いことになります。コストが高いといっても感はありますが。 それに一度生成すればオンメモリキャッシュする機構もありますし、問題はなさそうです。というか、匿名クラスで作る必要を感じないんですがそれは・・・

とりあえず定義した LifecycleObserver を wrap するクラスがあり、それを動的に生成しているようです。またその wrapper は Lifecycle State を監視していて、時が来たら LifecycleObserverの該当メソッドを叩くという非常にシンプルな構造でした。とさ。

台タイプのスタンディングデスクを買った

雑記。結論から言うと以下のスタンディングデスクを買って、なかなか良いスタンディングワークライフを送っている。

↓ こんな感じでモニターアームつけて、キーボード台は設置しないで使っている。

f:id:jmatsu:20170515125427j:plain

なんでスタンディングデスクなの?

人間は座りっぱなし・立ちっぱなしのどちらかにならない方が良いとかなんとか。
スタンディングデスクにして定期的に立ち仕事と座り仕事に切り替えると健康に良いらしい。

が、そんな理由じゃなくて、スタンディングワークはポモドーロと相性がいいと思っていることが一番の理由。
立っている間は集中して作業をする。休憩中は座ってのんびりコーヒーでも飲めばいい。
しかもスタンディングワークは(ひとによるけど)連続作動可能時間が存在するので、それを超えたら集中できない。
結果、強制的にメリハリがつく。はず。

スタンディングデスクの種類

スタンディングデスクには机そのものが昇降するタイプ、机の上に置く台タイプが存在していて、それぞれ手動/自動がある。

x 電動 ガス式 手動
机昇降式 Type-A Type-B Type-C
台昇降式 Type-D Type-E Type-F

今回買ったのは Type-E 。ガス式台昇降タイプ。
色々理由はあるけれど、身長が188cmあるせいでちょうどいい高さまで上げられる製品が少なかったので、台式 on the table にして無理矢理頑張っている。

Type-A

本当はこれが欲しかった・・・が、一番高い価格帯になる。

机全体が昇降するので、非常に使い勝手がいい。ロススペースもなく、下降時に物があると自動で止まるセンサーがついているものも多い。
性能を見ても高いのが当然で、全てがダントツトップ。耐荷重、設定可能高さ、対価年数、そして本体重量もダントツ。
一人では絶対設置しない方がいいし、結構な人が単純にできないくらいの重さと大きさがある。
そして腰に良いはずのスタンディングデスクを導入して、腰をヤッてしまったら何の意味もないので、もし導入するなら数人でやるか設置まで頼めるやつにしましょう。

ちなみに安いのもあったにはあったけれど、自分の身長だとスタンディングワークに使える高さまで上がらなかったので・・・

Type-B

これも机全体が昇降するので使い勝手はいいし、ロススペースもない。

価格は安い部類に入る。
ただ設定可能高さがあまり広くないので、その部分では使い勝手が台昇降式にも劣る可能性がある。
そもそもの高さが低いものも多く、作業には向いてないので選定外とした。

Type-C

可変なスタンディングデスクには成り得ないので除外。
しかも上に物を置いて動かすことができない。最初からスタンディングデスクとして使うならギリギリセーフというレベル。

この上に台タイプを置くことも考えたが、元々机はあったのでとりあえずスルー。

Type-D

あるの?

Type-E

今回買ったタイプ。

可動域は30cm~50cmのものが多い。また耐荷重は10kg~20kgに落ち着くので、モニターアーム + ディスプレイ * 2とかにすると厳しいものも多い。
当然なのだけど、載せる机の方が幅広でなくてはならないという制約はあるし、机の耐荷重が台重量(大体10kgはある)+台耐荷重を超えている必要がある。
立ち上げ時のロススペースはほぼない(支柱の形式次第)が、一番下げた状態にすると純粋に台の高さ分がロススペースになる。
今回買ったのはその中でも可動域や耐荷重の大きいもので、まあまあな値段になっている。
ちなみに飲み物を載せたままでも全く問題がない安定した昇降が可能。

Type-F

会社で使っている。

耐荷重が圧倒的に低く、最大の高さにすると不安定になる。そのためスタンディングワークには向いてない。
めっちゃ安いので、単純に高さ調整用の台としては優秀。

まとめ

コストに見合う価値はあったし、十二分に満足している。
あとスタンディングワークをするときは、足元にクッションを買うか、良いスリッパを買った方がいい。
今までスタンディングワークをしているときは靴を履いていたので気付かなかったが、足元がめっちゃ重要。

スタンディングワーク自体やったことがない人はとりあえず一度やってみることをオススメします。

特定のファイルの git addとかがめんどうくさいのでなんとかする

ある程度編集したときに「あー、このファイルだけコミットしておきたい」みたいな状況ってあると思うんですよ。 他にも「このファイルだけHEADの状態に戻したい」とか「unstageしたい」みたいな。

でも例えばJavaだとpackage名が一緒なファイルがずらーっと並んでしまい、補完しようにも先頭が大体一緒で本当に面倒くさい。

残念ながらJavaをやめるという選択肢がないので、gitのサブコマンドを作って対応する。

ファイル一覧から特定のファイルをaddする - git add $file

#!/usr/bin/env bash

: ${GIT_FILTER_COMMAND:=fzf}

ls_modified_files() {
  echo '--- LABEL : tracked'
  git diff --name-only
}

ls_untracked_files() {
  echo '--- LABEL : untracked'
  git ls-files --others --exclude-standard
}

main() {
  local -r selected_file=$(cat <(ls_modified_files) <(ls_untracked_files) | $GIT_FILTER_COMMAND)

  if [[ -f "$selected_file" || -d "$selected_file" ]]; then
    git add "$selected_file"
  else
    echo 'Not found or Canceled' >&2
  fi
}

main

ファイル一覧から特定のファイルをHEADの状態に戻す- git checkout -- $file

#!/usr/bin/env bash

: ${GIT_FILTER_COMMAND:=fzf}

ls_staged_files() {
  echo '--- LABEL : staged'
  git diff --name-only --staged
}

ls_modified_files() {
  echo '--- LABEL : modified'
  git diff --name-only
}

main() {
  local -r selected_file=$(cat <(ls_staged_files) <(ls_modified_files) | $GIT_FILTER_COMMAND)

  if [[ -f "$selected_file" || -d "$selected_file" ]]; then
    git checkout -- "$selected_file"
  else
    echo 'Not found or Canceled' >&2
  fi
}

main

ファイル一覧から特定のファイルをunstageする - git reset --$file

#!/usr/bin/env bash

: ${GIT_FILTER_COMMAND:=fzf}

ls_staged_files() {
  echo '--- LABEL : staged'
  git diff --name-only --staged
}

main() {
  local -r selected_file=$(cat <(ls_staged_files) | $GIT_FILTER_COMMAND)

  if [[ -f "$selected_file" || -d "$selected_file" ]]; then
    git reset -- "$selected_file"
  else
    echo 'Not found or Canceled' >&2
  fi
}

main

まだtrackしてないディレクトリの内部までほじくるとかはキャンセル処理とかが面倒なのでやってないけれど、余裕で可能だと思います。

おまけ

ファイル名を git-hoge としてPATH配下に認識させておくとgit hogeで起動できる。(サブコマンド)
拡張子をつけてエディタのファイル補完を適用したいなら、シンボリックリンクで対応すればいいと思います。

Homebrewで特定バージョンのFormulaをインストールする

mongodb を 3.4.1 から 3.2.11 にしたかったのでやってみた。
homebrew-versionsはdeprecatedだし、あんまり使いたくないなーと思ってたので使わずに。

Env

brew -v
# Homebrew 1.1.8-64-gc507222                                                                    
# Homebrew/homebrew-core (git revision 35fb7; last commit 2017-01-29)

Workflow

# Try to find the commit id
brew log --author=brew-test-bot@googlegroups.com mongodb
# Search 3.2.11 and copy the commit id (COMMIT_ID_3_2_11)

# Move to formula manager
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"

# Change the revision of the formula you wanna install
git checkout $COMMIT_ID_3_2_11 Formula/mongodb.rb

# Stop the service if needed
brew services stop mongodb

# Unlink the current formula
brew unlink mongodb

# Install the version you changed
brew install mongodb

# Check the installed formula
ls "$(brew --repo)../Cellar/mongodb/"

# Back to the latest commit
git reset -- Formula/mongodb.rb # To unstage the formula.
git checkout -- Formula/mongodb.rb

# Restart the service if needed
brew services start mongodb

3つの正規表現を知らないとハマるヨ

古来の正規表現 = 標準正規表現、basic regular expression、BRE

ダン正規表現 = 拡張正規表現、extended regular expression、ERE

独自にさらに拡張された拡張正規表現 = 長いので、超拡張正規表現とします (ちなみにもう正規言語の域を超えた)

の3種類が存在するのだけれど、みんなあまり気にしていない気がする。

BRE と ERE の違い

便宜上、左を拡張正規表現であるEREにしており、違う部分だけを抜粋(したつもり

ERE BRE 用途 代替表現
| unsupported OR 表現 I don’t know
+ unsupported 1文字以上の繰り返し \{1,\}
? unsupported 0 or 1文字 \{,1\}
() \(\) グループ化
{n,m} \{n,m\} n文字以上、m文字以下の繰り返し

であれば、詳しくは以下のコマンドを叩いて、読んでみると面白いです。(Macでしか確認していません)

man re_format

超拡張表現との比較が非常に多くなり、色々と面倒なので割愛するけれど、例として POSIX クラスを挙げておく。
e.g. 超拡張表現では POSIXクラスを [:space:] として表せるが、BRE、ERE共に [[:space:]] としなければならない。

それぞれの違いを例示してみる

他に詳しく違いを並べる時間もないので、例を出して「色々知っておかないとどはまりするかもよ」ということを以下で、備忘録という意図も含めて書いておきたい。
拡張正規表現は大体のプログラミング言語で扱われていて便利な分、尚更違いを知っておくべきである。

2つの例を用意する。

1つ、Mac で private ipを引くために、sed を使って ifconfig からぶっこ抜くことを考える。 2つ、axyzaopqalmn という文字列から aから始まり、aで終わる文字列 にマッチさせることを考える。

Mac で private ip を引く時の表現の違い

まずは1つ目。

一応 ifconfig の usage を超簡潔に示す。

ifconfig [interface name] inet

これで対応する interface の inet が出て来る。以下が出力例。

en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        inet xxx.xx.x.xxx netmask 0xfffffxxx broadcast xxx.xx.x.255

ここからぶっこ抜くために、全体マッチさせてipの部分で置換することを考える。 (ifconfig en0 inet|tail -1 を流し込むとする。)
たくさん思いつきますね?

# BSD sed
sed 's/[[:blank:]][[:blank:]]*inet \([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/g'

# BSD sed with modern regexp
sed -E 's/[[:blank:]]+inet ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).*/\1/g'

# BSD sed
sed 's/[[:blank:]][[:blank:]]*inet \([0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\).*/\1/g'

# BSD sed with modern regexp
sed -E 's/[[:blank:]]+inet ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*/\1/g'

# BSD sed
sed 's/[[:blank:]]\{1,\}inet \(\([0-9]\{1,3\}\.\)\{3\}[0-9]\{1,3\}\).*/\1/g'

# BSD sed with modern regexp
sed -E 's/[[:blank:]]+inet (([0-9]{1,3}\.){3}[0-9]{1,3}).*/\1/g'

( :digit: は長すぎるので使っていません )

BRE vs ERE で、グループ記法 ()、繰り返し記法 {n,m}、1以上繰り返し + の有無辺りに違いが見て取れる。

拡張正規表現を使ってみる with ジャバ

String parttern = "\s+inet ((\d{1,3}\.){3}\d{1,3}).*";
// replace the matched string with 1st group

メタ文字が使えてスッキリ。

axyzaopqalmn という文字列から aから始まり、aで終わる文字列 にマッチさせる時の表現の違い

(面倒くさいのでEREを使います)

特に条件を指定していないので、2つ出来るはず。(最長マッチ、最短マッチ)

echo 'axyzaopqalmn' | sed -E 's/a.+a/ここだよ/' # 最長
echo 'axyzaopqalmn' | sed -E 's/a[^a]+a/ここだよ/' # 最短

拡張正規表現ならば最長マッチは変わらないのだけれど、最短マッチは

a.+?a

で表せる。

結論

拡張正規表現しか知らないと sed とかで組むときにその人の発想が柔軟かどうかみたいな話になってしまうので 拡張正規表現の記法がどんなもののエイリアス(正確な表現ではないのだけれど)になっているか を知っておくべきだと思う。
勿論表現限界のせいで実現できないものも存在するので、知っておいた方がいい。シェル芸人になりたいなら尚更である。

例えば否定後読みなどは JavaScript (Chromium上) ですら1年前に実装されたばかりである。

https://codereview.chromium.org/1418963009

Tips

  • シェルスクリプト内ではBREの方が安全な場合もあるので、BREのみで表現可能であればBREでいいのかもしれない。 e.g.) 変数展開の文字をエスケープし忘れるなどが減る
  • 大体のコマンドは -E オプションでEREを使えるので、EREで表現可能であれば移植性も考えてEREでよい。
  • どうしてもEREでもだめだというなら perlruby にコマンドレベルで委譲するべき。問題ないのであればスクリプト全体をその言語にする必要はない
  • ある言語で定義した正規表現が別の言語だとそのまま使えない場合、バリデーションの統一に支障が出ることを念頭に置く。e.g.) サーバーサイドで定義したバリデーション用正規表現をクライアントで用いるケース

ちなみに正規表現エンジンの勉強がしたいなら以下の本がとてもオススメ。読み物としてもなかなかおもしろいです。

VectorDrawableをプレビューするChrome Extensionを作った

弊社では弱いネットワークや容量の小さい端末が普及しているような国にアプリを配布していることもあり、リソースを出来る限りVectorDrawableに置き換えています。

SVGファイルを自動でVectorDrawableに変換し、取り込んで利用しているのですが、今までxmlを見るだけでは以下の問題点がありました。

  • 稀に表示が崩れるものがあり、QA時点で発覚するとデザイナーへの負荷が大きくなる。
  • 色指定が異なることに気付けない。
  • Export名のミスによる上書きに気付けない。

つまり全ては VectorDrawableを読める人が少ない という問題点に帰結します。 いちいち手元に落として一個ずつAndroid Studioでプレビューするのも・・・ということもあり、題の通り Github上でVectorDrawableを見るChrome Extensionを作成しました。

f:id:jmatsu:20170110120701j:plain

chrome.google.com

現在、

に対応しています。

ソースコード

MITライセンスで公開しているので、ご自由にどうぞ。

jmatsu/vector-drawable-previewer

Chrome ExtensionのContent ScriptをTypeScriptで書くのは初めてだったので、見苦しい部分もあると思いますがそのときはPRをください。

おまけ

SVGからVector Drawableへの変換やこのツールについて、kyobashi.dex#4 で発表する予定です。

rmp-quipper.connpass.com

Special Thanks

アイコン作成 : @manse

adbがUSB接続端末を認識しないとき、複数起動していたadbプロセスを適当に1個killしたらadbが復活した・・・という備忘録

OpenSTFのために adb tcpip 7xxx とか色々をガンガン叩いたあと、USBデバッグをしようとしたら端末を認識しない。
adb kill-server をしても駄目。

f:id:jmatsu:20161225230656j:plain

まあこのエラーメッセージの感じ、adbプロセスがusbを向いていないんだろう・・・と推測。

適当にgrepをかけたら大量に生きていたので、プロセス番号が違うけどとりあえず上からkillしてみよう・・・とすると、
一個目を殺したらVysorが起動。つまりはそのまま認知するようになった。(一応エラーで出てたプロセス番号は一番下のものだったんだけど・・・)

f:id:jmatsu:20161225231204j:plain

プロセス数に数があるんだろうか? ならstart-server成功しないで欲しいなぁ・・・

メモ

usb接続に設定されたadbプロセスを調べる方法を見つける