Adapter#getView(...)での非同期実行を管理する

旧ブログ(blog.ogaclejapan.com)から移行してきた古い記事です。 移行に伴い、一部のレイアウトが崩れている可能性もありますmm

Author's Avatar mini

Masaki Ogata

AuthorMasaki Ogata

Published

Updated

Androidで一覧画面の使うようなケースではAdapterを使うと思われるが、このgetViewが非常に曲者で参った。。

安易にAsyncTaskでさばくと、非同期部分の負荷次第でスレッドを使い果たしRejectExecutionExceptionが発生する。 なので、代わりにExecutorServiceを使ってみた。

newFixedThreadPoolが良さげ

結論から言うと、Executors.newFixedThreadPoolがこの状況では一番適してる気がする。 内部キューはLinkedBlockingQueueなので、呼び出し側がブロックされるケースは無いし、数値的な根拠はないが実機での操作感が一番良かった…(;´∀`)

Executors.newCachecThreadPool()だと、内部キューにSynchronousQueueを使用してるので、受け取り側の取得が遅れると呼び出し側がブロックされる。 (一気にスクロールダウンしていくとGUIスレッドのプチフリが発生した)

が、Androidの挙動やライフサイクルを考慮すると、ExecutorService+で以下のような機能がほしくなった。

  • onPause時に現在実行中のタスクをすべてキャンセルすることできる

    ExecutorService#shutdown系はonDestoryで呼びたい。 (shutdown系は一回のみなのでonPauseから復活する可能性があるライフサイクルには適していない)

  • 同じIDを処理するタスクが既に実行中の場合はキャンセルしてくれる

    getViewはスクロールするたびに大量に呼ばれるので、既に不要なスレッド資源は早めに停止して再利用したい。

ExecutorServiceライクな自作Wrapperクラス

なので、Executorを拡張して、ExecutorServiceライクな簡易ラッパーを作成してみた。

↓インタフェースはこんな感じ

package com.ogaclejapan;

import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;

/**
* 送信された Runnable タスクを実行するオブジェクトです。
* <p>id毎にタスクを管理しているため、同一idのタスクがまだ動作中の場合は実行をキャンセルしてくれます。</p>
*/

public interface ManagedExecutor extends Executor {

/**
* すべてのタスクが実行を完了していたか、タイムアウトが発生するか、現在のスレッドで割り込みが発生するか、そのいずれかが最初に発生するまでブロックします。
* @param timeout 待機する最長時間
* @param unit 引数の時間単位
* @return この executor が終了した場合は true、終了前にタイムアウトが経過した場合は false
* @throws InterruptedException 待機中に割り込みが発生した場合
*/

boolean await(long timeout, TimeUnit unit) throws InterruptedException;

/**
* 指定した{@link Runnable}を非同期で実行します
* @param r 実行するタスク
* @param id タスクを識別するためのID
* @throws RejectedExecutionException タスクの実行をスケジュールできない場合
*/

void execute(Runnable r, int id);

/**
* 実行中のアクティブなすべてのタスクを停止します。
* <p>このメソッドはActivity#onPauseで呼び出すことを想定しています</p>
*/

void cancel();

/**
* すべてのタスクを停止し、リソースを解放します。
* <p>このメソッドはActivity#onDestoryで呼び出すことを想定しています</p>
*/

void dispose();

}

↓そして実装はこんな感じ

package com.ogaclejapan;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import android.support.v4.util.SparseArrayCompat;


public class ManagedExecutors implements ManagedExecutor {

/**
* 必要に応じ、新規スレッドを作成するスレッドプールを作成しますが、利用可能な場合には以前に構築されたスレッドを再利用します。
* @param corePoolSize
* @param maximumPoolSize
* @return
*/

@SuppressWarnings({ "unchecked", "rawtypes" })
public static ManagedExecutors newCachedThreadPool(int corePoolSize, int maximumPoolSize) {
return new ManagedExecutors(new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 60L, TimeUnit.SECONDS, new SynchronousQueue()));
}

/**
* 必要に応じ、新規スレッドを作成するスレッドプールを作成しますが、利用可能な場合には以前に構築されたスレッドを再利用します。
* @return
*/

public static ManagedExecutors newCachedThreadPool() {
return new ManagedExecutors(Executors.newCachedThreadPool());
}

/**
* 固定数のスレッドを再利用するスレッドプールを作成します。
* @param nThreads
* @return
*/

public static ManagedExecutors newFixedThreadPool(int nThreads) {
return new ManagedExecutors(Executors.newFixedThreadPool(nThreads));
}

/**
* 単一のワーカースレッドを使用する executor を作成します。
* @return
*/

public static ManagedExecutors newSingleThreadExecutor() {
return new ManagedExecutors(Executors.newSingleThreadExecutor());
}

private final ExecutorService es;
private final SparseArrayCompat<ManagedTask> managedMap = new SparseArrayCompat<ManagedTask>();
private final AtomicInteger serialId = new AtomicInteger(Integer.MAX_VALUE);
private final AtomicBoolean disposed = new AtomicBoolean(false);
private final AtomicBoolean cancelling = new AtomicBoolean(false);

private ManagedExecutors(ExecutorService es) {
this.es = es;
}

@Override
public void execute(Runnable r) throws IllegalStateException {
assertDisposed();
if (cancelling.get()) return;
submit(r, serialId.getAndDecrement());
}

@Override
public void execute(Runnable r, int id) throws IllegalStateException {
assertDisposed();
if (cancelling.get()) return;
submit(r, id);
}

@Override
public void cancel() {
if (cancelling.compareAndSet(false, true)) {
try {
final int size = managedMap.size();
for (int i = 0; i < size; i++) {
final ManagedTask storedTask = managedMap.get(i);
if (storedTask != null && !storedTask.future.isDone()) {
storedTask.future.cancel(true);
}
}
} finally {
cancelling.set(false);
}
}
}

@Override
public void dispose() {
if (disposed.compareAndSet(false, true)) {
es.shutdownNow();
managedMap.clear();
}
}

@Override
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
if (disposed.compareAndSet(false, true)) {
if(!es.isShutdown()) es.shutdown();
}
return es.awaitTermination(timeout, unit);
}

private void submit(Runnable r, int id) {
final ManagedTask storedTask = managedMap.get(id);
if (storedTask != null && !storedTask.future.isDone()) {
storedTask.future.cancel(true);
}
managedMap.put(id, new ManagedTask(es.submit(r)));
}

private void assertDisposed() throws IllegalStateException {
if (disposed.get()) throw new IllegalStateException("already disposed.");
}

@SuppressWarnings("rawtypes")
private static class ManagedTask {
final Future future;
private ManagedTask(Future future) {
this.future = future;
}
}

}

これで少しでも資源を有効に活用できたら万々歳( ゚∀゚)o彡°ヌルサク!ヌルサク

Author's Avatar

Masaki Ogata ( a.k.a. ogaclejapan )

5年間ほどAndroidアプリ開発者へ型変換していましたが、Designも含めてサービス開発に必要な技術をすべて吸収していきたいマン。WebとBackendの記憶を只今アップデート中 :P

もし気に入っていただけたら記事シェアのご協力をお願いします!!