【2022年版】OpenCV for Androidのlibopencv_java4.soのバイナリサイズを減らし、APKのサイズを減らす

# はじめに

## Qiitaマイルストーン

2019年01月20日に投稿した記事「[OpenCV for Androidのlibopencv_java4.soのバイナリサイズを減らし、APKのサイズを減らす](https://qiita.com/tfandkusu/items/188d51b5c1c76dd540fd)」ですが、おかげさまで3年かけて10LGTMを達成して、Qiitaマイルストーンで紹介されました。

https://twitter.com/qiita_milestone/status/1483700758906392576

## 現在ではできない方法

そこで3年前のやり方は2022年1月現在で可能なのかやってみたところ、できなかったです。そこで新たにやり方を調べたので紹介しようと思います。

# Dockerを使ってビルドする

結論からいうと、MacではDockerでビルドできました。Macネイティブではできなかった理由は後で解説します。

## 全体ソースコード

今回のソースコードはGitHubで公開しています。
https://github.com/tfandkusu/opencv_android_reduced_docker

## ビルド環境の構築

ベースとなるDockerイメージは[CircleCI](https://circleci.com/)からお借りしました。Android SDKとAndroid NDKが初めから入っています。

```Dockerfile:Dockerfile
FROM circleci/android:api-30-ndk
```

cmake、ninja、ccacheをインストールします。

```Dockerfile:Dockerfile
RUN sudo apt-get update
RUN sudo apt-get install cmake ninja-build ccache
```

カレントディレクトリをホームディレクトリにして、[OpenCVのソースコード](https://github.com/opencv/opencv/releases/tag/4.5.5)をダウンロードして解凍します。

```Dockerfile:Dockerfile
WORKDIR /home/circleci
RUN wget https://github.com/opencv/opencv/archive/refs/tags/4.5.5.tar.gz
RUN tar xvzf 4.5.5.tar.gz
```

[ビルドのためのディレクトリ](https://github.com/opencv/opencv/tree/4.x/platforms)に移動します。

```Dockerfile:Dockerfile
ENV BUILD_DIR /home/circleci/opencv-4.5.5/platforms/
WORKDIR ${BUILD_DIR}
```

[build_sdk.py](https://github.com/opencv/opencv/blob/4.x/platforms/android/build_sdk.py)を2カ所書き換えます。

C++言語の標準ライブラリは共有ライブラリ使用からスタティックリンクにします。これをやらないと、別途、5MB前後ある `libc++_shared.so` ファイルをAndroid NDKから持ってきてアプリに組み込み必要があり、スタティックリンクよりも容量が大きくなります。

[該当箇所](https://github.com/opencv/opencv/blob/9e3ba487fa380f5d8732389df0934f0f368d4659/platforms/android/build_sdk.py#L134)
[公式の解説](https://developer.android.com/ndk/guides/cpp-support?hl=ja#cmake)

```build_sdk.py
            self.cmake_vars['ANDROID_STL'] = 'c++_static'
```

[haveIPP関数](https://github.com/opencv/opencv/blob/9e3ba487fa380f5d8732389df0934f0f368d4659/platforms/android/build_sdk.py#L140)の戻り値をTrue固定にします。それによってx86およびx86_64アーキテクチャ向けの　`libopencv_java4.so` ファイルの容量が削減されます。

```build_sdk.py
    def haveIPP(self):
        return False
```

これら2点の書き換えのための、patchファイルを作成しました。

```build_sdk.patch
--- build_sdk.py	2021-12-25 12:53:27.000000000 +0900
+++ build_sdk_fixed.py	2022-01-30 06:52:41.000000000 +0900
@@ -131,14 +131,14 @@
             self.cmake_vars['ANDROID_TOOLCHAIN_NAME'] = toolchain
         else:
             self.cmake_vars['ANDROID_TOOLCHAIN'] = 'clang'
-            self.cmake_vars['ANDROID_STL'] = 'c++_shared'
+            self.cmake_vars['ANDROID_STL'] = 'c++_static'
         if ndk_api_level:
             self.cmake_vars['ANDROID_NATIVE_API_LEVEL'] = ndk_api_level
         self.cmake_vars.update(cmake_vars)
     def __str__(self):
         return "%s (%s)" % (self.name, self.toolchain)
     def haveIPP(self):
-        return self.name == "x86" or self.name == "x86_64"
+        return False
 
 #===================================================================================================
 

```

patchファイルをDockerイメージに含めて、patchを適用します。

```Dockerfile:Dockerfile
COPY --chown=circleci:circleci build_sdk.patch ${BUILD_DIR}
RUN patch -u android/build_sdk.py < ${BUILD_DIR}build_sdk.patch
```

環境変数 `ANDROID_SDK` `ANDROID_NDK` を設定します。

```Dockerfile:Dockerfile
ENV ANDROID_SDK /opt/android/sdk/
ENV ANDROID_NDK /opt/android/android-ndk-r21e/
```

`docker-compose.yml` を作成します。Docker側の生成物をホスト側から拾えるように `volumes` オプションを設定します。

`build_sdk.py` の `--modules_list` 引数で含めるOpenCVのモジュールを定義します。`--no_samples_build` を付けないとビルドできませんでした。

```docker-compose.yml
version: '3'
services:
  main:
    build: .
    volumes:
      - ./output:/home/circleci/opencv-4.5.5/platforms/output
    command: android/build_sdk.py output --no_samples_build --modules_list core,imgcodecs,imgproc,java
```


## ビルドの実行

これで環境が整ったので、こちらの2コマンドだけでビルドできます。

```sh
docker compose build
docker compose run main
```

## 生成物

これで `libopencv_java4.so` とjavaファイル群が生成されました。

```sh
find  output/OpenCV-android-sdk/sdk/native/libs -type f
```

```
output/OpenCV-android-sdk/sdk/native/libs/armeabi-v7a/libopencv_java4.so
output/OpenCV-android-sdk/sdk/native/libs/x86/libopencv_java4.so
output/OpenCV-android-sdk/sdk/native/libs/arm64-v8a/libopencv_java4.so
output/OpenCV-android-sdk/sdk/native/libs/x86_64/libopencv_java4.so
```

```sh
find output/OpenCV-android-sdk/sdk/java/src -type f
```

```
output/OpenCV-android-sdk/sdk/java/src/org/opencv/core/Scalar.java
output/OpenCV-android-sdk/sdk/java/src/org/opencv/core/MatOfFloat6.java
output/OpenCV-android-sdk/sdk/java/src/org/opencv/core/Size.java
output/OpenCV-android-sdk/sdk/java/src/org/opencv/core/MatOfRotatedRect.java
以下略
```

# 削減された容量

`libopencv_java4.so` はこのように削減されました。公式配布の `libopencv_java4.so` は別途 `libc++_shared.so` が必要ですが、今回のビルドでは不要です。

| アーキテクチャ | [公式配布](https://github.com/opencv/opencv/releases/tag/4.5.5) | libc++_shared.so | 今回のビルド(libc++_shared.soは不要) |
| --- | --- | --- | --- |
| arm64-v8 | 17M | 5.9M | 8.9M |
| armeabi-v7a | 11M | 4M | 5.7M |
| x86_64 | 51M  | 6.1M | 13M |
| x86 | 36M | 5.3M | 11M |

# Macネイティブでビルドできなかった理由

Android NDKはAndroid Studioでインストールできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/b029d8c8-df90-18b3-d644-3bbadcc69f42.png)

その上で `ANDROID_SDK` `ANDROID_NDK` 環境変数を設定した上で、`build_sdk.py` を実行すると、 `NullPointerException` が出てしまいました。

```
* What went wrong:
A problem occurred configuring project ':opencv'.
> java.lang.NullPointerException (no error message)
```

[OpenCVのIssue](https://github.com/opencv/opencv/pull/19501)を見ると、Android NDKのバージョンが新しくなると発生する問題だと分かりました。

そこで古いバージョンのAndroid NDKを[こちら](https://developer.android.com/ndk/downloads/older_releases?hl=ja)からインストールしました。
すると、このようなダイアログが出てしまい、ビルドが中断されてしまいました。Macのセキュリティを担保した上で、ダイアログを防ぐ方法は分かりませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/00393680-e7df-9d21-564d-15dc919f44a9.png)

# 補足

## 共有ライブラリが依存する共有ライブラリを調べる方法

`readelf` コマンドで調べることができます。

```sh
readelf -d libopencv_java4.so
```

```
Dynamic section at offset 0x8daa80 contains 31 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
 0x0000000000000001 (NEEDED)             Shared library: [liblog.so]
 0x0000000000000001 (NEEDED)             Shared library: [libjnigraphics.so]
 0x0000000000000001 (NEEDED)             Shared library: [libz.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x000000000000000e (SONAME)             Library soname: [libopencv_java4.so]
以下略
```

Macでは `brew install binutils` でインストールできます。