woshidan's blog

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

iOSでアプリがバックグラウンドへ遷移してもタスクが終了するまではアプリのプロセスをkillさせないようにする

AndroidではAndroid Oからバックグラウンド処理の実行制限が厳しくなることが話題ですが、iOSでは以前から基本的にはバックグラウンド処理はアプリがバックグラウンドに回った時点で停止させられます。

実際にはiOS側の判断で止めるので、タスクがすぐにkillされる状況は少なくともiOS10の段階ではなかなか開発段階の状況では再現できなかったりします*1。しかし、少なくとも実装時はバックグラウンドに回ったタスクはすぐkillされても困らない前提で書く必要があります。

そして、音楽アプリでの再生処理や地図アプリ用の位置情報の取得、ファイルのダウンロードなどバックグラウンドになってもしばらくの間動き続けることが保証されていてほしいタスクはよくあります。

この場合の対処としてできることは3種類紹介されており、それぞれ概要としては、

  1. Foregroundで短いタスクを開始する場合は、アプリがバックグラウンドへ遷移するときにそのタスクが終了するまでの時間を要求することができる
  2. Foregroundでダウンロード処理を開始する場合は、ダウンロード中にアプリが停止ないし終了してもよいようにそのダウンロード処理の管理をシステム側へ手渡すことができる
  3. 特定の種類のタスク(音楽再生など)を支援するためにバックグラウンドで実行する必要がある場合は、その支援タスクに1つ以上のバックグラウンドでの実行モードを宣言することができる

となっています。

今回は、1. のケースについての実装についてメモします。

実装概要

  1. beginBackgroundTaskWithExpirationHandler: を使ってiOSのSystemへバックグラウンドのタスクを実行するための追加の実行時間をリクエストする。引数のブロックには追加の実行時間が終わっても処理が終了していなかった場合の後処理の内容を記述します。最低限書く必要がある内容は、後の項目の 3. に記載するiOSのSystemに対するタスクの終了通知です。
    1. の際に、何のタスク用、あるいはコードのどこでリクエストしたのかの識別するための値として UIBackgroundIdentifierの値を受け取ります。
  2. GCDのキューなどにバックグラウンドで実行したいタスクを渡して実行します。タスクの最後で、 2. の値を引数に endBackgroundTask: を実行して、iOSのSystemに当該タスクは終了したため、そのために実行時間を取らなくていいことを伝えます。

サンプルコード

// https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html より
UIBackgroundIdentifier *bgTask;

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // リクエストした実行時間が切れてしまい、途中終了する場合に呼ばれる後処理を書く
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        // バックグラウンドに入っても継続して実行したい処理を書く
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}

endBackgroundTask: はUIスレッドからもバックグラウンドスレッドからも呼び出されていますが、ドキュメントによれば問題ありません。

最初、beginBackgroundTaskWithName:expirationHandler:ないしbeginBackgroundTaskWithExpirationHandler:は、公式やQiitaなどにあるサンプルコードの表面だけ見ると applicationDidEnterBackground:メソッドなど、Backgroundに遷移する前のタイミングで一回呼び出せばいいようなバックグラウンドに入る前に一度呼び出すもののように見えました。

しかし、beginBackgroundTaskWithExpirationHandler:によれば、Backgroundに遷移したら中断されたらユーザー体験に影響があるようなタスクのたびにこのメソッドを呼ぶべきだしアプリの実行中のどこで呼ぶこともできる、と書いてあるので、必要なタイミングで begin~end~ が対応するように呼んだほうがわかりやすいかもしれません。

なお、実行中のBackground処理の残り時間が必要な場合は backgroundTimeRemaining プロパティにて確認可能です。

現場からは以上です。

参考

*1:特に、実行時に他のアプリが動いていないシミュレータなどでは(苦笑)。

java.util.TimerをAndroidで使ってみた場合

以前気になったので素振りした時のメモ。AndroidのバージョンはAPI 21です。

まとめ

  • Timer.schedule(TimerTask task, long delay, long interval)delay秒後から、前回のタスクと今回のタスクの実行開始時間の間隔がなるべくintervalになるように定期的にtaskを実行する 
  • Timerのコンストラクタで新しいスレッドが動き出し、定期的に実行されるタイマーのタスクは全てそのスレッドで実行される
  • Activity.onStopが呼び出されてアプリがバックグラウンドへ隠れてもTimerの処理は止まらない

参考:

簡単にコード書いてログで確認してみた

package com.example.woshidan.timertest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity {

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

        Timer timer = new Timer();
        final Random random = new Random();
        final List<Integer> list = new ArrayList<>();
        final long startTime = System.currentTimeMillis();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                Log.d(Thread.currentThread().getName(), (System.currentTimeMillis() - startTime) + " ms. " + "n = " + list.size() + " start ");
                int wait = random.nextInt(1000);
                try {
                    Thread.sleep(wait + 1000); // アップロード処理の終了の代わりに適当にウェイト置いてる
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d(Thread.currentThread().getName(), (System.currentTimeMillis() - startTime) + " ms. " + "n = " + list.size() + " end ");
                list.add(list.size());
            }
        }, 500, 3000);
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d("MainActivity", "onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d("MainActivity", "onDestroy");
    }
}
// 定期的にリクエストされるタイマーのタスクは全て同じスレッドで実行される
D/Timer-0: 500 ms. n = 0 start 
D/Timer-0: 2218 ms. n = 0 end 
D/Timer-0: 3500 ms. n = 1 start // 前回と今回の間のstartの間隔はなるべく3秒間隔
D/Timer-0: 4896 ms. n = 1 end 
D/Timer-0: 6500 ms. n = 2 start 
D/Timer-0: 7733 ms. n = 2 end 
D/Timer-0: 9500 ms. n = 3 start 
D/Timer-0: 11096 ms. n = 3 end 
D/MainActivity: onStop // バックグラウンドになっても止まらない
D/Timer-0: 12500 ms. n = 4 start 
D/Timer-0: 14368 ms. n = 4 end 
D/Timer-0: 15530 ms. n = 5 start // 遅れることもある(前回分の実行に時間がかかったなど)
D/Timer-0: 17289 ms. n = 5 end 
D/Timer-0: 18568 ms. n = 6 start // そうするとその次で「前回と今回の間のstartの間隔はなるべく3秒間隔」を守るために、次以降も時間がずれたままになる
D/Timer-0: 19608 ms. n = 6 end 
D/Timer-0: 21608 ms. n = 7 start 
D/Timer-0: 23448 ms. n = 7 end 
D/Timer-0: 24645 ms. n = 8 start 
D/Timer-0: 26188 ms. n = 8 end 
D/Timer-0: 27678 ms. n = 9 start 
D/Timer-0: 29216 ms. n = 9 end 
D/Timer-0: 30718 ms. n = 10 start 
D/Timer-0: 32033 ms. n = 10 end 
D/Timer-0: 33744 ms. n = 11 start 
D/Timer-0: 34993 ms. n = 11 end 

コードでボタンのStateによって色を変えるようにする

前回の続き的なノリで、コードでボタンのStateによって色を変えるように指定するコードを書いてみます。

API21以上の場合

API21以上の場合は、以下のように記述できます。

        Button button = (Button) findViewById(R.id.button_bg_code);

        // 角丸の設定
        int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius);

        GradientDrawable gradientDrawable = new GradientDrawable();
        gradientDrawable.setCornerRadius(roundRadius);

        int[][] states = new int[][] {
                new int[] {-android.R.attr.state_pressed}, // not pressed
                new int[] { android.R.attr.state_pressed}  // pressed
        };
        int [] colors = new int[] {
                Color.RED,
                Color.GREEN
        };

        // API21以上対応
        gradientDrawable.setColor(new ColorStateList(states, colors));

        if (BuildConfig.VERSION_CODE >= 16) {
            button.setBackground(gradientDrawable);
        } else {
            button.setBackgroundDrawable(gradientDrawable);
        }

API20以下も対応する場合

これで先ほどと同じ振る舞いを確保しようとした場合は下記のようになります。

        Button button = (Button) findViewById(R.id.button_bg_code);

        // 角丸の設定
        int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius);

        GradientDrawable gradientDrawableNotPressed = new GradientDrawable();
        gradientDrawableNotPressed.setCornerRadius(roundRadius);
        gradientDrawableNotPressed.setColor(Color.RED);

        GradientDrawable gradientDrawablePressed = new GradientDrawable();
        gradientDrawablePressed.setCornerRadius(roundRadius);
        gradientDrawablePressed.setColor(Color.GREEN);

        StateListDrawable stateListDrawable = new StateListDrawable();
        stateListDrawable.addState(new int[] {-android.R.attr.state_pressed}, gradientDrawableNotPressed); // not pressed
        stateListDrawable.addState(new int[] {android.R.attr.state_pressed},  gradientDrawablePressed);    // pressed

        if (BuildConfig.VERSION_CODE >= 16) {
            button.setBackground(stateListDrawable);
        } else {
            button.setBackgroundDrawable(stateListDrawable);
        }

現場からは以上です。

参考

ShapeDrawableのRoundRectShapeとGradientDrawableをそれぞれ使って角丸ボタン作ってみた

ShapeDrawableのRoundRectShape、あるいは、GradientDrawableを使うとコードからフチの余白や色を指定できる形で角丸四角形の背景を持つボタンが作ることができます。

もともとShapeDrawableで書いていたのですが、こちらの記事でShapeDrawableのリソースをGradientDrawableコンパイルされていることを知り、GradientDrawableで書いてみたらスッキリ記述できたので書き直した、という話です。

どちらにせよ、リソースファイルから読み込んだほうが基本的に高速なので、あまり表示頻度が多くない箇所をカスタマイズして表示したいとか、そういった場合に使えば良いと思います。

ShapeDrawableのRoundRectShapeで作る場合

同じ指定をxmlで記述した場合

今回はxmlで記述した場合は下記のようになる指定をコードで実装することにします。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
    <padding android:left="@dimen/button_padding_horizontal" />
    <padding android:top="@dimen/button_padding_vertical" />
    <padding android:right="@dimen/button_padding_horizontal" />
    <padding android:bottom="@dimen/button_padding_vertical" />
    <!-- 上記の指定はButton要素に下記属性を追加するのと同じ結果となる。
      android:paddingLeft="@dimen/button_padding_horizontal"
      android:paddingTop="@dimen/button_padding_vertical"
      android:paddingRight="@dimen/button_padding_horizontal"
      android:paddingBottom="@dimen/button_padding_vertical"  -->
    <!-- デフォルトのボタンではButton要素は
      paddingを設定しなくてもButton要素に余白を持っているが、それは
      android.R.drawable.btn_default_normal.xmlなどで
      paddingが指定されているため
       -->
    <solid android:color="#f00"/>
    <corners android:radius="@dimen/button_round_radius"/>
</shape>

RoundRectShapeを利用してコードで記述した場合

        Button button = (Button) findViewById(R.id.button_bg_code);

        // 角丸の設定
        int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius);
        ShapeDrawable shape = new ShapeDrawable();
        shape.setShape(new RoundRectShape(new float[] {roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius},
                null,     // 背景のうち、真ん中の方をくりぬいた形にすることができるのですが、その領域の大きさをRectFで指定可能. 利用しない(=一色塗りつぶしにする)場合はnull
                null));   // 背景のうち、くりぬいた部分の長方形の角丸指定. 利用しない(=くりぬいた部分は長方形のまま)場合はnull

        // 余白の設定
        int horizontalPadding = getResources().getDimensionPixelSize(R.dimen.button_padding_horizontal);
        int verticalPadding = getResources().getDimensionPixelSize(R.dimen.button_padding_vertical);
        shape.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
        shape.getPaint().setColor(Color.RED);

        if (BuildConfig.VERSION_CODE >= 16) {
            button.setBackground(shape);
        } else {
            button.setBackgroundDrawable(shape);
        }
<!-- レイアウト -->
    <Button
        android:id="@+id/button_bg_resource"
        android:layout_below="@id/some_element"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/test"
        android:layout_marginTop="8dp" <!-- 見やすくするためつけてる -->
        android:text="TestTestTestTest" />

    <Button
        android:id="@+id/button_bg_code"
        android:layout_below="@id/button_bg_resource"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp" <!-- 見やすくするためつけてる -->
        android:text="TestTestTestTest" />
<!-- dimenリソースの設定 -->
<resources>
    <dimen name="button_round_radius">8dp</dimen>

    <!-- この辺の元ネタはこちら https://github.com/android/platform_frameworks_base/blob/c29fff50322599f53feadf9cf87df9956c9ac44e/core/res/res/values/dimens_material.xml#L109-L116 -->
    <dimen name="button_padding_horizontal">8dp</dimen>
    <dimen name="button_padding_vertical">4dp</dimen>
</resources>

見た目はこんな感じです。上がdrawableリソースで背景を指定したボタンで下がコードで背景を指定したボタンになります。

f:id:woshidan:20170509141454p:plain

GradientDrawableで作る場合

コードでの記述

        Button button = (Button) findViewById(R.id.button_bg_code);

        // 角丸の設定
        int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius);

        GradientDrawable gradientDrawable = new GradientDrawable();
        gradientDrawable.setCornerRadius(roundRadius);
        gradientDrawable.setColor(Color.RED);

        // 余白の設定はButton要素の属性でやる

        if (BuildConfig.VERSION_CODE >= 16) {
            button.setBackground(gradientDrawable);
        } else {
            button.setBackgroundDrawable(gradientDrawable);
        }
<!-- レイアウト -->
    <Button
        android:id="@+id/button_bg_resource"
        android:layout_below="@id/some_element"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/test"
        android:layout_marginTop="8dp"  <!-- 見やすくするためつけてる -->
        android:text="TestTestTestTest" />

    <Button
        android:id="@+id/button_bg_code"
        android:layout_below="@id/button_bg_resource"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="@dimen/button_padding_horizontal"
        <!-- GradientDrawableの場合はpaddingを設定するメソッドはないので、
        View側にpaddingの指定を置く -->
        android:paddingTop="@dimen/button_padding_vertical"
        android:paddingRight="@dimen/button_padding_horizontal"
        android:paddingBottom="@dimen/button_padding_vertical"
        android:layout_marginTop="8dp" <!-- 見やすくするためつけてる -->
        android:text="TestTestTestTest" />

見た目は変わってません。

参考

AndroidのNDKやABIについてのメモ

はじめに

C系の言語で動いているクロスプラットフォームのブリッジを作成するには、そのコンパイルなどの過程でNDKに関する知識があるとトラブルシューティングやテストケースの設計に役立つことがあります。

なので、ここではその概要をまとめておきます。はじめに用語からまとめようと思ったのですが、

https://developer.android.com/ndk/guides/index.html?hl=ja

Native Development Kit(NDK)は、Android アプリで C および C++ コードを使用できるようにするツールのセットのことです。NDK を使用して独自のソースコードをビルドしたり、事前にビルドされた既存のライブラリを活用したりできます。

https://developer.android.com/ndk/guides/concepts.html?hl=ja

アプリケーション バイナリ インターフェース(ABI)はアプリのマシンコードが実行時にどのようにシステムと連携するかを正確に定義したものです。 NDK はこれらの定義に対して .so ファイルをビルドします。

と言われてもよくわからなかったので、NDKがABIを使ってC, C++ソースコードから.soファイルをビルドする周辺で何をしているかを説明します。

.soファイルとは

と書いたものの、書いた本人は.soファイルについての事前知識が調査開始時点でなかったため、簡単に説明します。

soshared object fileの略です。

.soファイルは、UNIX系の環境下での共通ライブラリファイルのことで、Windowsでいうdllにあたります。具体的には、複数の実行ファイル間で共有して利用される処理の定義がまとめられています。

中身は実行形式ファイルですが、あくまで処理を含んでいるだけでエントリポイントがなく単体で起動することはできません。

各々の実行ファイルに含まれず、実行ファイルを起動した時に動的にロードとリンクが行われるため、動的(ダイナミック)リンクライブラリと呼ばれることがあり、Android Developers GuideのNDKについての記述では主に動的リンクライブラリ、と呼称されています。

Androidでは、アプリのコンパイル時にリンクされている方が.a(静的リンクライブラリ), アプリの起動時にロードされてリンクされる方が.soで、NDKを使ってABIに従って作成される方が基本的に.soという感じです。

自分が作成したライブラリが.so か、.aになるかはコンパイルの設定によりますが、Androidアプリのビルドの文脈だと、.soファイルの方しか、公式のドキュメントに記載がなかったので、こちらが多いかもしれません。

NDKを利用してビルドする際の流れ

https://developer.android.com/ndk/guides/concepts.html のフローをもとに適宜用語の説明を入れながら説明します。

  1. Androidプロジェクトを作成します
  2. Androidプロジェクト直下にjniディレクトリを作成し、ネイティブライブラリ(.aファイル, .soファイルなど.アーキテクチャごとにフォルダを用意したり…)とコンパイル対象のソースコード(CやC言語で書かれたソースコード)とそれらをコンパイルするモジュールにどう含むか記述していくAndroid.mkを配置
  3. 2のディレクトリに任意でターゲットABI, ツールチェーン, リリース/デバッグモード, STLを設定するApplication.mkファイルを作成(任意)
  4. デフォルト設定
  5. ABI: armeabi
    • ABIというのは、ネイティブライブラリをコンパイルする際、コンパイルした.so.aファイルがインストール対象の端末で動くことを保証するための規約みたいなものです。
  6. ツールチェーン: GCC 4.8(4.8 32bit / 4.9 64bit端末対応らしい)
  7. モード: リリース
    • リリースモードだと変数宣言が最適化されていたりして、C言語側のステップ実行が困難になったりします
  8. STL: system
  9. NDKツールを適切に利用するために作成されたシェルのラッパーであるndk-buildを使用して、Cのソースコードをネイティブ(.so, .a. 主に.so?)ライブラリへコンパイルしたり、静的ライブラリをリンクしたり
  10. Javaソースコードをビルドして、DVMで実行可能な.dexファイルを生成
  11. アプリの実行に必要な .so, .dex、その他ファイルをすべてapkファイルにパッケージ化

ところで

.soファイルやネイティブライブラリと.dexを一緒に処理していませんが、JNI用のヘッダとそれに対応した規則のメソッド名で作成していけば、JVM(DVM)上でメソッドテーブルが作成されると理解していて、それにしたがってJava<=>C,C++間でメソッドを利用し合うことが可能みたいです。

この辺を調べていると、CMakeといった言葉もでてきますが、ひとまずこの記事を書くにあたっての状況に限っては(Andorid Studioでビルドをしない場合は)関係なかったので省きます。

CMakeについては、また今度機会あればまとめます。

参考

https://developer.android.com/ndk/guides/concepts.html http://blog.katty.in/4347 http://www.peliphilo.net/archives/681 http://qiita.com/m1takahashi/items/3a3c9d2845e9b57aeda3 http://morado106.blog106.fc2.com/blog-entry-80.html

COUNT関数の引数にNULLが入る場合について

SQLで行数を数える時に利用する COUNT 関数で一番使い慣れているのは COUNT(*) なのですが、 COUNT(col) や、COUNT(1かNULLになる式)といった形を見かけたのでメモ。

見かけた時は面白いなと思ったのに短い。。

iOSのProvisioning Profile周りについてざっくり確認するシートを作りました

iOSのProvisioning Profile周りについて毎回ハマっているような気がしていたので、どのファイルをいつ用意して用意した後どうすればいいのか、といったことを手短に確認するために

  • 主になんて呼ばれているか
  • 代表的なファイル名
  • 主にいつ作成するか、どう作成するか
  • ファイルをDLするか、DLした後どうするか
  • XCode上でそのファイルに関連してどう設定するか
  • そのほか備考

をまとめて、作成する際に必要、など関連のあるところの線を引いた図を作ったので貼ります。

f:id:woshidan:20170331174938p:plain

上の図に千鳥足みたいなのありますが、一つのファイルに対して他方のファイルが複数存在するといった場合は複数存在する方が雑に鳥足みたいにしました。 なんかそういうツールや様式があった気がしますが、それは次の機会ということで。

参考

https://i-app-tec.com/ios/provisioning-profile.html http://macdays.hatenablog.com/entry/2013/10/11/172532 http://qiita.com/edo_m18/items/6f10e57f95b25d9dab4e http://qiita.com/fujisan3/items/d037e3c40a0acc46f618

注釈

※1 Apple Developer Centerに登録した1つのチームから複数の開発者を招待して、その開発者はそれ以外のチームに所属していないという想定。個人開発している人もいるのでいつも事実では無いですが会社アドレスのアカウントでは事実なこと多いのでは
※2 証明書周りがよくわからなくなってむやみやたらに再発行され各種ファイルの作り直しに励むのはよくあるので助けてください
※3 1つのアプリについてProductionとSand Boxというつもり。

気になるところあったらツッコミください。