View on GitHub
mixi-inc/AndroidTraining
非同期処理
この章では、Android での非同期処理について解説します。

参考:Services | Android Developers
参考:Loaders | Android Developers
参考:Processes and Threads | Android Developers

目次

Service

Service とは、バックグラウンドで動作する Android のコンポーネントです。

Service の状態

Serviceは、その呼び出し方によって 2 つのいずれかの状態をとります。

開始

Context#startService(Intent)によって呼び出された時の状態です。
Context#startService(Intent)を呼び出したコンポーネントとは別のライフサイクルを持つので、そのコンポーネントが死んでも生きていることがあります。
通常、この呼出で呼び出されたServiceは、結果を呼び出し元に返すことはありません。Serviceの処理内容が終わったら、自身でServiceのライフサイクルを終わらせる必要があります。

バインド

Context#bindService(Intent)によって呼び出された時の状態です。
Serviceをバインドすることによって、呼び出し元のコンポーネントとServiceの間に、クライアント-サーバの関係が構築されます。
よって、バインドされたServiceと連携したり、リクエストを送ったり、結果を取得したり、プロセス間通信をも行うことが可能となります。
バインドされたServiceのライフサイクルは、バインドしたコンポーネントのライフサイクルに合わせられます。
複数のコンポーネントがServiceをバインドすることも可能ですので、全てのバインドしたコンポーネントが破棄された時に、Serviceのライフサイクルも終了します。

Service のライフサイクル

Serviceがどのように呼び出されたかによって、ライフサイクルが異なります。

Service Lifecycle

Context#startService(Intent)によって開始されたSerivceは、自身が終了を宣言するか、誰かが終了を命令するまで生存し続けます。

Context#bindService(Intent)によってバインドされたServiceは、バインドしている呼び出し元が全員バインドの解除を行うまで生存し続けます。

Service を構築するクラス

Service

一般的な Service を作成するひな形クラスです。
バックグラウンドで動作するものではありますが、その実動作しているスレッドは、Service を動かしているプロセスのメインスレッドです。
つまり、Service 内でブロックする処理を記述すると、メインスレッドの処理がブロックします。よって、独自のプロセスで動かしたい場合は、その旨 AndroidManifest で宣言する必要があり、また、ブロックする処理を実行する場合は、自分でスレッドを新しく立てる必要があります。

また、サービスの状態(開始、またはバインド)によって、プログラムの書き方に若干の違いがあることに注意してください。

下記に、その例を示します。

/**
 * Service を継承したクラス。
 * {@link Context#startService(Intent)}で呼び出すためのサンプル。
 */
public class StartedService extends Service {
    public static final String TAG = StartedService.class.getSimpleName();

    /**
     * {@link Service} のライフサイクルの開始。
     */
    @Override
    public void onCreate() {
        super.onCreate();
        Log.v(TAG, "onCreate");
    }

    // バインド用のメソッド。今回は特に必要ないので null を返す。
    @Override
    public IBinder onBind(Intent intent) {
        Log.v(TAG, "onBind");
        return null;
    }

    /**
     * {@link Context#startService(Intent)} の呼び出しで呼ばれる。
     * このメソッドの処理は、{@link Context#startService(Intent)}を呼び出したスレッドと同じスレッドで実行されるので
     * メインスレッドで {@link Service} を起動した場合に、ここでネットワーク通信などスレッドをブロックする処理をしてしまうと
     * UI の処理がブロックされ AND となる。
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.v(TAG, "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * {@link Service} のライフサイクルの終了。
     */
    @Override
    public void onDestroy() {
        Log.v(TAG, "onDestroy");
        super.onDestroy();
    }
}
/**
 * バインドするサービス。
 */
public class BoundService extends Service {
    public static final String TAG = BoundService.class.getSimpleName();
    private final IBinder mBinder = new ServiceBinder();

    @Override
    public void onCreate() {
        super.onCreate();
        Log.v(TAG, "onCreate");
    }

    // 最初にバインドした時のコールバック
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    // 再度バインドした時のコールバック
    @Override
    public void onRebind(Intent intent) {
        super.onRebind(intent);
        Log.v(TAG, "onRebind");
    }

    // バインドを解除された時のコールバック
    @Override
    public boolean onUnbind(Intent intent) {
        Log.v(TAG, "onUnbind");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.v(TAG, "onDestroy");
    }

    // サービスをバインドした後、バインドしたサービスのインスタンスそのものを得るためのインタフェース
    public class ServiceBinder extends Binder {
        public BoundService getService() {
            return BoundService.this;
        }
    }
}

開始するサービスも、バインドするサービスも、AndroidManifest には以下のように記述します。

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <service
            android:name="jp.mixi.sample.service.BoundService"/>
        <service
            android:name="jp.mixi.sample.service.StartedService"/>
    </application>

IntentService

Intent による呼び出しで開始される Service であり、かつ、Service の開始要求を 1 つずつ順に処理するワーカスレッド上で動作する特別な Service です。 一度に複数の処理を並列して行う必要がない場合は、IntentService を利用するほうが実装が簡単です。

public class MyIntentService extends IntentService {
    public static final String TAG = MyIntentService.class.getSimpleName();

    public MyIntentService() {
        this(MyIntentService.class.getSimpleName());
    }

    public MyIntentService(String name) {
        super(name);
    }

    /**
     * {@link Service} のライフサイクルの開始。
     */
    @Override
    public void onCreate() {
        super.onCreate();
        Log.v(TAG, "onCreate");
    }

    /**
     * 親クラスで必要な処理がひと通り揃っているため、通常は Override の必要はない。
     */
    @Override
    public IBinder onBind(Intent intent) {
        Log.v(TAG, "onBind");
        return super.onBind(intent);
    }

    /**
     * 親クラスで必要な処理がひと通り揃っているため、通常は Override の必要はない。
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.v(TAG, "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * {@link Context#startService(Intent)} によって呼び出される。
     * ワーカスレッド上で実行されるため、ネットワーク通信等のスレッドをブロックする処理を直接記述しても問題ない。
     */
    @Override
    protected void onHandleIntent(Intent intent) {
        Log.v(TAG, "onHandleIntent");
    }

    /**
     * {@link Service} のライフサイクルの終了。
     * {@link IntentService} では、1 回の {@link Context#startService(Intent)} の呼び出しで
     * 1 つのライフサイクルが回るように作られている。
     */
    @Override
    public void onDestroy() {
        Log.v(TAG, "onDestroy");
        super.onDestroy();
    }
}

こちらも、AndroidMafifest には、Serviceと同じ宣言を行います。

Service の呼び出し

開始するサービスと、IntentServiceは、以下のように呼び出します。
開始するサービスは、サービス自身で終了の命令を実行しない場合、サービスを止めるメソッドを呼び出します(IntentServiceは自分で終了するため必要ありません)。

public class MyActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // do something ...

        Intent intent = new Intent(this, StartedService.class);
        startService(intent);
    }

    @Override
    protected void onStop() {
        Intent intent = new Intent(this, StartedService.class);
        stopService(intent);
        super.onStop();
    }
}

バインドするサービスは、以下のように呼び出します。
呼び出した後、バインドしたサービスのインスタンスを受け取って、直接そのインスタンスを操作する事ができるようになります。
バインドするサービスは、バインドするコンポーネントのライフサイクルにしたがって管理する必要があるので、コンポーネントのライフサイクルの終わりにサービスのバインドを解除する必要があります。

public class MyActivity extends Activity {
    private BoundService mBoundService;
    // BoundService を扱う時のインタフェース
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.v(TAG, "onServiceDisconnected");
            mBoundService = null;
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.v(TAG, "onServiceConnected");
            mBoundService = ((BoundService.ServiceBinder) service).getService();
        }
    };

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

        // サービスのバインド
        // バインド時の、バインド元とバインド先サービスの橋渡しをするための ServiceConnection インスタンスを一緒に渡す
        // Context.BIND_AUTO_CREATE によって、バインド時に自動で Service のライフサイクルが始まるようになる
        bindService(new Intent(MainActivity.this, BoundService.class), mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        unbindService(mConnection);
        super.onStop();
    }
}

Loader

非同期に処理を行うための新しいフレームワークです。特に、ネットワークやファイル I/O を介してデータを読み出すためのフレームワークとして設計されているものです。
Activity や Fragment のライフサイクルと、非同期処理を分離する目的で作られており、後述するAsyncTaskの改良版とも言えます。

Loaderは Honeycomb 以後から導入されました。
SupportPackage にも含まれているので、2.x 系の OS でも利用することができます。

AsyncTaskLoader

ネットワーク通信やその他のファイル I/O などによってデータを読み出す場合は、このクラスを拡張して非同期処理を記述します。

// Support Package のものを利用する
import android.support.v4.content.AsyncTaskLoader;

public class MyAsyncTaskLoader extends AsyncTaskLoader<String> {
    public static final String TAG = MyAsyncTaskLoader.class.getSimpleName();
    private String mCachedData;

    public MyAsyncTaskLoader(Context context) {
        super(context);
    }

    // 非同期処理の中身
    @Override
    public String loadInBackground() {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            Log.e(TAG, "interrupted!: ", e);
        }
        return "hogehoge";
    }

    @Override
    public void deliverResult(String data) {
        // ローダがリセットされ、そのローダのライフサイクルが終了となる場合
        if (isReset()) {
            // キャッシュデータがある場合は、キャッシュを削除して、メモリから破棄可能にする
            if (mCachedData != null) {
                mCachedData = null;
            }
            return;
        }

        // 得られたデータをキャッシュする
        mCachedData = data;

        // ローダが開始されている場合、親にデータが得られたことを通知する
        if (isStarted()) {
            super.deliverResult(data);
        }
    }

    @Override
    protected void onStartLoading() {
        // キャッシュがある場合はそちらを返す
        if (mCachedData != null) {
            deliverResult(mCachedData);
            return;
        }

        // データソースに変更があったり、キャッシュデータがない場合は loadInBackground() に行くようにする
        if (takeContentChanged() || mCachedData == null) {
            forceLoad();
        }
    }

    // ローダの非同期処理がストップする時のコールバック
    @Override
    protected void onStopLoading() {
        cancelLoad();
        super.onStopLoading();
    }

    // ローダがリセットされる時のコールバック
    @Override
    protected void onReset() {
        onStopLoading();
        super.onReset();
    }
}

Loader の呼び出しとコールバック

ローダとのやりとりは、LoaderManagerLoaderCallbacksを用いて行います。

public class MainActivity extends FragmentActivity implements LoaderCallbacks<String> {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // ローダの管理をするオブジェクト
        LoaderManager manager = getSupportLoaderManager();
        Bundle argsForLoader = new Bundle();
        // ローダを初期化して非同期処理を開始する
        manager.initLoader(0, argsForLoader, MainActivity.this);
    }

    // id に対応した Loader のインスタンスを作って返す
    // args は Loader に渡したい引数を Bundle に詰めたもの
    @Override
    public Loader<String> onCreateLoader(int id, Bundle args) {
        switch (id) {
            case 0:
                return new MyAsyncTaskLoader(this);
            default:
                return null;
        }
    }

    // 結果を受け取るコールバック
    // メインスレッドで動作する
    @Override
    public void onLoadFinished(Loader<String> loader, String result) {
        Toast.makeText(this, result, Toast.LENGTH_LONG).show();
    }

    // ローダがリセットされる時のコールバック
    @Override
    public void onLoaderReset(Loader<String> loader) {}
}

CursorLoader

別の章で解説する、データベースからのデータの読み出しに特化したクラスです。

AsyncTask

名前の通り、非同期処理のためのクラスです。
非同期に実行したい処理と、処理前、処理後のメインスレッド上での処理を記述するため、このクラスを継承して使います。

AsyncTaskでは、内部でスレッドプールを持っています(Donut までは単一のスレッドで動作していた)。
デフォルトでは 5 つのスレッドがプールされており、内部で並列して複数の非同期処理を実行できるようになっています(Honeycomb 以降は、スレッドの並列実行における諸問題を回避するため単一スレッドで動作するように戻っている)。
最大でプールされるスレッド数は 128 です。

AsyncTaskオブジェクトは、使い回しができません。一度処理が完了すると、その後にもう一度非同期処理の実行を依頼した時点で、例外が投げられます。
よって、再度非同期処理を実行したい場合は、新たにAsyncTaskオブジェクトを作成する必要があります。

/**
 * 非同期処理を実行するためのネストクラス。
 *
 * ジェネリクスの仕組みを用いて、非同期処理に渡す引数の型、進捗を監視するコールバック用の型、非同期処理の結果を表す型を指定する。
 *
 * Activity や Fragment のライフサイクルに合わせて、自分で AsyncTask をコントロールする必要があり、これを行わないと
 * 特に {@link AsyncTask#onPostExecute()} で、参照するオブジェクトが既にメモリから破棄されていて NullPointerException となることが起こりえる。
 */
public class MyAsyncTask extends AsyncTask<Void, Void, Void> {
    private Context mApplicationContext;

    public MyAsyncTask(Context applicationContext) {
        super();
        mApplicationContext = applicationContext;
    }

    /**
     * 非同期処理を実行する前に UI スレッドで実行する処理を書く
     */
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        Toast.makeText(mContext, "onPreExecute", Toast.LENGTH_SHORT).show();
    }

    /**
     * 非同期処理の進捗を受け取るコールバック。
     */
    @Override
    protected void onProgressUpdate(Void... values) {
        super.onProgressUpdate(values);
        Toast.makeText(mContext, "onProgressUpdate", Toast.LENGTH_SHORT).show();
    }

    /**
     * 非同期処理の本体
     * 引数は非同期処理内容に渡すためのパラメータ配列。
     */
    @Override
    protected Void doInBackground(Void... params) {
        // 2 秒おきに進捗を通知する
        try {
            publishProgress();
            Thread.sleep(2000L);
            publishProgress();
            Thread.sleep(2000L);
            publishProgress();
            Thread.sleep(2000L);
            publishProgress();
            Thread.sleep(2000L);
            publishProgress();
            Thread.sleep(2000L);
            publishProgress();
        } catch (InterruptedException e) {
            Log.e(MyAsyncTask.class.getSimpleName(), "thread interrupted: ", e);
        }
        return null;
    }

    /**
     * 非同期処理の実行後に、UI スレッドで実行する処理。
     * 引数は {@link AsyncTask#execute(Object...)} の返り値。
     */
    @Override
    protected void onPostExecute(Void result) {
        super.onPostExecute(result);
        Toast.makeText(mContext, "onPostExecute", Toast.LENGTH_SHORT).show();
    }
}
public class MyActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // do something...

        // 非同期処理の開始
        new MyAsyncTask(getApplicationContext()).execute();
    }
}

実習・課題

プロジェクトの開き方は課題プロジェクトの開き方を参照してください。

実習

この実習では 1-8-AsyncPracticeを使用します。

  1. ServicesActivity を使って各Serviceを呼び出し、Serviceの開始方法の違いによるライフサイクルの違いについてレポートしてください。
    各Serviceはライフサイクルをログを出力するように実装されています。
  2. AsyncTaskLoaderActivity を使ってMyAsyncTaskLoaderがどのようなログが出力されているかをレポートしてください。
    MyAsyncTaskLoaderはライフサイクルをログに出力するように実装されています。
  3. AsyncTaskActivity を使ってAsyncTaskを実行してください。
    AsyncTaskActivity内のTODOコメントの指示に従いコメントを外しUI の処理を実行するとどうなるか、理由を添えてレポートしてください。

課題

この実習では 1-8-AsyncAssignmentを使用します。

  1. AsyncTaskActivity にSharedPreferences にデータを書き込むための IntentService を作成してください。
  2. AsyncTaskActivity に課題1で書き込んだSharedPreferences からデータを読み出すための AsyncTaskLoader を作成してください。