woshidan's blog

あいとゆうきとITと、とっておきの話。

Activityの起動とlaunchModeについて

ActivityのlaunchMode の違いが気になっていたので、公式で推奨されている standard, singleTop についてざっくり整理しました。

launchMode=“standard"、あるいは指定がない場合

Androidのシステムは常に、ターゲットタスク*1内で新しいActivityのインスタンスを作成します。

つまり、

<activity android:name=".MainActivity"
    android:launchMode="standard"
    >
    <intent-filter> <!-- IntentFilterの部分は本記事の趣旨には関係ない -->
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

となっている場合、

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, MainActivity.class));
            }
        });
    }

と書いたらボタンを押せば何枚でもMainActivityが生成されてスタックに積まれていくわけです。

こんな感じに。

f:id:woshidan:20170901010221g:plain

launchMode=“singleTop"の場合

Androidのシステムは、ターゲットタスク内で

  • 起動しようとしているActiivtyがタスクのトップにある場合は新しいActivityを生成しない
  • 起動しようとしているActiivtyがタスクのトップにない場合は新しいActivityを生成する

というふるまいをします。ややこしいですね。

ちょっと動かしてみましょう。

<activity
    android:name=".MainActivity"
    android:label="FirstActivity"
    android:launchMode="singleTop">
    <intent-filter> <!-- IntentFilterの部分は本記事の趣旨には関係ない -->
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity android:name=".NextActivity"
    android:launchMode="singleTop"
    android:label="SecondActivity"
    >
</activity>
// NextActivityもほぼ同じコード
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, MainActivity.class));
            }
        });

        findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, NextActivity.class));
            }
        });
    }
}

交互に二つのActivityを起動する場合は、新しいActivityが生成されスタックに積まれていきます。

f:id:woshidan:20170901010056g:plain

同じActivityを連続して起動しようとすると、singleTopの 起動しようとしているActiivtyがタスクのトップにある場合は新しいActivityを生成しない という性質により、新しいActivityのインスタンスは生成されず、スタックにあるActivityは増えません。

f:id:woshidan:20170901010153g:plain

ところで、launchModestandard のアクティビティを利用している場合、前のActivityから渡されたIntentをonCreateで受け取って表示やAPIへのリクエストパラメータに利用します。

それでは、singleTopのActiivtyがスタックの一番上にあるときに、もう一度Activityを起動しようとしたことを検知、あるいは、その際渡されたIntentを元に表示などを変更したい場合、どうしたらいいでしょうか。

公式ドキュメントにも書いてありますが、Activity.onNewIntent(Intent)で可能です。

// 先ほどのMainActivityのコードを少し編集
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                intent.putExtra("KEY_MESSAGE", "sent new intent");
                startActivity(intent);
            }
        });

        // 略
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        
        Toast.makeText(this, intent.getStringExtra("KEY_MESSAGE"), Toast.LENGTH_SHORT).show();
    }

f:id:woshidan:20170901010031g:plain

ECやメディア系のアプリで起動時にダイアローグを表示して、押されたボタンによっては、トップ画面の表示をデフォルトではなく特定の単語やカテゴリで検索かけた結果を表示するようにしたい、なんていう場合などは覚えておくと便利そうです。

現場からは以上です。

*1:おおむね起動しようとしているActivityの属するアプリのタスク

NoSQLデータベースファーストガイドを読んだ

NoSQLデータベース ファーストガイド

NoSQLデータベース ファーストガイド

春先にRDBMSの勉強を少ししたのでNoSQLの話も少し覗きました。2011年発行とやや古いのですが、その分一歩ずつ各種ソフトウェアの背景を把握しながら前に進んでいけるような内容でよかったです。

  • 紹介されていたのはmemcached, Tokyo Tryant, Redis, MongoDB, MySQL + HandlerSocket
    • 当時のものではありますが、サーバ1台目から、レプリケーションやるところまで手順とログあったのどんな感じなのか感覚がわかりやすくて良かった
    • 3章の事例, 4章のパフォーマンス計測のコードがあるところもありがたい*1
  • memcachedなどのkey-valueストアはテーブルに相当するものがないということを認識して雷を打たれた感じになりました
    • なんていうか、マジックナンバー的なコラムが2つ*2用意されたテーブルみたいです
    • パフォーマンスや使い方は全然違いますが、データが持たせられる構造的に
  • 各NoSQLデータベースについて
    • 各ソフトウェアの見出しは適当につけましたが、使うときは4章および現在の各種ソフトウェアのドキュメント参照のこと
    • memcachedは揮発するけど速い
      • 揮発していい、計算に数分かかるけど一度参照されたらしばしば参照されるようなデータに使いやすい
      • RDBMSなどと組み合わせて使うことが前提
    • Tokyo Triantは書き込む分、memcacedより遅いがRDBより速い
      • ブログのアクセス数みたいに希薄しては困るけどしばしば更新 & アクセスされる、みたいな値に使うのが良さそう
        • 検索条件の都合で、範囲を指定してのデータ取得に弱いので、MySQLなど範囲指定が柔軟な他のDBでサマリテーブル作って組み合わせると良さそう
    • rediesはこれらのいいとこ取りしたような感じ
      • 配列の取り扱いが強いのと、集合演算があるので、フォローしてる人のつぶやき一覧みたいにしばしば更新&参照される割に絞り込みがややこしそうなところで使えそうな感じがありました
      • この例みて少し強くなった気持ちになれたので自分でも似たような例を考えて一回試し書きしてみたい
      • 探され方の分だけデータを挿入するためのキーを用意する、みたいな印象
    • mongo dbはドキュメントの構造の柔軟さがまず魅力
      • embedded ドキュメントのとこは実際データの書き込み・保存とか、その辺よく読み取れなかったのでもう一回触り直ししたいが、ドキュメント型の他のNoSQLのぞいてそっちを触った方が良さそう?
      • コンソール叩いてデータベースやコレクションが書き込み時に必要になってから生成されているの示している部分がよかった
      • mapperのバリデーションはアプリケーション側で値のバリデーションやマッピングに利用するために書いているのであり、mongo的にはいらない
      • BSON(データ形式)のこともうちょっと調べたい
    • MySQL + HandlerSocket
      • 速いな! という気持ちですが、あれから時間も経っていて、いくらかのNoSQLはけっこう枯れてきたと思われるので、NoSQLの仕事はNoSQLに任せた方がいいかなという感触

とりあえず、RDBMSの本を読んで、集合演算やしばしば更新されるテーブルに対してどうしようかなーという感じがあり、具体的には

  • ブログのアクセス数
  • 頻繁にアクセス&更新される一覧の表示

に関して悩んだことがあったので、知りたいことはここにあったのかーという感じでした。自分不勉強ですねぇ。。

また、レプリケーションの話が各ソフトウェアごとに書いてあって、データが分散しているとはどういうことかー、みたいなのが少しだけ感覚つかめた気がして*3よかった。例を浴びるの is よいです。これは不意打ちによかったです。

こういうの、インフラ寄りの人からすると当たり前かもしれないんですが、rediesを導入する機能を書き始めるWebアプリケーションエンジニアからすると触るの自体大変だったりして具体的なログの感じとか、データが保存されているイメージとかつかめなくてやたら難航することがあるような記憶があるので、お手本のログがあるの、正常な状態はこれだよ、と教えてもらえる感じでとてもよかったです。

現場からは以上です。

*1:いま同様の検証やるときの参考にもなると思いました

*2:1つが検索用のキーでもう一つが表示などに使う意味のある値

*3:気だけとかいう可能性はありまくりなので、いつかちょっと何か作りたいですね

Androidの通知チャンネルの振る舞いをプロジェクトのtargetSDKVersionを変えて少し試してみた

Android OのEaster Eggはoctopusでした*1が、OのコードネームはOreoでしたね。さて、Android Oではユーザーが通知をより便利に管理するために通知チャンネルが導入されました。

通知チャンネルがなんたるかや主要な扱い方については公式他に任せるとして、この記事については、通知チャンネルについて自分が試してみた少しマイナーなことについてまとめようと思います。

TL;DR

  • Android Oの端末でも、アプリのtargetSDKVersionが25以下なら通知チャンネルを作成せずとも通知は届く
  • Android Oの端末でも、アプリのtargetSDKVersionが25以下かつライブラリのtargetSDKVersionが26で、ライブラリ側で通知チャンネルを作成しているならば、その通知チャンネルが作成&利用可能であると同時に「Uncategorized」 という通知チャンネルが作成される
  • Android Oの端末で、アプリのtargetSDKVersionが26なら、通知チャンネルを作成しないと通知が届かないし、その旨のToastが出る
  • 通知チャンネルを作成したあと、同じIDを指定して作成し直すことで「チャンネル名」などのチャンネルの設定が更新できるが、バッジの有無の設定は更新できない

以下、検証用のコードなどを置いておきます。

Android Oの端末でも、アプリのtargetSDKVersionが25以下の場合

// アプリのプロジェクトのbuild.gradle
android {
    compileSdkVersion 25
    buildToolsVersion '25.0.3'

    defaultConfig {
        applicationId 'com.example.woshidan.test'
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1"
    }
...
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final Notification.Builder builder = new Notification.Builder(this)
                .setSmallIcon(android.R.drawable.ic_btn_speak_now)
                .setContentTitle("Title")
                .setContentText("Text");

        final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify("tag", 1234, builder.build());

届いた通知は以下のような形で表示され、通知チャンネルの表示は出ないようです。

Android Oの端末でも、アプリのtargetSDKVersionが25以下かつライブラリのtargetSDKVersionが26で、ライブラリ側で通知チャンネルを作成している場合

のように、ライブラリプロジェクトを作成して、ライブラリプロジェクト側で targetSdkVersion を26にします。

// libraryプロジェクトのbuild.gradle
apply plugin: 'com.android.library'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.1"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
// アプリ側のプロジェクトのbuild.gradle
dependencies {
    ...
    compile project(path: ':mylibrary')
}
package com.example.woshidan.mylibrary;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.support.annotation.RequiresApi;

/**
 * Created by woshidan on 2017/08/22.
 */

public class NotificationChannelCreator {
    @RequiresApi(api = 26)
    public static void createChannel(final Context context) {
        NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル", NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription("テストの説明です");
        channel.setShowBadge(true);

        // create or update the Notification channel
        final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);
    }
}
// アプリ側のプロジェクト
public class MainActivity extends Activity {

    @RequiresApi(api = 26)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        NotificationChannelCreator.createChannel(this);

        // 通知チャンネルを指定しないで通知を送る
        // ここで通知を送っていない場合でもUncategorizedチャンネルはできる
        final Notification.Builder builder = new Notification.Builder(this)
                .setSmallIcon(android.R.drawable.ic_btn_speak_now)
                .setContentTitle("Title")
                .setContentText("Text");

        final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify("tag", 1234, builder.build());

Android Oの端末で、アプリのtargetSDKVersionが26の場合

// アプリのプロジェクトのbuild.gradle
android {
    compileSdkVersion 26
    buildToolsVersion '26.0.1'

    defaultConfig {
        applicationId 'com.example.woshidan.test'
        minSdkVersion 16
        targetSdkVersion 26
        versionCode 1
        versionName "1"
    }
...
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final Notification.Builder builder = new Notification.Builder(this)
                .setSmallIcon(android.R.drawable.ic_btn_speak_now)
                .setContentTitle("Title")
                .setContentText("Text");

        final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify("tag", 1234, builder.build());

通知チャンネルの更新について

        NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル", NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription("テストの説明です");
        channel.setShowBadge(true);

        // create or update the Notification channel
        final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);

一度上記のコードで通知チャンネルを作成すると、下記のようなチャンネルができます。

その後、下記のコードで更新すると

        NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル Updated", NotificationManager.IMPORTANCE_HIGH);
        channel.setDescription("テストの説明を変えました");
        channel.setShowBadge(false);

        // create or update the Notification channel
        final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);

こうなります。

現場からは以上です。

Android SDK Toolsのバージョンを25.2.xから25.3.xにあげたらcocos runコマンドが実行できなくなった

症状

Android SDK Toolsのバージョンをあげて cocos runコマンドを実行した際、以下のメッセージが出てビルドできなくなりました。

$ cocos run -p android --android-studio
Building mode: debug
Using Android Studio project : /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio
running: '/Users/woshidan/Library/Android/sdk/tools/android update project -t android-21 -p /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio/app'

*************************************************************************
The "android" command is deprecated.
For manual SDK, AVD, and project management, please use Android Studio.
For command-line tools, use tools/bin/sdkmanager and tools/bin/avdmanager
*************************************************************************
Invalid or unsupported command "update project -t android-21 -p /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio/app"

Supported commands are:
android list target
android list avd
android list device
android create avd
android move avd
android delete avd
android list sdk
android update sdk
Error running command, return code: 2.

原因

cocos コマンドは Android SDK Tools のバージョン 25.2.x 以下でサポートされている android コマンドに依存しており、そのコマンドの一部が Android SDK Tools25.3.0 でサポートされなくなりました。

サポートされなくなったコマンドは Ant( =Eclipse) 用のコマンドで、GoogleEclipse のサポートをやめると言っているので今後もサポートされることはないでしょう。

対応

本当は、この辺りを読んで、NDKでcocos コマンドでビルドしていた部分を書き換えて行くのが良いと思いますが、さしあたっては 公式リポジトリの関連issue にある、こちらのコメント からDLできる古めのAndroid Toolsを使ってしのぐかなーという感じです。

ソフトウェアテスト技法ドリルを読んだ

ソフトウェアテスト技法ドリル―テスト設計の考え方と実際

ソフトウェアテスト技法ドリル―テスト設計の考え方と実際

Web系のソフトウェアエンジニアだとある程度TDDの文脈でテストコードを書くと思います。

自分は今アプリ用のSDKの担当でSDKについてリリース前のQAも行うのですが、サーバや外部サービスとの連携部分で膨らみがちなテストを漏れが出ないようにしつつ、手早く設計&実行できるようにするか、みたいなところが気になって読みました。

前書きによると、この本は、Boris Beizerの「ソフトウェアテスト技法」に書いてあることの一部を初級者でも読めるように噛み砕きつつ、設計の際に利用できるツールの紹介をしたり、日本人の考えた手法も合わせて取り上げたり、といった感じです。

もともと敷居が高いものをさらっと取り上げた結果薄い、みたいな感じなので、TDD少しやったことあります、程度の自分にとっては、HAYST法あたりから結構噛み砕くのが重たいな、といった感じでした。

「異常値は他の異常値を隠す」、状態遷移表、Nスイッチカバレッジ(N=2~3)、タイミングも水準、あたりが明日からすぐ使えそうな感じでした。

以下、印象に残ったことをいくらかメモします。

  • 怪しい箇所の探し方
    • 間・対象・類推・外側、意地悪な条件を意識
  • 知っているものほど危うい
    • 外部連携などした時に、前提が異なってデータの移行ができないなどのトラブルの元IOS基準など外部基準を調べたり(日付のiCalenderなど)
  • 同値分割・境界値分析は、ふだん「テストや動作確認をする」という言葉を使う時に連想するようなことに近い
  • 有効な値は複数同時に確認できるが、無効な値については一回に一つずつ(=異常値は他の異常値を隠す)
  • 関係性がある複数の変数を同時にテストする「ドメイン分析テスト」
    • on/off/in/outで、ある因子(=関数に入れる一つの変数orテスト時に変化させる項目)をon/offさせるときは、他の因子はin(Binderのドメイン分析マトリクス)
    • 複数の条件の組み合わせをチェックしたいとき、原因結果グラフを使い、それに対応したツールを使うことでディシジョンテーブル(因子の値とその結果を表す表。)が自動生成できる
    • これが成り立たなかったら次はこの条件を考える… みたいなことをしたいときはCFD法で書くとスッキリしやすい
  • 仕様としては機能と機能の間に関連がないことになっているシステム上の要因どうしを組み合わせて「想定外」のバグがないかのテストを効率よくしたい
    • HAYST法やペアワイズを使う
    • 直交表-> 複数の条件のうち、任意の2~3つの因子の組を選んだら、因子の取りうる代表値(水準という)の全ての組合わせがテストができるようなテストを作る
    • HAYST法の詳細は、 https://www.fujixerox.co.jp/company/technical/tr/2015/pdf/p_04.pdf
    • HAYST法に関連して紹介された「エキスパートの知見を必要最小限とする」ためのプロセスは度々見返すと良さそう
    • 因子同士の組み合わせにより発生する、一緒に実行できない水準の組み合わせを禁則と言って、禁則がある場合の処理として、その因子の組み合わせを一つの因子として、禁則の部分を除いて水準を作り直す(相互排他因子融合手法)
    • 直交表の割り当ては因子の組み合わせが決まれば既存の割り当て表から選ぶ感じ?
    • ペアワイズはなんらかの行列的演算を持って、直交表が持っている「全組み合わせがどう数回あらわれるという性質」を崩してテストケース減らしたい時に使う
    • それぞれの機能で必要とわかっている前提の条件がいくつもあるような場合の成否をするのはこの章のテスト設計手法の話ではない
  • 状態遷移のテスト
    • C0 パスカバレッジ 100% 全ての状態を少なくとも一度以上
    • C1 パスカバレッジ 100% 全ての遷移も少なくとも一度以上
      • では足りないことがあるので、状態遷移表から「イベントが適用できない」(=遷移できない)場合、N/A(Not Applicable)を探す
    • ある遷移のあと、内部状態が変化したことももれなくテストしたい -> Nスイッチカバレッジ
      • 状態遷移表を関係行列に… それをN乗-> N回のイベントで到達できるすべてのパスがわかる
    • 状態が多すぎる場合はどうするか?
      • ラルフチャートから内部変数をリストアップしてやる。状態の遷移については、典型的な遷移パターンを因子としてまとめてしまう。タイミングも因子として扱うことが
  • 身体測定とソースコードの静的解析はいままで取ったことがある周りの状態と比較して現在のプロダクト(人間)の状態がやばいか早めに判断する、という点で似てるよね

直交表の例

3つの因子があるとき、そのうち2つの因子を選んで、2つ同士は、すべての水準の値の組み合わせがテストできるようにする表。

No コイン1の表裏 コイン2の表裏 コイン3の表裏
1
2
3
4

水準の値の組み合わせについて確認すると、

  • コイン1の表裏とコイン2の表裏を見ると {コイン1-表, コイン2-表}, {コイン1-表, コイン2-裏}, {コイン1-裏, コイン2-表}, {コイン1-裏, コイン2-裏} の4通りが全て出ている。
  • コイン2の表裏とコイン3の表裏を見ると {コイン2-表, コイン3-表}, {コイン2-表, コイン3-裏}, {コイン2-裏, コイン3-表}, {コイン3-裏, コイン3-裏} の4通りが全て出ている。
  • コイン3の表裏とコイン1の表裏を見ると {コイン3-表, コイン1-表}, {コイン3-表, コイン1-裏}, {コイン3-裏, コイン1-表}, {コイン3-裏, コイン1-裏} の4通りが全て出ている。

API19, API21のAndroid端末で透過GIFを表示すると、透過色の部分が真っ黒になることがある

stackoverflow.com

stackoverflow.com

API19とAPI21のNexus 5のエミュで再現。API18以下、API23以上のエミュでは未確認。

透過色を決めておいて、その色の部分を後から透明にするという対応もありますが、他の画像形式を用いてもよいのではないでしょうか。 一定以下のAPI Level以下ならわかりやすいんですが、確認しててこの二つのバージョンだけだったので焦りました。。

NSURLSessionを使って通信処理を書いてみる

http://woshidan.hatenablog.com/entry/2017/06/18/014734 の記事の続きで、先の記事の中の

2. Foregroundでダウンロード処理を開始する場合は、ダウンロード中にアプリが停止ないし終了してもよいようにそのダウンロード処理の管理をシステム側へ手渡すことができる

の部分についてメモします。

ポイント

  • ファイルをダウンロードするときは、NSURLSessionを使う
    • そうすることでアプリがバックグラウンドでデータ通信している間はiOSのシステム側がアプリを停止してもダウンロード処理はシステム側でしてくれる
  • ダウンロード/アップロードが終わったり、アプリに注目させる必要がある場合はアプリを起動させることができる
  • どのNSURLSessionオブジェクトによるデータ通信処理か、どのNSURLSessionオブジェクトによるapplication(_:handleEventsForBackgroundURLSession:completionHandler:コールバックかを判定するためには、NSURLSessionとNSURLSessionConfigurationを作成する際に idenrifier(NSString) を使う

試し書き

試しに書いてみたときのメモと置いておきます。

class ViewController: UIViewController, URLSessionTaskDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        let button = UIButton();
        button.frame = CGRect(x: 200, y: 200, width: 120, height: 50);
        button.setTitle("download test", for: UIControlState.normal);
        button.backgroundColor = UIColor.blue;
        button.addTarget(self, action: #selector(onClick(sender:)), for: UIControlEvents.touchUpInside);
        self.view.addSubview(button);
    }
    
    func onClick(sender: UIButton) {
        // let session = URLSession();
        // -> [NSURLSession downloadTaskForRequest:downloadFilePath:resumeData:completion:]: unrecognized selector sent to instance 0x6180000103a0
        // see: https://stackoverflow.com/questions/32905874/nsurlsession-datataskforrequestcompletion-unrecognized-selector-sent-to-instan
        // let config = URLSessionConfiguration.default;
        let config = URLSessionConfiguration.background(withIdentifier: "test");
        // ここのidentiferが同じで、続けてボタン押してDLしようとした場合
        // 2017-07-05 06:58:17.522 NSURLSessionTest[52291:6981685] A background URLSession with identifier test already exists!
        config.sessionSendsLaunchEvents = true;
        // config.isDiscretionary = true;
        
        // URLSessionのコンストラクタでDelegateの設定をすること
        // let session = URLSession(configuration: config);
        let session = URLSession(configuration: config, delegate: self, delegateQueue: nil);
        let url = URL(string: "https://some.page.html");
        
        // let config = URLSessionConfiguration.background(withIdentifier: "test"); <- URLSessionConfiguration.background.default の変更
        // 上記変更はバックグラウンドで処理ができるようにするため
        // この場合は下記のようにタスクを生成する際に、Completion Handlerを渡すのではなく
        // URLSessionTaskDelegateを渡し、レスポンス取得後の処理はDelegateに書く
        // Terminating app due to uncaught exception 'NSGenericException', reason: 'Completion handler blocks are not supported in background sessions. Use a delegate instead.'
        //        let task =  session.downloadTask(with: url!, completionHandler: { (data, response, error) in
        //            DispatchQueue.main.async(execute: {
        //                print(response);
        //                print("DONE");
        //            })
        //        });
        let task =  session.downloadTask(with: url!);
        task.resume();
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    
    // URLSessionTaskDelegateのメソッド
    // データ通信完了時の処理
    // https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411610-urlsession
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if (error != nil) {
            print("HTTPリクエスト失敗処理");
        } else {
            print(task.response);
        }
    }
    
}

参考

久しぶりだったので試しに少し書いて見るだけで力尽きてしまった。もう一回調べたほうがよさそう。 URLSessionTaskDelegate と URLSessionDelegateの違いとか、各Delegateメソッドの違いとか。