しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

iOSアプリの圧縮済みAssets内の画像のサイズを調べて小さくする

せっかくインストールしたアプリを削除されたくないので、インストール後のバイナリサイズを削ってみました。
今年は細かい作業やりたくないので、忘れないうちに記録に残しておくことにしました。

まず見るWWDCのセッション

App ThiningとかAssets Catalogのことを知っておけば、アーカイブ前にもろもろ対策できます。

アプリのサイズ

手元やCIでarchiveした.ipaのサイズと、App Store上のサイズと、インストール後のサイズの3つはそれぞれ異なります。

これは、App ThiningやApp Store向けの暗号化などの影響によるもので、 主に後者の理由で、App Store上で表示されるアプリサイズ(やApp Store Connectに表示される想定サイズ)は、端末にインストール後のサイズより大体大きくなります。

Assetsの中でかさばっているものを調べる

Xcodeでarchiveすると、.appの中にAssets.carという圧縮ファイルが生成されます。
この中で大きいものは、端末にインストールした後も嵩張ることが多いので、そこを重点的に削っていきます。

assetutilというツールを使って、Assets.carのメタ情報を一度展開します。

xcrun --sdk iphoneos assetutil --info Assets.car >> extracted

ここで吐き出されたファイルはJSON形式で、アセットごとに

{
  ...
  "AssetType" : "Vector",
  "SizeOnDisk" : 1234567,
  "Name" : "hoge",
  ...
}

というような形式で吐き出されます。
SizeOnDiskのところに注目すればいいので、気合のある人はJSONをパースしてソートします。

気合のない人は、

cat extracted | grep -n5 -E "\"SizeOnDisk\" : [0-9]{7}"

のようなコマンドを打てば、大体1MB以上の大きなファイルが引っかかってくれます。

画像を小さくする

Assets Catalogのコンパイラオプションを一応確認しておきます。

  • 何も指定していない場合、上のセッションのように適切にAssetsが使われていれば、PNGはいい感じになっている場合が多いので、減色処理などをしてもあまり効果がない
  • JPEGはそのままなので、大きい場合は適宜リサイズやqualityを下げたりが必要
  • PDFなどベクター画像に関しても、なぜかそのままのことが多いので、ファイルを小さくするか、JPEG,PNGなどの他のフォーマットの変換する

App Store上のサイズは、少ししか減らなかったのですが、端末インストール後のサイズは、ほとんど上の手法で削減したのサイズと同じくらい小さくなりました。

Swiftのコードにシェルスクリプトでfinalをつける

こんばんは、final警察です👮

既存のコードにfinalをつけるのがめんどくさかったので、シェルスクリプトで一括finalをつけてみました。

Bashスクリプト

Bashわかんねーって言いながら、Mojaveまでデフォルトだった3.2でも動くように書いてみました。

#!/usr/bin/env bash

TARGET_DIR="Sources/"
while read result
do
  RESULT=(${result//:/ })
  FILE_PATH=${RESULT[0]}
  CLASS_NAME=${RESULT[2]}
  if [ `git grep -IE "class.+:.*[, ]$CLASS_NAME\W" $TARGET_DIR | wc -l` -eq 0 ]; then
    echo "Added 'final' to $CLASS_NAME."
    sed -i -E "s/^( *)class $CLASS_NAME/\1final class $CLASS_NAME/" $FILE_PATH
  fi
done < <(git grep -I "^ *class [A-Z]" $TARGET_DIR)

ちょっといじればJavaとかにも使えると思います。

解説

git grep

git管理下の場合、普通のgrepより速いです。
デフォルトで8スレッドで動いてくれます。 -Iでバイナリファイルを除いた検索ができます。

もっと速いgrepも探すとあります。

class.+:.*[, ]$CLASS_NAME\W

継承しているクラスを探します。 $CLASS_NAMEBaseの時に、

class Hoge: Base {

のような書き方にマッチします。
末尾の\WHoge2のような部分一致のケースを弾いています。

Lintが入っている前提の、ある意味性善説的なマッチなので、

class Hoge:Base {

のようなイレギュラーなケースに対応する場合は、class.+:(.*[, ]){0,1}Base\Wみたいに書いておいたほうが安全です。

あと、typealiasを使って継承みたいな書き方をされると、対応できません。

while read ~ done < <(expr)

forだとスペース入りの文字列がsplitされてしまうので、こう書いています。

なぜfinalをつけるか

保守性の文脈だと、Swiftでは基本的に継承は使わず、protocolを使うことになってるはずなので、不用意な継承を避けるためにもfinalをつけたほうが良いです。
その他の言語でも、大体コーディング規約などで推奨されているはず。(たとえば書籍だとEffective Java(3rd Edition)のItem 17とか)

Effective Java (English Edition)

Effective Java (English Edition)

とくにSwiftの場合、C++などと同じく、継承される可能性がなくなれば、実行時に実際に呼ばれるメソッドやプロパティを決めるDynamic Dispatchの必要がなくなるため、パフォーマンスが上がります。

developer.apple.com

Dynamic Dispatchの話はまた今度。

締め

厳密にやるなら、がっつりSwift Syntaxとかでやる感じでしょうか。

仕事しんどいし、腰の左あたりがめっちゃいたいので、しんどくなくなって忘れてなかったらやります。

LLDBのcommand regexを使って動的なaliasを作る

LLDBのcommand regexを使うと、zshのanonymous functionのaliasみたいな感じで、 引数に置換かけながらLLDBコマンドを実行するaliasが作れます。

これを利用することで、簡単なコマンドならば、Pythonスクリプトを用意する手間が省けます。

やりかた

構文はシンプルで、

command regex <cmd-name> [s/<regex>/<subst>/ ...]

で、置換パターンはsedのようにs///で書けば問題ないです。(複数可)

これを.lldbinitに記載しておけば、毎回LLDBが立ち上がるたびに、コマンドがセットされます。

scior.hatenablog.com

一例として、XcodeのunsafeBitCast(:to)のaliasを紹介します。

command regex cast 's/(.+) (.+)/expr -l swift -- let $casted = unsafeBitCast(%1, to: %2.self)/'

一般にある置換のように、()で囲んだ部分は%1, %2...で利用できます。

この場合、第1引数のインスタンスやアドレスを、第2引数のタイプにキャストして、$castedに格納します。
上の一行を、~/.lldbinitなどに記載しておけば、castというコマンドが登録されます。

(lldb) cast view.subviews[0] UIButton
(lldb) po $casted.frame
▿ (134.5, 305.0, 106.0, 57.0)
  ▿ origin : (134.5, 305.0)
    - x : 134.5
    - y : 305.0
  ▿ size : (106.0, 57.0)
    - width : 106.0
    - height : 57.0

この書き方の場合、letを利用していて書き換えが効かないため、何度もキャストしたい場合は別途工夫がいるかも知れません。