eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

XCode 14にアップデートしてflutter buildが通らなくなった際の対処

原因

  • XCode 14からCocoaPodsの生成物(Pod)にも署名を要求するようだ
    • n=1なので、他の不要な場合もあるかもしれない
  • Issueがある

github.com

対応

  • cocoapods-pod-signをインストール
  • Flutterは /usr/bin/gem のPATHを優先的に検索するようなので、複数バージョンのgemをインストールしてる場合は標準gemでinstallするほうが面倒がなくて良い
gem install cocoapods-pod-sign
platform :ios, '15.0'
plugin 'cocoapods-pod-sign'

Flutter + Redux Architectureでパフォーマンスチューニング

パフォーマンスチューニングが必要になった理由

  • プロジェクトでRedux Architectureを採用したが、求める機能に合致するライブラリがないためチーム内で自作した pub.dev
  • これにより求める機能が実装されたが、プロジェクトでは特定条件下でState更新が1000回 / 秒を超える場合がある
  • UI = f(State)を愚直に実装していたため、Widget.build()が1000回 / 秒走ることとなり、パフォーマンスに明らかな悪影響が出た

解決方法

  • Stateを流すためのStreamと、WidgetをビルドするためのStreamを分けた
  • StateをハンドリングするためのStreamは取りこぼしがあってはならない(イベントハンドリングなど)ため、実装をそのままとした
  • それとは別に、WidgetをビルドするためのStreamを追加した
    • こちらは最大で60fpsでデータが流れる仕様となっているため、仮に1000回 / 秒State更新が走ってもWidgetに反映されるのは60回 / 秒が最大である
  /// レンダリング用に流通量を制限したStreamを生成する.
  BehaviorSubject<TState> _initializeRenderStream(Duration renderingInterval) {
    final result = BehaviorSubject<TState>.seeded(state);
    _subscription.add(
      Stream.periodic(
        renderingInterval,
        (computationCount) => state,
      ).distinct().listen(result.add),
    );
    return result;
  }
  • これによりWidget.build()のスパイクによる異常rebuildを回避してパフォーマンスが向上した

flutter build ipaのad-hocビルドが異様に遅い問題の対応

発生した問題

  • AppStore用ビルドは20分前後でビルドが完了する
  • 検証用のAd-Hocビルドが60分以上時間がかかる
  • XCode 13.2.1, Flutter 2.10.4

試したこと

  • Swift/Clangの最適化レベルを変更 -> 効果なし
  • dSYM出力無効化 -> 効果なし

突き止めた問題

  • --export-options-plist でAd-Hoc用ビルドにするとビルド時間が3倍に伸びる
  • 試しにビルド20分のAppStore用ビルドをAdHoc用のplistに切り替えるとビルド時間が同じくらいに遅くなった
  • ログを観察しても、ipa出力時に異様に時間がかかっているので間違いなさそう

plist仕様を確認

  • plist仕様を確認し、AdHoc用途で不要そうでdefault trueの項目をチェック
  • これらを明示的にfalseにしたところ、ビルド時間が20分まで早くなった
 <key>uploadBitcode</key>
    <false/>
    <key>compileBitcode</key>
    <false/>
  • compileBitcode が一番怪しいけど、そこまで切り分けはツラいのでこれでヨシとする
  • なんでこのオプションで異様に時間がかかるのか(appstoreビルドではtrueでも影響ないのか)はよくわからない

dartのFreezedとPImplイディオムでメンバ保護を行う

前提

Flutterの開発でReduxアーキテクチャを採用している。

Stateの管理に freezed を使うと、簡単な代わりに全てのメンバがpublicになってしまう。

一部のメンバはprivateにしたり、計算済みの値をキャッシュしたい。

改善

概念。

@freezed
class ExampleStateImpl with _$ExampleStateImpl {
  factory ExampleStateImpl({
    /// このデータはpublicにしたくない.
    /// dataListの合計計算は必要になる時まで計算を遅延したい
    required List<int> dataList,
  }) = _ExampleStateImpl;

  const ExampleStateImpl._();
}
class ExampleState {
   // Stateの実装を隠す
   final ExampleStateImpl _impl;

   factory ExampleStateImpl() {
       return ExampleStateImpl._(ExampleStateImpl(dataList: []));
   }

   ExampleStateImpl._(this._impl);

   final int? _dataListSumCache;
   
   int get dataListSum {
      // 合計値を計算して返す。
      // 計算済みのキャッシュがあればそっちを返す
   }

   ExampleState addData(int newValue) {
      return ExampleState(_impl.copyWith(
          // Freezedの機能を使ってデータコピーを返す
      ));
   }
}

実際はもっと複雑なパターンとなるが、Freezedのメンバーを直接見せたくないけどcopyWithの恩恵は欲しい場合に使う。

dartでKotlin.internal funっぽいことをする

dartのinternal fun

dart 2.15には)ない(たぶん)。

特定packageだけでアクセスしたいメソッドとか作れない。

作ったらみんなアクセスされてしまう。

解決方法

// src/example.dart
class Example {
  String _value = '';

  String get value => _value;
}

extension ExampleSetValue on Example {
  void setValue(String s) {
    _value = s;
  }
}
library example;

// exportするときに、Extensionを隠してやる
export 'src/example.dart' hide ExampleSetValue;

これで行儀良く使っている限りは外部のpackageはExample.setValue()をコールできない。

package内は 'src/example.dart' を直接importする。

無理矢理importすれば使えなくもないので、lintと併用で。

ついにGSuiteの無料版がシャットダウンされるので移行した

GSuite無料版

  • @gmail.com ではなく、独自ドメインのGSuiteが条件付きで無料で使えていた
    • 10年ほど前まで(いまほどGSuiteが普及してない時期)は、無料で10アカウントまで独自ドメインのGSuiteを利用することができた
  • 5年前時点ではもう無料では作れず、有料プランのみに切り替わってた
    • 普及したんだね!

2022/5月にシャットダウン

  • 無料登録したGSuiteアカウントは2022年2月現在まだ無料で使える
  • ただし、無料提供が5月で終了すると発表された(5月までに切り替えが必要で、その後7月までは無料で使える)

さよなら開発アカウント

  • 開発専用を含めて、4つのアカウントを運用していた
    • これだと毎月2500円以上
    • ネトフリの4Kプランよりも高い
  • Admin用と副業用の2アカウントを残し、アカウントを削除
    • これで毎月1300円

無料はいつか終わる

  • あいつも有料化する
  • こいつも有料化する
  • けど、今日じゃない

と思い続けて10年、ついに有料化のビッグウェーブにのまれたという話

Kotlin 1.6に変更してproguardをかけたら通信周りが死んだので直した

事象

  • アプリにログインできない

調査

  • APIcurlすると、正常に動作する(サーバーは正常動作してる)
  • Debugビルドで実行すると、正常に動作する(プログラム自体は正常である)
  • サーバーのログを確認すると、送信値のValidationに失敗している

Releaseビルドでの動作

  • APIRetrofit2 を経由でコールしている
  • JSON変換は moshi を利用している
  • このどちらかに問題が発生した可能性が高い

APIサーバーにどんな値が送られているのか

  • 送信値の直接的なログはない
    • 値のValidationに失敗した、という情報しか出してない
  • まずは手元のアプリでどんな値が実際に送信されているかLogcat

github.com

  • 結果として、snake_caseとして送信しなければならない値がcamelCaseとしてJSON化されていることがわかった
    • moshiには @Json("user_name") のようにAnnotationを付与しているので、Annotationが殺されているのがわかった
data class ExampleLogin(
   @Json("user_name")
   val userName,
)
  • 器用にsnake_caseがcamelCase化処理されるとは考えにくいので、おそらくdata classのプロパティ名がそのまま使われているのだろうと予想する

proguardルールを変更した

  • どうやってもダメ
  • 全部のclassやinterfaceをkeepしてもAnnotationが剥がれてしまう

Annotationを付け替えた

  • filedに付与すると、proguardを通してもAnnotationが生きている
  • なんとなくだが、Proguardの最適化によってField/Methodの扱いが変わってしまったように思う
  • ただし、このままだとProguard前のデバッグで不正になる
data class ExampleLogin(
   @JvmField
   @field:Json("user_name")
   val userName,
)

Annotationをさらにつけた

  • モリモリ全部盛りで、FieldとMethodに付与した。
  • これでDebugもReleaseもOK
  • 変換用Classは全部Generatorが吐き出しているので、テンプレートだけ対応してあげてヨシ.
data class ExampleLogin(
   @JvmField
   @field:Json("user_name")
   @Json("user_name")
   val userName,
)

Kotlin 1.6はなかなか色々大変だったが、これでひと段落のはず