M1 Max Macbook Pro(14inch)の所感
購入したスペック
環境構築で躓いたところ
Android SDKをAndroid Studioからインストールも認識もできない
- なにかに引っかかってエラーらしい
- IntellijのAndroid Pluginに付属しているGUIでインストールするとAndroid Studioからも認識される
- Apple Silicon環境ではemulatorコマンドが消されているので、Android Studioのインストーラーがコケる
- emulatorだけcanaryチャンネル(--channel=3)で手動でインストールすると以後正常動作する
javacがクラッシュする
- JDK9から付属しなくなったいつものjavax
- ごちゃごちゃやってるうちに治った
Android Emulatorが起動しない
- Android StudioのAVD Managerで作成すると常に失敗する
- コマンドラインで作成すると成功する
- stableチャンネルに"emulator"がない(Apple Siliconだけ?)ようなので、canaryから取り寄せる
sdkmanager --channel=3 emulator avdmanager create avd -n instrumentation -k "system-images;android-31;google_apis_playstore;arm64-v8a" -d "Nexus 9"
Flutterアプリビルド速度
- VSCodeからdebugビルドが30秒〜15秒 -> 9秒〜10秒に短縮
Androidアプリビルド
- Gradleコマンドでフルビルド75秒 -> 65秒に短縮
費用対効果
- ビルド速度だけを見たら、40万円出すのは微妙と思うかも
- 何やってもほぼファンはまわらず、静かに作業できる
- 全部のUnit Testを並列実行させたらファンが回ったので、ファンは壊れてなさそう
- 作業中も概ねキビキビ動いてくれる
- 逆に言えば、40万円払って出先でもデスクトップ(より多少劣るが)並のスペックを手に入れたと思えばまあ良いかも
- アップグレード先としてはアリ、高性能なMacを必要とせず既にデスクトップで強力な母艦があるなら微妙
- けど超静か
- 静かで強力な開発環境がほしいなら(ARMでよければ)アリ
Android StudioとVSCodeの開発効率のメモ
コード生成
- equals系のオーバーライドで差が出る
- Android Studioは割と素直にできるし、data classは自動的に行われる
- VSCode + Dartは標準機能に無いのでExtensionを追加
- VSCode + Dart Extensionは型の単純なtypedefを認識してくれないので、一部コード補完が効かなくなる
リファクタリング
- Android Studioはリファクタリング後のコード整合性が保たれやすい
- リファクタリングをやりやすい環境
- VSCode + Dartはリファクタリング後にimportのファイルパスが壊れたりするパターンが多い
- 単純なClass内のprivate変数リネームも遅い場合がある。原因は不明。
- なので、リファクタリング効率はよくない
プロジェクト環境整備
IDEの快適さ
Flutterアプリ開発環境の長所短所
M1X出ないのかよ!!
Apple発表会で出なかったので、手持ちの環境の再整備を行うことにした
各環境のレビュー
Intel Mac mini 2018 + Windows
- i7 6C12T / RAM 64GB
- 従来の開発環境
iOSアプリビルドにはmacが必須なので用意した環境。 WindowsからVSCode Remoteで接続して開発。
- 利点
- 欠点
- 根本的にi7が遅いのでビルドも遅い。
- 騒音がひどい。
Intel Mac Book Pro 2020
- i7 4C8T / RAM 32GB
- 持ち運び用
リモートワークで自宅内ノマドする機会が増えたので用意した環境。
- 利点
- 欠点
Windows
Ryzen 3950X 16C32T / RAM 64GB
利点
- ビルドが早い
- Macと比べて動作が静か
- 欠点
- iOSビルドができない
- Flutter以外の周辺ツールと日本語環境のかみ合わせが悪い
- よく文字化けする
- flutter pub getでmklinkできずにコケる
- 管理者権限でpub getすれば通るけど気持ち悪い
Ryzen 3950X + Windows / WSL2
- 利点
- ビルドが早い
- Macと比べて動作が静か
- 欠点
- iOSビルドができない
- adb接続にトリッキーなことをしなければならない
- Android Studioとか開けないのでAndroid Kotlin側のコードをいじるときに不便
Ryzen 3950X + Ubuntu
最終的に
- Mac mini + VSCode Remoteに戻った
- 安定は大事
- 騒音対策としてアクティブノイズキャンセリングイヤホンを使った
- ものすごい効果的。超便利
Android12対応でやったこと
もうすぐAndroid 12がリリースされるので、最低限対応した内容をメモする。
Android Studio AF対応
- 新AndroidStudioにビルドツールを切り替えた
UnitTestをRobolectricからInstrumentation Testをメインに変更
- Android Studio AFから、Robolectricが実行しづらくなった(できなくはないが、使いにくい)
- なので、Instrumentation Testのみを行う方向に切り替えた
- それに合わせてCIも変更した
- CIでエミュレータを作成&起動してInstrumentation Test
Lint対応
- Android 12でLint系が更新されたので、チクチクと変更
Activity.exported
属性の対応
- AndroidManifest.xmlのActivityタグで、exported属性設定が必須になった
- 内部・外部を問わず、aar内部で利用されているActivityにも適用されるので、ライブラリが古いとビルドが通らない場合がある
- ビルドツールチェインがタコなので、問題が起きてももとのライブラリ名が不明
抽出
- API 30の状態でapkをビルド -> 分解してAndroidManifest内のActivityを総チェック
- exported属性がついていないActivityを洗い出して修正
androidx.test
が内部で使用しているActivityも、古いバージョンだとexported属性が付いてないのでバージョンを更新
PendingIntentのフラグが厳密化されたので対応
androidx.work
ライブラリはalpha版に切り替えないといけないので、stableが早く出てほしい
SONYのHDDレコーダーのリモート予約が正常動作しなくなったのでエンジニア的カンで直した
利用機器
- SONY製 BDR-BDZ-FW2000(容量2TB, ホームサーバー機能対応のモデル。以下レコーダー)
- マンション標準のネットワーク(グローバルIPは割り当てられない, 1家庭500~800MBps程度の速度が最大)
- Google Nest Wi-Fi(ただしレコーダーはハブを介して有線接続)
事象
- レコーダーのリモート予約機能が「レコーダーがネットワークに繋がっていない」という旨のエラーを表示してしまった
- SONYのQ&Aはすべて試したが改善しない
- 利用価値の半分がリモート予約(外出先で子供が「コレ予約してよ!」って言うことが多い)
観察
- レコーダーのネットワークチェックは正常
- そもそもホームサーバー機能の一部であるリモート視聴は行えるので、ネットワークは接続できている
- 無線・有線を切り替えたが共に問題ない
- iOS / Android両方で同じ事象
考察
- アプリの挙動を見ると、リモート予約(POST/PUT)系だけではなく、「予約済み番組一覧」のGETが行えない
- アプリのレビューやTwitterの検索をしてみるが、一斉不具合は起きていない
- レコーダー <--> SONYサーバー <--> アプリの経路のうち、レコーダー <--> SONYサーバーの部分に(俺の環境のいずれか)問題がある恐れが濃厚である
更なる考察
- アプリ側 / 経路内のプログラムの問題として、Invalidな予約データを受け取る -> 例外吐いて停止している恐れがある
- レコーダーは年単位で予約している番組(ガイアの夜明けとか)が存在している
- 古い番組のDate系に問題があるのではないか?
行動
- 一旦すべての予約データを(後で治せるようにメモして)削除する
- レコーダーを再起動する
- これで無事に復旧した
- 正確なところはわからないが、サーバーやアプリ側のValidationに問題があったのではないだろうか?
GCPを使って子供関係の通知をLINEに統合する
なにをやったのか
- 子供が保育園・小学校・学習塾に通っている
- それぞれ、お知らせがWebに掲載される(日記とかも含む)
- 個別連絡はメールで届く(学年単位の緊急連絡とか)
- いちいち学校のウェブサイトで確認したりメールを検索するのが面倒なので、LINEに通知を統合した
LINEの通知はIFTTTを使う
Web Hookへの流し込み戦略
スクレイピング
- 保育園や小学校、当然ながらRSSのような便利ソリューションは存在しない
- なので、Firebase Functionsでポーリングすることにした
- Firebaseの認証処理がスキップできる(デフォルトでAdmin権限が割り当て)られるので、各種処理が楽になる
- 基本的に httpトリガー を使って、任意のタイミングで呼び出せるようにする
- Firebase Functions自体にもスケジューリング機能はついているけど、 Cloud Scheduler でKickするほうが再設定やデバッグが楽なので、スケジューリングは設定していない
- 学校のCMSに負荷をかけるのは本意ではない(し、そこまでWebページの更新頻度は高くない)ので、cronは1時間に1回行ってる.
キャッシュ処理
- IFTTTのWebHookにはキャッシュがないので、単純にCron実行すると同じ通知が何度も流れてしまう
- そのため、 Firestore に通知済みのキャッシュを放り込んで、キャッシュが存在したらHookをスキップするようにする
通知とキャッシュのサンプルコード
- 通知のタイトル・本文・リンクURLの組み合わせをMD5化して、それをキャッシュキーにする
- キャッシュヒットしたら、それは通知済みなのでスキップ
var firebaseApp *firebase.App var firestoreClient *firestore.Client var webhook iftttWebhook.IFTTT func init() { if app, err := firebase.NewApp(context.Background(), nil); err != nil { panic(err) } else if fs, err := app.Firestore(context.Background()); err != nil { panic(err) } else { firebaseApp = app firestoreClient = fs } webhook = iftttWebhook.New("割り当てられたAPIキー") } func md5sum(s string) string { sum := md5.Sum([]byte(s)) return hex.EncodeToString(sum[:]) } func notifyWebPageLink(ctx context.Context, title string, body string, link string) { uid := md5sum(title + "/" + body + "/" + link) ref := firestoreClient.Collection("ifttt").Doc("web-notify").Collection("cache").Doc(uid) if _, err := ref.Get(ctx); status.Code(err) != codes.NotFound { log.Println(fmt.Sprintf(" * Exist Document: %v", ref.Path)) return } log.Println(fmt.Sprintf(" * Put Document: %v", ref.Path)) log.Println(fmt.Sprintf(" * Title: %v", title)) log.Println(fmt.Sprintf(" * Body: %v", body)) if _, err := ref.Create(ctx, map[string]interface{}{ "created_at": time.Now(), "title": title, "body": body, "link": link, }); err != nil { log.Println(fmt.Sprintf("Firestore write failed: %v", err)) } webhook.Emit("通知ID", title, body, link) }
// これをhttpトリガーでPublish func CronHoikuenWeb(w http.ResponseWriter, r *http.Request) { うまいことスクレイピング()... w.WriteHeader(http.StatusNoContent) } // これをhttpトリガーでPublish func CronShogakkouWeb(w http.ResponseWriter, r *http.Request) { うまいことスクレイピング()... w.WriteHeader(http.StatusNoContent) }
GmailからIFTTTへの通知戦略
- 塾も入退室時にメールで通知してくれる
- 保育園や小学校は緊急連絡(遠足延期とか)がメールで来る
- なので、メールもLINEにGOだ。
GASをcronで呼び出す
- Firebase Functionsだと、自分のGmailへのアクセスが面倒(認証とか)
- なので、Google Apps Scriptから通知を飛ばすことにした
- GASはGmail APIを使うとOAuthが自動的に開始されるので、認証がらく。
- 認証状態を保持したまま、Cronを走らせられる
- 設定項目もユーザーフレンドリー(cron構文使わなくてよい)
- メールはGoogleインフラなので、1分に1回ポーリングしても大丈夫そう
- リアルタイム性を重視したいならpub/subに流して〜とかあるけど、面倒な割にそこまでする必要はない
- gmail検索条件をそのまま利用できるので、
from:example@example.com newer_than:1h
とかできるし、めちゃくちゃ複雑なクエリも作れる- けど俺はシンプルにfromと直近のn時間のみでクエリを投げた
- LINE通知は嫁さんにも届くので、間違ってもFANZAの課金通知を飛ばすわけにはいかねぇんだよ!!
UrlFetchApp.fetch
の投げ先は、Functionsで作ったhttpトリガー.- POSTリクエストでパラメータを受け取り、それをそのままさっきの
notifyWebPageLink()
に流すようにした
- POSTリクエストでパラメータを受け取り、それをそのままさっきの
// Functions側はこう func WebHookFromGmail(w http.ResponseWriter, r *http.Request) { ctx := r.Context() bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { panic(err) } else { log.Printf("read body: %v", string(bodyBytes)) } var request map[string]string if err = json.Unmarshal(bodyBytes, &request); err != nil { panic(err) } else { log.Printf("parsed: %v", request) } title := request["title"] body := request["body"] link := request["link"] notifyWebPageLink(ctx, title, body, link) // ok w.WriteHeader(http.StatusNoContent) }
// GAS側はこう。 function onCron() { notifyGmail("gmailの検索条件", "通知タイトル", "リンクURL") } function notifyGmail(query, from, link) { GmailApp.search(query).forEach(function(thread){ thread.getMessages().forEach(function(message) { console.log("============================"); console.log(message.getSubject()); console.log(message.getBody()); notifyExternalWebHook(from, message.getSubject() + " :: " + message.getBody(), link); }); }); } function notifyExternalWebHook(title, body, link) { var requestBody = JSON.stringify({ title : title, body: body, link: link, }); console.log(requestBody); var options = { method : "POST", payload : requestBody, }; UrlFetchApp.fetch("自分のエンドポイントURL", options); }
最後に
- セキュリティもくそもない書き捨てコードなので、何かあったら自己責任で頑張ろうね!
- あと、IFTTTのWebHookにキャッシュつけてくんねかな!!
ビルドの並列化をしすぎてCPU占有率100%になってしまったAndroid Studioプロジェクトの対処
何が起きたか
- モジュール分割や様々な工夫をしてビルド時の並列性を高めた
- 高めすぎて、Ryzen 3950Xの
16コア 32スレッド
の能力を超えた並列性を持ってしまった - その結果、Gradleのビルドを行うとすべてのCPUリソースを完全に食い尽くすようになった
- ZoomとかDiscordとか、セッションが途切れるレベルで食い尽くす
- メモリ空き容量は30GBくらいあるので大丈夫なはず
解決方法
- /gradle.propertiesに次のオプションを加えて、プロセスの優先度を下げた
- Gradle 5.0からのオプションらしい。デフォルトは
normal
が設定される。
org.gradle.priority=low
その他
- まさか並列実行性でRyzenが遅れを取るとは思いもしなかった