小規模もくもく会運営についての知見とポエム
この記事は勉強会運営 Advent Calendar 2017の19日目の記事です
はじめに
会社でもくもく会を運営しているが今年はついに20回を超え、ありがたいことに他社からも「御社のもくもく会を パクリ 参考にしてうちでももくもく会はじめました 」と言ってもらえることが増えた。勉強会運営としてはある程度いい感じに回っている気がするので誰かの参考になればと知見とポエムを書き残しておきたいと思う。
注意事項
上記で「いい感じに回っている」と記載したが、それは健全な勉強会コミュニティを形成すると言う意味であり、採用目的(「いいエンジニアをシュッと採用したい」など)の観点ですぐに効果が出ることを期待しているのであれば、おそらくこの記事はあなたの求めているものではない。
「Androidもくもく会@Rettyオフィス」の歴史
私が運営しているのは「Androidもくもく会@Rettyオフィス」というAndroidエンジニアを対象としたもくもく会である。この勉強会はもうすぐ2年ほど経つが、2年間の間に色々模索しながら形態を変えてきた。 形態によって大きく分けて3つの時期に分けられ、それぞれどのような学びがあったかをまとめたいと思う。
1. 「もくもく + 懇親会」期
そもそもこの勉強会自体は私がはじめたものではない。(第1回目は参加すらしていない。)もともと他社のエンジニアに弊社のことを知ってもらったり、業界のエンジニアと知り合いたいと言う軽い採用的な目的もそれなりにあった時期だったと思う。そのため、もくもくした後はケータリングの軽食とドリンクを提供した懇親会をやっていた。この時期の運営は別の人がやっており、私の役割は当日もくもく会のホストを務めるくらいだったと思う。ホストとして何をやっていたかというと、会の進行のみである。会の規模も定員10名程度とかなり小規模なものだ。最初に会場の案内をして、参加者全員に自己紹介と今日やることを一言ずつ言ってもらい数時間ひたすらもくもく。時間が来たら成果発表を簡単にしてもらって後は懇親会に移行する流れだ。
今見てみると不思議なことに第1回から参加者も豪華だ。(なぜみんな参加してくれたかは謎。)
ただ、この時期は直接的な採用の実績はなく、しかも運営側の負担が大きかった。(具体例をあげると、ケータリングの調整や平日開催だったため割と遅い時間まで拘束されるなど。)これはおそらく弊社がグルメサービスを運営している会社であり、ケータリングもかなり気合を入れる文化があったので特殊だったかもしれない。
何度か開催した後、コストがかかる・開催に関わる人の負担が大きいこと、後は現場のエンジニアが運営していないと言う理由でこの形態はやめた。特に後者は定期的に開催できない理由の大きな原因だったように思う。これはいい学びだったので、次からは主催を自分に移してもらうようにした。
2.「もくもくのみ」期
反省を生かし、この時期は「なるべく負担が少なく、かつ本当にやりたいことだけをやる」にフォーカスしていた。本当にやりたいこととは、勉強会コミュニティの運営とした。 ケータリングの発注はなし、作業しながら飲めるドリンクだけ用意していた。後は本当にもくもくするのみ。だってもくもく会だもの。ただし参加者の自己紹介だったり、何をしているかなどのコミュニケーションは取れるようにしていた。
この時期に学んだことは、「もくもく会で軽食の用意はなくても来る人はくるし、来ない人は来ない」である。あと、懇親会は苦手だというエンジニアはいると思うので、もくもく後の懇親会は逆に参加のハードルをあげている部分もあったのではと振り返って思う。
「もくもく + 質問コーナー」期
第9回くらいに転機が訪れた。
この日は台風16号の影響で天気が荒れており当日参加者は2人しか来なかったのだが、この2人がAndroidを始めたばかりという特殊な回だった。2人とも周りにAndroidのことを聞ける人がおらず、もくもく会というよりは質問会になったのだが、それがとても楽しかった。
Androidを始めるとだいたいみんなぶつかるであろう「FragmentとActivityのライフサイクル」など、"ちょっと経験のある誰かに聞いてみたいこと"が次々に飛び出して来て盛り上がった。懇親会とはちょっと違う空気感で、ただただ技術について話すのはとても楽しかったのだ。だってエンジニアだもの。
これが楽しかったので次回からもくもくした後に「せっかくの機会なので他社のエンジニアに聞いてみたいことコーナー」を作るようになった。意外とほぼ毎回のようにいろんな質問が飛び出して来るのでやってよかったなと思う。この形態にしてから参加者の方から「うちでも同じフォーマットでもくもく会始めました」と言ってもらうことが出て来て、参加者側から見ても満足してもらえていると思っている。
他社への影響
観測している範囲で3社ほど弊社のフォーマットでもくもく会を始めてくれている。一時期もくもくコンサルでお小遣い稼ぎしたいと思ったけど、一度参加すれば簡単に真似できるフォーマットなので諦めた :P
気をつけていること
もくもく会を運営していくにあたって、いくつか気をつけていることがある。
日程について
日程はとても大切で、ポイントとしては大きな勉強会とかぶる日程は避けるという点である。potatotipsやshibuya.apkとうっかり日程を被せてしまった時は本当に人が来なかった。ターゲット層が多数参加するような勉強会が開催される日は避けた方良い。
Twitter ハッシュタグについて
「もくもくのみ」期あたりから勉強会用にTwitterハッシュタグを用意した。静かにもくもくしている中で声をあげづらいけど聞きたいことやエアコン下げてほしいなどの要望を掬うことが当初の目的だったが、意外といい副作用があった。Twitter界隈で広くエンジニアと繋がっている人が勉強会の広告塔になってくれるのだ。これは他の勉強会でも同じだと思うが、「〇〇さんがいつも楽しそうにつぶやいているので一度参加してみたかった」と言われた時は嬉しかった。
ちょっと失敗したと思うのはAndroidらしくアッパーキャメルケース (#AndroidMokuMokuRetty) でハッシュタグを作ってしまったこと。一般的に広く使われるスネークケースにすればよかった。しかも上記であげている他社のもくもく会もみんなこれに倣ってアッパーキャメルケースにしてくれておりなんだか申し訳ない。
余談だが、メルカリのAndroidもくもく会のハッシュタグは#mokumoku_android でだいぶ攻めたなと思っている。さすが岡野さん…
そろそろ向かう。それにしても思い切ったハッシュタグだなぁ。(自分のとこは一応他社と被らないように社名を入れた) #mokumoku_android
— むーむー (@muumuumuumuu) 2017年1月31日
さいごに
これだけ思い入れのある勉強会だが、おそらく私が主催するのは多くて後2回ほどである。この記事を読んで興味を持った人がいたらぜひ参加してほしい。(募集人数は随時増やせるので枠は気にせずとりあえず参加ポチってください。)久しぶりの人もお別れ会だと思ってぜひ参加してほしい。
Custom View を作るときに気をつけること
Custom Viewの背景画像を変えたいときにちょっとやらかしたのでメモる٩( 'ω' )و
何が起こったか
Custom Viewを作って動的に背景画像を変えようとして切り替わらない(ように見える)現象に遭遇。
レイアウトファイルで指定していた背景をコードから動的に切り替えようとしたときに、なぜか切り替わらないように見える 😇
何がおかしいか皆さんも考えながらコードをご覧ください。
コード
レイアウトファイル (layout_custom.xml)
静的に赤丸のdrawableを背景に指定しています。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:background="@drawable/red_oval" android:layout_width="match_parent" android:layout_height="match_parent"> </LinearLayout>
CustomViewの本体コード (CustomView.kt)
初期化時に上記のレイアウトファイルを読み込んでいます。viewの生成が完了し、attachされたタイミングで動的に背景を黒丸に変更しています。
class CustomView : LinearLayout { constructor(context: Context?) : super(context) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) init { LayoutInflater.from(this.context).inflate(R.layout.view_custom, this) } override fun onAttachedToWindow() { super.onAttachedToWindow() this.setBackgroundResource(R.drawable.black_oval) } }
現実
さて、上記コードを書いたときに私は背景は黒丸が出ることを期待していましたが、実際には赤丸が表示されています😩
何がダメだったのか
さて、ここまで読んでくれた皆さんは原因に気がついたでしょうか。
今回の原因はCustom Viewでlayout fileをinflateするときにrootを this
に指定しているため、this
= Custom Viewクラス (Linear Layoutを継承しているクラス)のchildとしてlayoutファイルからinflateしたviewたちがぶら下がる構成になっていました。そのため、コードから this
に対して黒丸に背景を変更しても、その上にいる赤丸を背景に指定しているview (layout fileの一番上の階層にいるview)の背景が重ねて描画されているため見た目的には何も変わらないと言うオチになっていました。
どうすればよかったか
ちゃんと正しいviewに対してアクセスして背景を変えてやりましょう。(本当はちゃんとviewにidつけたりすべきですがサンプルなので見逃してください😇)
override fun onAttachedToWindow() { super.onAttachedToWindow() this.getChildAt(0).setBackgroundResource(R.drawable.black_oval) }
これで無事黒丸背景になりました!٩( 'ω' )و
KotlinとReduxをAndroidアプリに導入した話をしました。
先日自社のエンジニアイベントで「KotlinとReduxをAndroidに導入したら」という話をしてきました。質疑応答や懇親会でいろんな質問をいただいたのでここに残しておきます。
イベント
登壇したのはこちらのイベント。アプリだけでなくバックエンドやインフラ、果ては機械学習まで幅広いテーマを扱うイベントでした。聞き手の皆さんの知識もばらつきがありそうなので、割と丁寧にいろんなことを説明したつもりです。しかしその分内容を詰め込みすぎて早口に…
当日の雰囲気はこちらで。
登壇内容
内容はこちらのスライドをご覧ください。ちなみに、最後の方の時間があれば話そうと思っていたFluxの話は時間切れでしませんでした。
スライドにも乗っていますが、今回初めて会社としてOSS Libraryを公開しました。こちらについてもいくつか質問をいただきました。
質疑応答や懇親会で話したこと
Reduxについて
Q: Stateの持ち方を途中で変更すると大変じゃないですか?
A: 今の所変更したいケースは出てきていないのでわかりません。変更してない理由としては、必要最低限の情報しかReduxにのせていないからです。別のアプローチとしては情報を綺麗に正規化して持たせるという方法もあります。今はどちらのアプローチが良いのか、AndroidとiOSでそれぞれ試している最中です。
—–
Q: API通信はReduxの図のどこでやるんですか?
A: スライドには入れてませんが、実際にはActionを生成するCreatorProducerというのがいます。ここで、APIの戻り値の情報を適切なActionに詰めます。CreatorProducerが生成したActionがStoreにdispatchされるようになっています。
—–
Q: SharedPreferenceなどローカルに情報を退避する方法を採用しないのですか?
A: 採用するのもいいのですが、退避場所を含めると2箇所で情報を持つことになるのでReduxの原則から外れるのと、情報によって「(退避する・しないといった)管理の方法」がバラバラになるのを避けたかったので今の所採用していません。もしも退避するのであればmiddlewareでやるのがいいと思います。
—–
Q: フロントエンドでReduxはやったことがあるけれど、アプリにReduxを取り入れる時のメリットがうまくイメージできなかったのですが、どういった点でよかったですか?
A: AndroidアプリはActivityやFragmentの双方向の情報のやり取りが非常に面倒臭いので、一箇所にデータをまとめて各自がそこを監視するというReduxはその点でよかったです。
—–
Q: FluxではなくReduxにした理由は?
A: Fluxにしたい(=Storeを複数持ちたい)と思うタイミングが定期的にやってきますが、今の所Reduxにしています。Fluxにしたいと思ってしまう時は、だいたいActivity/Fragment間で情報の共有がしたいときなので、しばらく悩んだあとサボらずにinterfaceを書いています。もしかしたらそのうちFluxを試すかもしれません。
--ここから懇親会では話せなかったけど、書いてて思いついたので追記-- メモリマネジメントの観点でFluxにする可能性も将来的にあるかもしれません。 一時的にかなり大きなサイズの情報を扱う必要が出てきた時に、 不要になったらそのStoreごと破棄してメモリを解放させる必要が出てきた場合などです。 今の所この点では困っていませんがサービスによってはあり得ると思います。 --ここまで懇親会では話せなかったけど、書いてて思いついたので追記--
—–
Q: Reduxを始める時に、何を参考にするのがいいですか?
A: この動画がオススメです!AndroidではなくJS前提ですが、Reduxのポイントは抑えられると思います。
Redux入門にオススメの動画ですー https://t.co/LcsdiZpD0p #Retty_tech_cafe
— むーむー (@muumuumuumuu) 2017年9月1日
Kotlinについて
Q: 今Kotlinの割合はどれくらいですか?
A: Kotlin:Java = 1:2 くらいです。
—–
Q: JavaとKotlinのコードが混在していて困ったことはありませんか?
A: 特にありません。強いて言うなら久しぶりにJavaのコードに手を入れなくてはいけない時に、「あ、Kotlinじゃないからこの書き方できない…」ちょっと残念な気持ちになるくらいです。
OSSについて
Q: 今回初めてOSS Libraryとして公開したと思いますが、どういった理由から公開に至ったのでしょうか?
A: 公開したLibraryはReSwiftというOSS Libraryをリスペクトして作ったものです。なので、OSSに帰したいという気持ちが作ったメンバーの中にありました。そういう経緯からOSS公開に至りました。
—–
Q: OSSで公開することに対して、社内でハードルはありましたか?
A: 作った人が公開したいという気持ちがあれば基本的にそれを尊重したいというカルチャーだったので、ハードルは特にありませんでした。しかし、当然議論はありました。公開することしないことに対するそれぞれのメリットやリスクを比較して、公開に至りました。
おまけ
長々と語っていますが、実はまだストアに公開されているアプリにはほんの一部しかReduxを取り入れられていません… 早く綺麗に作って公開したい!
内容盛りすぎてめっちゃ早口になった気がする😇 たくさんReduxについて話したけど、リリースはもうちょっと待っててね!(現状のアプリは画面間で全然情報共有できてない🙇♀️🙇♀️🙇♀️) #Retty_tech_cafe
— むーむー (@muumuumuumuu) 2017年9月1日
localeに繁體中文を設定してもvalues-zhが選択されない
今回もマニアックなトピックです。
タイトルの通り。ちょっと困ったので調べて見た。
何が起こったか
下記どちらかに当てはまる場合、
- 端末で繁體中文を言語設定で選択している
- アプリ独自のlocale設定で
zh_HK
(中国語-香港)を設定している
res
下の values-zh/strings.xml
が読まれず res/strings.xml
が選択された。
例えば、values/strings.xml
と、
<resources> <string name="test">Test</string> </resources>
values-zh/strings.xml
がある場合、
<resources> <string name="test">測試</string> </resources>
言語設定を繁體中文にしていても測試ではなくTestが表示される。同じ中国語(zh)ならvalues-zh
が優先されると思いきや、そんなことない。
ここでvalues-zh/strings.xml
をvalues-zh-rHK/strings.xml
にすれば測試になってまあ当面困ることはないが釈然としない。
ちなみに確認環境はNexus 6P (OS 7.1.2) 実機。
N以降でおきてるっぽい
そういえばNからLocale周りで変更入ったな?🤔 というのを思い出した。
試しにEmulator (Nexus 5 / API 23) で動かして見たらvalues-zh/strings.xml
が選択されて測試が表示された。
同じEmulator環境でもN系のものはだめ。(Nexus 6P / API 25とNexus 5X / API 23 で確認。)
一瞬Multi localeになった影響でEnglishがlocale listの二番目にきている影響か?と思ってEnglishを削除して見たけどだめ。
よく考えたらそもそもvalues-en
を作ってないテスト環境でも再現したから影響しようがなかった。
Frameworkのコードを読んでみよう
N以降のFrameworkのコードを読んでみる。ResourcesImpl.java#320 あたりから見ていくと読みやすいかな。
この辺りのコードでbestLocale
を取ってきて設定しているっぽい。
320 public void updateConfiguration(Configuration config, DisplayMetrics metrics, 321 CompatibilityInfo compat) { 351 // If even after the update there are no Locales set, grab the default locales. 352 LocaleList locales = mConfiguration.getLocales(); 353 if (locales.isEmpty()) { 354 locales = LocaleList.getDefault(); 355 mConfiguration.setLocales(locales); 356 } 357 358 if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) { 359 if (locales.size() > 1) { 360 // The LocaleList has changed. We must query the AssetManager's available 361 // Locales and figure out the best matching Locale in the new LocaleList. 362 String[] availableLocales = mAssets.getNonSystemLocales(); 363 if (LocaleList.isPseudoLocalesOnly(availableLocales)) { 364 // No app defined locales, so grab the system locales. 365 availableLocales = mAssets.getLocales(); 366 if (LocaleList.isPseudoLocalesOnly(availableLocales)) { 367 availableLocales = null; 368 } 369 } 370 371 if (availableLocales != null) { 372 final Locale bestLocale = locales.getFirstMatchWithEnglishSupported( 373 availableLocales); 374 if (bestLocale != null && bestLocale != locales.get(0)) { 375 mConfiguration.setLocales(new LocaleList(bestLocale, locales)); 376 } 377 } 378 } 379 }
では「何がbestなのか」を評価するためのロジックはどうなっているかというと、LocaleList.java#302 あたり。
302 @IntRange(from=0, to=1) 303 private static int matchScore(Locale supported, Locale desired) { 304 if (supported.equals(desired)) { 305 return 1; // return early so we don't do unnecessary computation 306 } 307 if (!supported.getLanguage().equals(desired.getLanguage())) { 308 return 0; 309 } 310 if (isPseudoLocale(supported) || isPseudoLocale(desired)) { 311 // The locales are not the same, but the languages are the same, and one of the locales 312 // is a pseudo-locale. So this is not a match. 313 return 0; 314 } 315 final String supportedScr = getLikelyScript(supported); 316 if (supportedScr.isEmpty()) { 317 // If we can't guess a script, we don't know enough about the locales' language to find 318 // if the locales match. So we fall back to old behavior of matching, which considered 319 // locales with different regions different. 320 final String supportedRegion = supported.getCountry(); 321 return (supportedRegion.isEmpty() || 322 supportedRegion.equals(desired.getCountry())) 323 ? 1 : 0; 324 } 325 final String desiredScr = getLikelyScript(desired); 326 // There is no match if the two locales use different scripts. This will most imporantly 327 // take care of traditional vs simplified Chinese. 328 return supportedScr.equals(desiredScr) ? 1 : 0; 329 }
まず完全にLocaleが一致するか見ている。ここは一致しないはずなので次へ。
次にLonguageが一致しないか見ている。ここは一致するので次へ。
次にPseudo Localeか見ている。ここは当てはまらないので次へ。
最後ににscriptが一致するかを見ている。ISO15924で繁体字は Hant
と定められていて、これがいわゆる中国語の簡体字Hans
とは別扱いだった。
わざわざコメントでも書いてあった。
There is no match if the two locales use different scripts. This will most imporantly take care of traditional vs simplified Chinese.
まとめ
というわけで、values-zh
は簡体字扱いなので、端末の設定を繁體中文(繁体字)にしても文字が違うということで選択されなかったという結論になった。スッキリ!
LocaleといえばLanguageとCountry (region) くらいしか気にしていなかったけれど、script(文字) も気にしないといけなかった。 Referenceにも当然だけどscriptのことが書いてあった。ただ、これだけだとscriptがmatchingのロジックにどう関わるかわからないんじゃないかなぁ… というわけでたまにはFrameworkのコード読むの大事!٩( ‘ω’ )و
Kotlin初心者から抜け出したい?それなら、
Land of Lispを読んだらどうだろう。 (すみません。9割ネタです。)
- 作者: M.D. ConradBarski,Conrad Barski,川合史朗
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/02/23
- メディア: 大型本
- 購入: 1人 クリック: 18回
- この商品を含むブログ (19件) を見る
Kotlinを初めて数ヶ月だったあなたへ
今年のGoogle I/OでKotlinがAndroidで正式サポートされるとアナウンスされた頃からでしょうか、Kotlin初心者向けの勉強会が一気に増え、実際に自分の周りもAndroidエンジニアを中心にKotlinを触る人が増えてきました。
最初はみなさんこちらの赤べこ本から始める人が多いかと思います。
Kotlinスタートブック -新しいAndroidプログラミング
- 作者: 長澤太郎
- 出版社/メーカー: リックテレコム
- 発売日: 2016/07/13
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
(英語が得意な人であればKotlin in Actionもいいですね)
- 作者: Dmitry Jemerov,Svetlana Isakova
- 出版社/メーカー: Manning Pubns Co
- 発売日: 2017/02/19
- メディア: ペーパーバック
- この商品を含むブログを見る
赤ベコ本、初心者でもとっつきやすくて本当に素敵な入門書です。ただ、入門書も読み終わって初心者からそろそろもう一歩進みたいそこのあなた。
Java(特にAndroid Java1 )をずっと続けてきてからKotlinに移行した人、そんな人におすすめなのが Land of Lisp!
なぜLand of Lispがおすすめなのか
一体どこからLispが出てきたのか?この本は半分Lispについての本ですが、 半分は関数型プログラミングについての入門書です。
Kotlinは関数型プログラミングのパラダイムを取り入れています。
例えば、高階関数。公式ドキュメントを読む時も高階関数(Higher-Order Functions)とかさらっと出てきます。それが一体なんなのか、なんのためにあるのかちゃんと理解していますか?
例えば、遅延評価。Java 8が使えないAndroidでは初めましての人も多いかと思います。Kotlinではvalで再代入不可な変数を宣言することができますが、これをpropertyで持つとなると当然初期化をする必要があります。クラスのインスタンス化時点では取れない情報を使って初期化したい時、遅延評価が本当に強力な機能となります。
Androidの例で恐縮ですが、Activityのpropertyとしてintentのextraを持ちたい場合、下記のようなコードはかけません。なぜならクラスのインスタンス化時点でthis.intent
は正しいintentが入っていないからです。
class HogeActivity : AppCompatActivity() { private val fuga = this.intent.getStringExtra(FUGA_KEY) // 動かないよ! }
そこでvalで宣言するのを諦めてvarにして書けないことはないのですが、イマイチなコードです。
class HogeActivity : AppCompatActivity() { private var fuga = "" // 動くけどイマイチだよ! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) this.fuga = this.intent.getStringExtra(FUGA_KEY) } }
そこで、下記のように遅延初期化すれば解決します。このコードの場合、初めてfuga
にアクセスしようとした時にfuga
が初期化されます。なので、例えばonCreate()
でfuga
にアクセスするとこの時にはthis.intent
に正しい情報が入っているのでfuga
には想定通りの値で初期化されます。これでfuga
をvalで宣言することができます。
class HogeActivity : AppCompatActivity() { private val fuga by lazy { this.intent.getStringExtra(FUGA_KEY) // 動くよ! } }
おまけ
そもそもなんでLand of Lispなんか読み始めたかというと昨年の誕生日に同僚氏が自宅に送りつけてきたからです。(当然wishlistには入れていません。)今まで5人くらいに送りつけて実際に読み始めたのは私が初めてだそうです😇
読了して思ったのは「この本やっぱり頭おかしいな」でした。
最後に。この本を読んで副作用満載のコードをみると発狂しそうになるかもしれませんが責任取れませんので悪しからず。
Dexopenerを使ってKotlinで書かれたfinalなclassをmockする話
[2017.7.27 追記あり]
4ヶ月以上前の話の続きです。下書きにずっと眠っていたのですが、諸事情により公開が遅くなってしまった…
こちらの記事でAndroidでKotlinのmockつらい〜〜〜 😇😇😇😇って話をしたら、
id:tmurakami さんがコメントでDexOpenerを教えてくださったので試してみました。 (id:tmurakamiさんがDexOpenerの作者さんです。)
使い方
上記RepositoryのREADMEにある通り、testコードのcompile時に対象libraryを指定して、 testInstrumentationRunner
を指定するだけです。
dependencies { androidTestCompile 'com.github.tmurakami:dexopener:0.9.1' }
android { defaultConfig { testInstrumentationRunner "com.github.tmurakami.dexopener.DexOpenerAndroidJUnitRunner" }
これだけでfinal classをmockできるようになります!楽!!
中で何が起こっているのか
せっかくなのでlibraryの中身を読んでみました。Open source最高!!
こちらのlibraryは同じ作者さんの classinjector
というlibraryに依存を持っているのでコードを読むときは手元にcloneして読むことをおすすめします。
読み間違いなどあったらすみません!ご指摘いただけると嬉しいです!
ざっくり概要
[2017.7.27 追記]
最新version( 0.10.3
)で方針が変わってかなり早くなりました!圧倒的…!
ただし、minSdkVersionが16にあがっていましたのでご注意を。
version | time |
---|---|
0.9.1 | Total time: 3 mins 37.83 secs |
0.10.3 | Total time: 9.162 secs |
dexファイルを取って来て、下記のクラス・メソッドのアクセス修飾子からfinalをマスクしてbit反転していました。
- class
- inner class
- method
というわけでクラスやメソッドが増えれば増えるほど処理に時間がかかります。
コード詳細
まずはTest Runner ( DexOpenerAndroidJUnitRunner.java
) のnewApplicationで DexOpener#install()
をコール。
このメソッドの中で ApplicationInfo
を持ったbuilderを生成し、 DexOpenerImpl
に処理を移譲します。
この時、Builderに BuiltinClassNameFilter
を設定して、buildinのclassはmockできないようにしています。
BuiltinClassNameFilter.java
private final String[] disallowedPackages = { "android.", "com.android.", "com.github.tmurakami.classinjector.", "com.github.tmurakami.dexmockito.", "com.github.tmurakami.dexopener.", "com.github.tmurakami.mockito4k.", "java.", "javax.", "junit.", "kotlin.", "kotlinx.", "net.bytebuddy.", "org.hamcrest.", "org.jacoco.", "org.junit.", "org.mockito.", "org.objenesis.", };
ApplicationInfo.dataDir
に code_cache/dexopener
でcacheを生成した後、
ClassInjector
を使ってApplicationInfo
からclasses*.dexファイルを取って来て、
AndroidClassSource.java
@SuppressWarnings("TryFinallyCanBeTryWithResources") private ClassSource newDelegate() throws IOException { List<ClassSource> sources = new ArrayList<>(); ClassNameReader r = new ClassNameReader(classNameFilter); ZipInputStream in = new ZipInputStream(new FileInputStream(sourceDir)); // sourceDirはApplicationInfoから取得したやつ try { for (ZipEntry e; (e = in.getNextEntry()) != null; ) { String name = e.getName(); if (name.startsWith("classes") && name.endsWith(".dex")) { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[16384]; for (int l; (l = in.read(buffer)) != -1; ) { out.write(buffer, 0, l); } ApplicationReader ar = new ApplicationReader(ASM4, out.toByteArray()); sources.add(dexClassSourceFactory.newClassSource(ar, r.readClassNames(ar))); } } } finally { in.close(); } return new ClassSources(sources); }
[2017.7.27 追記]
コメントいただきました通り、最新版ではASMDEXではなくdexlib2を使う方針に変わっていました。失礼しました。
下記のfinalをbit反転しています。
- class
- inner class
- method
visitorパターンで実装されていました。ASMDEXのClassVisitor を使っているためかな?
ApplicationOpener
@Override public ClassVisitor visitClass(int access, String name, String[] signature, String superName, String[] interfaces) { int acc = access & ~ACC_FINAL; return new ClassOpener(api, super.visitClass(acc, name, signature, superName, interfaces)); }
感想
本体側をKotlinで書かれているアプリのinstrumented auto testしたい〜〜って時に選択肢の一つとして良さそうです🎉🎉
[2017.7.27 追記]
パフォーマンスもかなり良いので、minSdkVersionが16以上という方はぜひ使ってみたらいいと思います!🎉🎉
Kotlin 1.1.2-5くらいからSAM変換がちょっと変わってcrashが起きた件
Kotlinのバージョンあげたら既存コードがcrashしたのでちょっと調べてみたメモ。
何が起こったのか
kotlin のversionを1.0.5-3から1.1.2-5にあげたら既存コードがcrashするようになった。具体的には、こんな感じのIllegalStateExceptionが吐かれる。
java.lang.IllegalStateException: invoke(...) must not be null
何が悪かったか
Kotlin側に渡すJavaで定義したFunction0
のオブジェクトでinvoke()
をoverrideした際にreturn nullしていたのが悪かった。Javaでいうvoidに当たるUnitのインスタンスをreturnすることで落ちなくなる。
new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() { @Override public Unit invoke() { doSomething(); return Unit.INSTANCE; // return nullするとcrashになるよ } });
Android Studio(v2.3)だとinvoke()メソッドを補完するときにreturn nullで補完してくれるからいちいち手で書き換えないといけない_:(´ཀ`」 ∠):
Javaでいうvoidはkotlinのnull許容・非許容と離れた概念だからどっち返しても大丈夫でしょ、とか思ってたら思わぬところで痛い目をみる。
で、問題はなんだったの?
実はこのcrashが発生するのにいくつか条件があって、
- JavaでFunctionN系のinvoke()メソッドをreturn nullしてoverrideする
- 1で定義したインスタンスをkotlinのコードに渡す
- 2のkotlinのコードからさらにjavaのコードに渡して、invoke()ではなくSAM変換後のメソッドとして実行する
3がちょっとなんて言ったらわからないのでコードを交えて説明。
適当なAndroidのコードで試したものです。まずはFunction0
public class SamConversionExperimental { public void invokeFunction() { new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() { @Override public Unit invoke() { doSomething(); return Unit.INSTANCE; // return nullするとcrashになるよ } }); } private void doSomething() { } }
その後、受け取ったkotlinコード側でSAM変換がかかるような感じで実行してやればcrash. 手元だとRunnable#run() が走るようにしてcrashさせた。
(Runnableは単一メソッドrun()だけを持つインターフェイス)
fun invokeFunctionFromKotlin(function: () -> Unit) = Handler(Looper.getMainLooper()).post(function)
SAM変換変わったね?
エラー文から察するに、この辺りのチェックコードがversion上がって入ってきたっぽい。 github.com
public static void checkExpressionValueIsNotNull(Object value, String expression) { if (value == null) { throw sanitizeStackTrace(new IllegalStateException(expression + " must not be null")); } }
で、このメソッドがどこから呼ばれているかというと、この辺り。
private fun analyzeTypesAndRemoveDeadCode(): Map<AbstractInsnNode, Type> { val insns = methodNode.instructions.toArray() val frames = analyze(internalClassName, methodNode, OptimizationBasicInterpreter()) val checkedReferenceTypes = HashMap<AbstractInsnNode, Type>() for (i in insns.indices) { val insn = insns[i] val frame = frames[i] if (insn.isInstanceOfOrNullCheck()) { checkedReferenceTypes[insn] = frame?.top()?.type ?: continue } else if (insn.isCheckParameterIsNotNull() || insn.isCheckExpressionValueIsNotNull()) { // ここにチェックが入っている checkedReferenceTypes[insn] = frame?.peek(1)?.type ?: continue } } val dceResult = DeadCodeEliminationMethodTransformer().removeDeadCodeByFrames(methodNode, frames) if (dceResult.hasRemovedAnything()) { changes = true } return checkedReferenceTypes }
なんかこの辺りのcommitでcheck増えてそうな気配を感じるのでこれかなぁ