ヤフー株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。LINEヤフー Tech Blog

テクノロジー

AndroidにおけるSDK開発時に意識していること

こんにちは。ヤフーで広告に関わるソフトウェアの開発をしている加藤 真也(@shikato)です。

今回はAndroid向けSDK開発について、作り方や普段意識していることを紹介したいと思います。
このテーマにした理由は、AndroidやiOSにおける広告などのSDKを使う際の知見は比較的充実しているように思うのですが、それらのSDKを開発する際の知見はあまりないように感じているためです。

本記事は、私が最近担当しているAndroidの広告関連SDK開発時の内容となりますが、いわゆるアドテクのような広告業界ならではの話題は少なめとなっています。
開発者に向けた、ライブラリのようなソフトウェアの開発全般に適用できる話題を盛り込みましたので、広告にあまり興味がない方にも読んでもらえるとうれしいです。

ヤフーの広告に関わるSDKについて

そもそもSDKとは

Yahoo!辞書SDKという単語を調べてみると、以下の説明が見つかります。

《software development kit》ある特定のハードウエアやオペレーティングシステム上で動作するソフトウエアを開発する際に必要な各種のツールをひとまとめにしたもの。  
通常、サンプルプログラム、APIのライブラリー、技術文書などが含まれており、企業が自社製品のソフトウエア開発を促すために無償で配布することが多い。ソフトウエア開発キット。

「SDK」の検索結果 - Yahoo!辞書

それをヤフーの広告関連SDKに当てはめると「ヤフーの広告でマネタイズするための仕組みをアプリに組み込むためのSDK」と言ったところでしょうか。
基本的には社内向けで、主だった導入先としてYahoo! JAPANアプリYahoo!天気アプリYahoo!ニュースアプリなどがあります。

ちなみにですが、Androidアプリ開発をする上で一番馴染みのあるSDKといえば、やはりAndroid SDKでしょうか。
GoogleからAndroid SDKが提供されているおかげで、サードパーティデベロッパーもAndroidアプリ開発が可能となっています。

Android向けSDKの作り方

私が担当している広告関連のSDKは「SDK」という大層な名前がついていますが、その作り方自体はAndroid向けライブラリの開発とさほど違いはありません。
Android向けライブラリの開発に関しては、Cookpadさんがクックパッド開発者ブログにてとても参考になる記事を書かれていますので、そちらを参照していただければと思います。

Android のライブラリづくりとライセンスについて

Android向けSDK開発時に意識していることを分類する

では、ここからは私がAndroid向けSDK開発時に意識していることやTipsを書いていきます。

私が普段意識している事を大きく分類すると以下の3つの項目になりました。

  • 如何にしてアプリ開発者がSDKを簡単に組み込めるようにするか
  • 如何にしてアプリに影響や制限を与えない設計とするか
  • アプリ開発者の声を聞く

では1つずつ紹介していきます。

1 如何にしてアプリ開発者がSDKを簡単に組み込めるようにするか

まず1つ目の分類項目についてです。
私は、Android向けSDKを評価する指標として「SDK提供者が提供したい機能を、アプリ開発者が簡単に組み込めるSDKとなっているか」があると考えています。
ここからは、その指標を満たすために意識していることやTipsを紹介していきます。

1.1 ドキュメントの充実

言わずもがなですが、ドキュメントは充実させておきたいです。
アプリ開発者に対して必要なのは、導入ドキュメントAPIリファレンス、あとはFAQあたりでしょうか。

頻繁に問い合わせを受ける内容はFAQ化しておきます。
問い合わせという行為は、アプリ開発者とSDK開発者ともにそれなりに負担がかかるので、不要な問い合わせが発生しない状況を作っておきます。

1.2 導入サンプルの充実

やはり導入サンプルも充実させておきたいところです。
GitHubでSDKのコードを管理しているのであれば、GitHubで公開されているプロジェクトでよく見られる構成と同様に、リポジトリ直下にsampleのようなディレクトリを作ります。
そこにサンプルアプリを設置しておけば、アプリ開発者が他のGitHubで公開されているプロジェクトと同じ要領で迷わず利用できます。

1.3 簡潔明瞭なAPI

アプリ開発者が直接触れるクラスやメソッドは、その用途が明瞭になるように理解しやすい名前をつけます。
ここは多少時間をかけてでも誰もが納得する名前を考えるべきです。コードレビューなどを活用して議論します。
加えて、アプリ開発者が直接触れる必要のないクラスやメソッドは、ProGuardで難読化して隠蔽します。

ProGuardの設定を配布する

そのProGuardの設定ですが、AndroidのライブラリフォーマットであるAARに同梱できます。
SDK内でリフレクションを使っているなどの理由で、特定のクラスやメソッドにアプリ側でProGuardを使って難読化してほしくない場合は、そのProGuardの設定をAARに同梱しましょう。
そうしておけばアプリ開発者にSDK用のProGuard設定を追記してもらう手間が省けます。

android {
  defaultConfig {
      consumerProguardFiles 'proguard-rules.pro'
  }
}

参考
http://gfx.hatenablog.com/entry/2016/01/16/184846
http://tools.android.com/tech-docs/new-build-system

1.4 build.gradleに依存関係を記述するだけでSDKを取り込めるようにする

GradleがAndroidアプリのビルドツールに採用される前は直接jarファイルを配っていましたが、今はbuild.gradleに依存関係を一行記述するだけでアプリにSDKを取り込んでもらうことが可能です。
その方法についても先ほど紹介させていただいたCookpadさんの記事で詳しく説明されています。

1.5 ログについて

1.5.1 問い合わせやすいログ出力

SDKが出力するログは、導入アプリで何か問題が発生した時に貴重な情報となるので、ログの内容からSDK開発者である自分たちが問題を識別できるログ出力を心がけます。
頻出してしまう類の問題に関しては、SDKが出力するログから、アプリ開発者が必要な対応内容を確認できるようにFAQ化しておきます。

1.5.2 SDK開発者用のログとアプリ開発者用のログを分ける

SDK開発者用のログと、アプリ開発者用のログは明確に分けます。
そして、Build Variantsを使うなどして、アプリへの組込時にはSDK開発者用のログは出力されないようにします。
こうすることで、アプリ開発者を不要な情報で惑わさないようにします。

1.6 SDKの更新

1.6.1 Release NotesやChangeLog

どういう変更をしたのかをアプリ開発者が認識できるようにRelease NotesChangeLogとして記録します。
私が担当しているSDKは社内で運用されているGitHub Enterpriseから社員であれば誰でもコードを閲覧できるようになっているため、GitHub Releasesに、そのバージョンに取り込まれるPull Requestのリンクや詳細な変更点を記述するようにしています。

1.6.2 セマンティックバージョニング

最近、OSSとして公開されているプロジェクトなどで、よく採用されているのを目にしますが、私が担当しているSDKでもセマンティックバージョニングを採用しています。
セマンティックバージョニングを採用することで、SDKを更新した際に、アプリ開発者はどういう変更があったのかをバージョン名からある程度推測できます。
SDK開発者としては、後方互換性のない変更を伴うメジャーバージョンアップを可能な限り避けられるように、拡張性のある設計を心がけたいところです。

1.7 Manifest Merger

Android Gradle PluginにはManifest Mergerという機能が備わっています。
こちらはその名前の通り、AARに含まれるManifestファイルをアプリのManifestファイルにマージしてくれる機能です。
つまり、SDKで必要なパーミッションやActivityをSDK側のManifestファイルで宣言しておけば、アプリ側でSDKのための宣言をせずに済むということです。

ですがパーミッションの追加は、ダウンロード数に影響したり追加するパーミッションによってはセキュリティを考慮する必要が出てきたりと、かなりセンシティブな行為と言えます。
ActivityやServiceの追加も同様で、アプリ開発者が気づかない形でServiceによるバックグラウンド処理が追加されてしまった、といったことも発生するかもしれません。
こういった問題を引き起こさないためにも、Manifest Mergerを使う場合は、導入ドキュメントなどでマージされる項目をアプリ開発者に説明した上で使用するようにします。

Manifest Mergerによってマージされる項目は以下ファイルから確認できます。

# マージ後のAndroidManifest.xml
app/build/intermediates/manifests/full/debug/AndroidManifest.xml
# Manifet Mergerのログ
app/build/outputs/logs/manifest-merger-debug-report.txt 

# appやdebugは例です

1.8 SDKを使う側の視点を持つ

当たり前かもしれませんが、SDKを使う側の視点に立てないと利用しづらいSDKになってしまいがちです。
SDKを使う側の視点を持つには、やはりアプリを開発した経験が必要です。
SDK開発者は自身でもアプリを開発する経験を積んで、SDKのユーザーであるアプリ開発者の視点に立てるようにしましょう。

SDK開発者はどこまで視点を持つべきか

最近のアプリ開発では普通にネイティブアプリとして開発する以外にも、以下のような環境を利用して開発をすることも多いかと思います。
ゲーム開発ならUnityCocos2d-xUnreal Engine、クロスプラットフォーム開発ならXamarinなどです。

SDK開発者としての理想は、それらの環境を利用しているアプリ開発者の視点に立てる事かと思いますが、対応環境が増えれば増えるほどその道のりは険しくなっていきそうです。
その際には、経験者からサポートしてもらったり自身でも利用するなどして、少しでもアプリ開発者の目線に立てるように可能な限り善処したいところです。

2 如何にしてアプリに影響や制限を与えない設計とするか

ここからは、2つ目の分類項目である如何にしてアプリに影響や制限を与えない設計とするかについてです。
SDK開発者は以下のような状況が可能な限り発生しないようにSDKを設計すべきだと私は考えています。

  • SDK起因でアプリのパフォーマンスが悪化する
  • SDK起因でアプリに何かしらの制限が生まれる
  • SDK起因でアプリがクラッシュする(当然ですね)

では、その考えを実践するために意識していることを紹介していきます。

2.1 アプリに影響がありそうな実装を避ける

SDK開発において、特に何も意識していないと意図せずアプリに影響を与えてしまう実装をしてしまうことが多々あります。
今回はその一例として、SDK開発でやってしまいがちなAsyncTaskの実装について、Androidのコード※を交えながら紹介します。

android/platform_frameworks_baseにてandroid-6.0.1_r68としてタグ打ちされているコードで確認しています。

2.1.1 AsyncTaskの直列実行

AsyncTaskで処理を開始する際にはAsyncTask#executeを使用するかと思いますが、SDKで使う場合には問題が生じる場合があります。
では実際にどういう問題が生じるのかをAsyncTask#executeの実装を見ながら確認していきます。

以下がAsyncTask#executeの定義です。
AsyncTask#executeOnExecutorsDefaultExecutorを渡しています。

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}

https://github.com/android/platform_frameworks_base/blob/android-6.0.1_r68/core/java/android/os/AsyncTask.java#L549-L552

そのsDefaultExecutorはstatic変数でありSERIAL_EXECUTORが格納されています。

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

https://github.com/android/platform_frameworks_base/blob/android-6.0.1_r68/core/java/android/os/AsyncTask.java#L215

SERIAL_EXECUTORにはnew SerialExecutor()が格納されています。

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

https://github.com/android/platform_frameworks_base/blob/android-6.0.1_r68/core/java/android/os/AsyncTask.java#L210

SerialExecutorは小さいstaticクラスです。
SerialExecutor#executeは引数のr(アプリ開発者がAsyncTaskで定義した処理=タスク)を実行するためのRunnableをキュー(mTasks)の末尾に挿入しています。
さらにSerialExecutor#scheduleNextを呼び出し、キューから先頭のRunnableを取り出しTHREAD_POOL_EXECUTORで実行しています。
一度SerialExecutor#scheduleNextが呼ばれれば、後はキューに挿入しているRuunable#run内でSerialExecutor#scheduleNextが再帰的に呼ばれるため、キューが空になるまで順番に処理されます。

private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

https://github.com/android/platform_frameworks_base/blob/android-6.0.1_r68/core/java/android/os/AsyncTask.java#L226-L250

AsyncTask#executeは、このような実装となっているため、アプリとSDKがともにAsyncTask#executeを使った場合に問題が発生します。
AsyncTaskを使ってSDK側で何か時間のかかる処理をすると、タイミングによってはアプリ側の処理はその分だけ待たされる可能性があり、双方が非常に気づきづらい形でアプリに悪い影響を与えてしまいます。

では並列実行するにはどうすれば良いでしょうか。

2.1.2 AsyncTaskの並列実行

AsyncTask#executeのドキュメントを見てみると、以下の説明が見つかります。

If you truly want parallel execution, you can use the executeOnExecutor(Executor, Params...) version of this method with THREAD_POOL_EXECUTOR;

https://developer.android.com/reference/android/os/AsyncTask.html#execute(Params…)
)

AsyncTask#executeOnExecutorAsyncTask.THREAD_POOL_EXECUTORとともに使えば良さそうです。

ではAsyncTask#executeOnExecutorの実装を見てみます。
説明されていた通りにAsyncTask#executeOnExecutorの引数にAsyncTask.THREAD_POOL_EXECUTORを渡すとAsyncTask.THREAD_POOL_EXECUTORでタスクが実行されそうです。

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params) {
    if (mStatus != Status.PENDING) {
        switch (mStatus) {
            case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task has already been executed "
                        + "(a task can be executed only once)");
        }
    }

    mStatus = Status.RUNNING;

    onPreExecute();

    mWorker.mParams = params;
    exec.execute(mFuture);

    return this;
}

https://github.com/android/platform_frameworks_base/blob/android-6.0.1_r68/core/java/android/os/AsyncTask.java#L588-L610

AsyncTask.THREAD_POOL_EXECUTORも見てみると、その実体はThreadPoolExecutorとなっています。

public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

https://github.com/android/platform_frameworks_base/blob/android-6.0.1_r68/core/java/android/os/AsyncTask.java#L202-L204

それらの引数は以下のように定義されているため、AsyncTask.THREAD_POOL_EXECUTORはCPUのコア数に対応したスレッド数を持つスレッドプールと言えそうです。
new LinkedBlockingQueue<Runnable>(128)となっているため、スレッド数を超過したとしてもタスクはキューに最大で128まで保持されます)

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; 
private static final int KEEP_ALIVE = 1;

https://github.com/android/platform_frameworks_base/blob/android-6.0.1_r68/core/java/android/os/AsyncTask.java#L183-L186

private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);

https://github.com/android/platform_frameworks_base/blob/android-6.0.1_r68/core/java/android/os/AsyncTask.java#L196-L197

以上のことからAsyncTask#executeOnExecutorの引数にAsyncTask.THREAD_POOL_EXECUTORを渡すと、CPUのコア数に対応したスレッドを持つスレッドプールでタスクが処理されるため並列実行である、ということが分かります。
実はAsyncTask#executeもタスク自体はAsyncTask.THREAD_POOL_EXECUTORで実行されているのですが、タスクを直接AsyncTask.THREAD_POOL_EXECUTORに渡さず、前述したように一旦SerialExecutorを使ってキューイングしているため直列に実行されています。

ちなみにですが、今回紹介したAsyncTask#executeが直列実行になったのはHONEYCOMB(Android 3.0)からで、それ以前はAsyncTask#executeAsyncTask#executeOnExecutor相当の挙動をしていました。(AsyncTask#executeOnExecutorは存在していませんでした)
https://github.com/android/platform_frameworks_base/blob/android-cts-2.2_r8/core/java/android/os/AsyncTask.java#L376-L397

2.1.3 AsyncTask.THREAD_POOL_EXECUTORで並列実行してもアプリに影響が出る場合

AsyncTask.THREAD_POOL_EXECUTORを使って並列実行してもアプリに影響を与えてしまう場合があります。
それは、static変数として定義されているAsyncTask.THREAD_POOL_EXECUTORのスレッドやキューをSDKが専有してしまう場合です。
そのリスクが懸念される場合は、AsyncTask#executeOnExecutorの引数にAsyncTask.THREAD_POOL_EXECUTORを渡さずに、SDK側で定義したExecutorを渡すようにしましょう。
そもそもSDK開発ではAsyncTaskを使わない、という選択も有効だと思います。

2.1.4 AsyncTask問題まとめ

今回紹介したAsyncTaskの例のように、SDK開発者が意図せずアプリに影響を与えてしまう実装をしてしまうことが多々あります。
SDKを開発する際には、アプリに影響がないかを常に意識しながら実装することを心がけておきたい所です。

2.2 OSSなどのライブラリに可能な限り依存しない

SDK開発ではOSSなどのライブラリに可能な限り依存するべきではないと私は考えています。
その理由としてライブラリのバージョン競合問題があります。

ではなぜライブラリのバージョン競合問題が発生するのかを説明していきます。

2.2.1 Gradleの推移的な依存関係の管理

Androidアプリのビルドツールとして採用されているGradleは推移的に依存関係を管理しています。

Gradleのドキュメントに推移的な依存関係の管理についての説明があります。

Transitive dependency management is a technique that enables your project to depend on libraries which, in turn, depend on other libraries.

https://docs.gradle.org/current/userguide/dependency_management.html#sec:dependency_management_overview

仮にあるアプリAが存在しているとします。
そのアプリAが、あるSDK-Aに依存していて、さらにそのSDK-AがライブラリAやライブラリBに依存することを可能とするテクニックが推移的な依存関係の管理です。

依存関係は以下のタスクを実行すると確認できます。

# appは例です
./gradlew :app:dependencies

では上述したようにアプリAが依存するSDK-AがライブラリA(バージョン2.0.0)に依存していて、アプリA自身もライブラリA(バージョン1.0.0)に依存している場合、どのようにGradleは依存関係を解決するのでしょうか。

以下のようなケースです。

アプリのbuild.gradle

dependencies {
    // sdk-aとlibrary-aは架空のSDKとライブラリです
    compile 'jp.co.yahoo.android.sdk:sdk-a:1.0.0'
    compile 'jp.co.yahoo.android.library:library-a:1.0.0'
}

SDK-Aのbuild.gradle

dependencies {
    // library-aは架空のライブラリです
    compile 'jp.co.yahoo.android.library:library-a:2.0.0'
}

Gradleのドキュメントにバージョン競合を解決するための戦略についての説明があります。

Newest: The newest version of the dependency is used. This is Gradle's default strategy, and is often an appropriate choice as long as versions are backwards-compatible.

先程のケースで言うと、より新しいバージョンであるライブラリA(2.0.0)が使用されるということです。

ではライブラリA(2.0.0)には1.0.0と後方互換性のない変更が加えられており、アプリA側で、その変更が影響する1.0.0の機能を使っていたとします。
この場合アプリAはライブラリA(1.0.0)を使う必要があるため以下のような手段を取ることになります。

  • 推移的に依存関係を解決しない
  • バージョンを強制する
  • 推移的な依存関係を除外する

では1つずつ見ていきます。

2.2.2 推移的に依存関係を解決しない

transitivefalseとすることで推移的に依存関係を解決する機能をOFFにできます。

dependencies {
  // sdk-aは架空のSDKです
  compile ('jp.co.yahoo.android.sdk:sdk-a:1.0.0') {
      transitive = false
  }
}

2.2.3 バージョンを強制する

Gradleは使用するライブラリのバージョンを強制できます。

configurations.all {
    // library-aは架空のライブラリです 
    resolutionStrategy.force 'jp.co.yahoo.android.library:library-a:1.0.0'
}

2.2.4 推移的な依存関係を除外する

excludeを使用することで特定の推移的な依存関係を除外できます。

dependencies {
  // sdk-aとlibrary-aと架空のライブラリです
  compile ('jp.co.yahoo.android.sdk:sdk-a:1.0.0') {
      exclude group: 'jp.co.yahoo.android.library', module: 'library-a'
  }
}

2.2.5 バージョンの指定では解決できない場合

ここまで特定のバージョンを指定するための方法を見てきましたが、それだけで解決できない時はどうすべきでしょうか。
例えばアプリAとSDK-Aが、それぞれライブラリA(1.0.0)とライブラリA(2.0.0)にしかない機能を使っている場合です。
この場合はバージョンを指定するだけでは解決できないので、アプリAとSDK-Aのどちらかが歩み寄る必要が出てきます。
SDK-Aを導入しているアプリが少数の場合は、それで何とかなるかもしれません。
ですが、大多数のアプリに組み込まれるSDKの場合だとどうなるでしょうか。
アプリ側、SDK側、ともにつらい状況が頻繁に発生する未来が容易に想像できるのではないでしょうか。

2.2.6 バージョン競合問題まとめ

このようなバージョン競合問題を避けるためにも、SDK開発では依存ライブラリを可能な限りなくすべきだと私は考えています。

私が担当しているSDKで言うと、広告で唯一使用可能な永続IDである広告ID(Googleがポリシーで定めている)を取得するために必要なGoogle Mobile Adsには依存しますが、主だった使用用途が実装コスト削減などのOSS(RxJavaRetrofitGsonなど)には依存しません。

もちろん一開発者として、これらのライブラリを使いたい衝動には駆られるのですが、SDK開発者としてはデメリットの方が大きいと判断しているので我慢しています。
その分テスティングフレームワークをいろいろ試したりデバッグ時にしか使わないソフトウェアを使うことでライブラリを使いたい衝動を満たすようにしています。

ちなみに拙作であるgradle-android-coverage-checkというGradle Pluginも、そういった衝動から生まれたものです。
gradle-android-coverage-checkに関してはQiitaに投稿した記事でも紹介しているので興味がある方は読んでみてください。

2.3 対応OSバージョン

対応OSバージョンの選択によってアプリ側に大きな制限が生まれてしまうので、SDK開発者としては可能な限り低いバージョンから対応したい所です。
ですが、対応するバージョンによっては対応コストが跳ね上がってしまうこともあるので、Googleが公開しているOSバージョン別シェア数などを見て判断します。
SDK開発の場合だと、それらに加えて市場に出回っているアプリの対応バージョンにも目を通しておきたい所です。

2.4 規模が大きくなってきたら分割する

モジュールの分割は有名な所だとGoogle Play Servicesが取っている手法です。
主なメリットしては以下が挙げられそうです。

  • Android 64K(65536)問題
  • 容量軽減
  • 提供する機能を明瞭にする

Android 64K(65536)問題を回避する手段はいくつかあると思いますが、そもそもアプリが対応しなくてもすむようにSDK側でも可能な限り善処すべきです。
メソッド数の他にも、SDKの容量や提供する機能が膨れ上がってきたならば、Google Play Servicesと同様に必要な機能をアプリ開発者が取捨選択できるようにすることを考慮しましょう。
(Google Play Services規模のSDKを開発する事はまれだと思いますが)
モジュールを分割する際の粒度ですが、広告関連のSDKの場合だと広告商品ごとに取捨選択して取り込めるようにするのも面白いかもしれないと考えています。

2.5 非公開なAPIの使用を避ける

リフレクションを使うとAndroid SDKの非公開なAPIを叩けますが、可能な限り避けるべきです。(SDK開発の場合は特に)
理由としては、そのAPIが非公開なだけに新しいOSバージョンで急に使えなくなるなどのリスクがあるためです。
その結果、SDK起因でアプリが新しいOSに対応できない、という制限を生んでしまいます。

iOSの場合はさらに深刻で、非公開APIを使用しているとリジェクトされてしまいます。

2.5.1 Apps may only use public APIs. Learn more about public APIs.

https://developer.apple.com/app-store/review/guidelines/#software-requirements

2.6 変化に追随する

SDK開発に限った話でもないですが、Androidの新しいバージョンが出た際には動作テストは必ずしましょう。
これを怠ると、ひとつ前の項目でも述べたSDK起因でアプリが新しいOSに対応できない、という制限がアプリに発生してしまいます。

2.7 想像力を働かせる

どのような構成のアプリに組み込まれるかわからないので、想像力を働かせて、いろいろな構成のアプリに組み込み可能なSDKを目指します。
ただ「SDKを使う側の視点を持つ」の章でも述べたように、こればかりは経験がモノを言う領域だとも感じるので、対応環境が増えれば増える程つらくなっていきそうです。

2.8 品質について

これもSDKに限った話でもないですが、ソフトウェアとしての品質にはこだわります。

SDKという類のソフトウェアは大多数のアプリに組み込まれるケースが多いかと思います。
ヤフーの場合だと、大きい所で、Yahoo! JAPANアプリや、Yahoo!天気アプリYahoo!ニュースアプリがあり、それらに私が関わっているSDKが組み込まれています。
つまりSDKの軽微な問題が、大多数のユーザーが影響を受ける大きな問題に発展してしまうこともあり得るということです。
特に広告関連のSDKは金銭が直接絡む以上、より大きな問題に発展する可能性があります。
SDKの方で修正できたとしても導入先アプリへの反映には時間がかかったり、最悪反映されないケースもあり得ます。

そのため、SDK開発では普通のアプリを開発する以上に品質に対して敏感になる必要があると感じています。

一概に品質と言っても信頼性や安全性などさまざまな観点があり、それらを向上させるための手法もさまざまだと思います。
よって具体例はこの場では挙げきれないですが、少なくとも発生しうるリスクと高い品質を維持する必要性は認識しておきたい所です。

3 アプリ開発者の声を聞く

ここからは、私が意識していることの3つ目の分類項目であるアプリ開発者の声を聞くについてです。

SDKのユーザーであるアプリ開発者の声に耳を傾けると、自分たちでは想像つかない問題も発掘されることがあります。

3.1 アプリ開発者がフィードバックしやすい場を作る

アプリ開発者がフィードバックしやすい場を提供します。
私が最近担当しているSDKは主に社内向けなので、MYMと呼ばれる社内チャットでアプリ開発者からのフィードバックを受け付けています。
こちらで紹介されているように、MYMは誰でも気軽にやりとりできるコミュニケーションツールなので、アプリ開発者がフィードバックをするための負担は小さいと思っています。

3.2 感謝の気持ちを持つ

時に厳しい内容のフィードバックをいただくこともありますが、フィードバックはプロダクトの品質を向上させるための貴重な情報なので、めげずに役立てていきましょう。

3.3 社内OSS化

私が担当しているSDKは、社内で運用されているGitHub Enterpriseで管理しています。
言うなれば社内OSSなので、まれに他サービスのエンジニアの方から、SDKが起因となりアプリで発生している問題を解決するためのPull Requestが送られてくることがあります。
非常にありがたい行為なので、Pull Requestを送る側が開発時に戸惑わないように、開発時のルールなどはCONTRIBUTING.mdのような形で掲示しておきます。
GitHubのIssue and Pull Request templatesを使うのもアプリ開発者のPull Requestによる負担を減らす上で有効です。

最後に

ここまでAndroid向けSDK開発時に私が意識していることを書いてきましたが、皆様のお役に立てる内容になっていたでしょうか。
普段からライブラリのようなソフトウェアを開発されている方からすると当たり前な事柄が多かったかもしれませんが、皆様に何か1つでも新しい発見を与えることができましたら幸いです。
冒頭でも述べたようにモバイル向けSDK開発に関する情報は、あまり世に出回っていないように感じているので、今回の記事がきっかけで増えていってもらえるとうれしいです。

求人情報

最近、私が所属するチームではSDK開発者の数が足りていない、という問題を抱えています。
ということで、iOS/Androidにおける広告に関わるSDK開発者を絶賛募集中なので、興味の有る方は以下の求人情報をご覧になっていただければと思います!

ヤフーのアプリビジネスの基盤となるSDK開発エンジニアを募集!

蛇足かもしれませんが、私は以下のモチベーションがあったため自らの意志で今のチームに異動しました。

  • 普通にアプリを開発するのとは一味違うデベロッパー向けソフトウェアの開発をしてみたい
  • ヤフーで仕事をするからには、その収益の柱である広告というプロダクトに一度は関わってみたい

それでは、ここまで読んでいただきまして、ありがとうございました!

こちらの記事のご感想を聞かせください。

  • 学びがある
  • わかりやすい
  • 新しい視点

ご感想ありがとうございました

このページの先頭へ