Android-x86-4.4r2 の設定はこちら


Androidで非同期処理を Thread でやろうとするとこんな例外が出て怒られる。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
UI スレッド以外が UI を操作出来ないって事らしい。

普通は Handler か AsyncTask を使えば回避できるのだが、ここで一つ問題が発生する。

Android のデフォルト動作では画面が回転すると Activity の破棄と再構築が行われる。
したがって AsyncTask が処理を終らせて結果を Activity に通知しようとしても既に存在しない可能性がある。

とりあえずの解決法としては AndroidManifest.xml の <activity> タグに以下の属性を追加して回転しても再構築を行わせないようにすることができる。

android:configChanges="orientation|keyboardHidden"

とは言えこれで逃げられないケースも有るのでまじめな対策を考える。

そもそも、Android の Activity はテンポラリな物で何時破棄されてもおかしく無いという位置付けになっている。にもかかわらずアプリケーションは Activity を中心に設計するようになっているので問題が発生しているように思える。

ここのページの方は Activity はあくまで「従」立場で使えとおっしゃってますが現実には色々難しいかと思います。既存のコードも有るし。


問題の本質は「主」である Activity が「従」である AsyncTask の知らない間に入れ替わってしまっていることだ。

朝、出社したら課長の席に知らないおじさんが座っているようなものである。

こういう場合は人事課に現在の所属課の課長が誰なのか問い合わせられれば良いわけだけど Android にはそういう仕掛けが用意されていない。

と言うわけで、Activity を ID で管理するクラスを用意して AsyncTask は ID から必要な時に Activity 問い合わせる方式で実装しみた。

ソースコード


ActivityManager.java:
package org.kotemaru.android.asyncrotate;

import java.util.HashMap;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;

/**
 * Activityの管理クラス。
 * - Activityが destroy/create されても同一IDで継続的にアクセスできる。
 * - インスタンスをApplication.registerActivityLifecycleCallbacks()に設定する事。
 * - Bundle のキー "___ACTIVITY_ID___" を汚染する。
 * @author kotemaru.org
 */
public class ActivityManager implements ActivityLifecycleCallbacks {
    public final String ACTIVITY_ID = "___ACTIVITY_ID___";
   
    /** Application内で一意のActivityのIDカウンタ */
    private Integer nextActivityId = 0;
   
    // マップ
    private HashMap aid2activity = new HashMap();
    private HashMap activity2aid = new HashMap();

   
    /**
     * ActivityからIDの取得。
     * - すでにIDを持っている場合はそれを返す。
     * - IDを持っていない場合はマップに新規登録して返す。
     * @param activity
     * @return Application内で一意のID
     */
    public synchronized String getActivityId(Activity activity) {
        String aid = activity2aid.get(activity);
        if (aid == null) {
            aid = (nextActivityId++).toString();
            aid2activity.put(aid, activity);
            activity2aid.put(activity, aid);
        }
        return aid;
    }
    /**
     * IDからActivityの取得。
     * - 登録されているIDからActivityを引いて返す。
     * - Activity が destroy/create されていてると更新されている。
     * @param aid ActivityのID
     * @return Activity。未登録の場合はnull。
     */
    public synchronized Activity getActivity(String aid) {
        return aid2activity.get(aid);
    }
   
   
    /**
     * Activity.onCreate()のハンドリング。
     * - Bundleに ___ACTIVITY_ID___ を持っていればマップを更新。
     */
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        if (savedInstanceState == null) return; // First
        String aid = savedInstanceState.getString(ACTIVITY_ID);
        if (aid == null) return; // Not managed.
       
        synchronized (this) {
            aid2activity.put(aid, activity);
            activity2aid.put(activity, aid);
        }
    }
   
    /**
     * Activity.onSaveInstanceState()のハンドリング。
     * - ___ACTIVITY_ID___ にActivityのIDを保存。
     */
    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        String aid = activity2aid.get(activity);
        outState.putString(ACTIVITY_ID, aid);
    }

    /**
     * Activity.onDestroy()のハンドリング
     * - マップからActivityを削除。
     * - Activityインスタンス開放の為、必須。
     */
    @Override
    public synchronized void onActivityDestroyed(Activity activity) {
        String aid = activity2aid.get(activity);
        if (aid == null) return; // Not managed activity.
        aid2activity.put(aid, null);
        activity2aid.remove(activity);
    }
   
   
    @Override
    public void onActivityStarted(Activity activity) {
    }
    @Override
    public void onActivityResumed(Activity activity) {
    }
    @Override
    public void onActivityStopped(Activity activity) {
    }
    @Override
    public void onActivityPaused(Activity activity) {
    }

}

AsyncHelperApplication.java:
package org.kotemaru.android.asyncrotate;

import android.app.Application;

public class AsyncHelperApplication extends Application {
    private ActivityManager activityManager = new ActivityManager();

    @Override
    public void onCreate() {
        // API14 からサポートのActivityのライフサイクルのコールバック設定。
        registerActivityLifecycleCallbacks(activityManager);
    }

    @Override
    public void onTerminate() {
    }

    public ActivityManager getActivityManager() {
        return activityManager;
    }
}
MainActivity.java:
package org.kotemaru.android.asyncrotate;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.widget.TextView;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       
        // 3秒後にメッセージを書き換えるタスクを起動
        TextView m = (TextView) this.findViewById(R.id.message);
        new SlowAsyncTask(this).execute(m.getText()+"{3sec}");
    }
   
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }
}

SlowAsyncTask.java:
package org.kotemaru.android.asyncrotate;

import android.app.Activity;
import android.os.AsyncTask;
import android.widget.TextView;

public class SlowAsyncTask extends AsyncTask {
    private ActivityManager activityManager;
    private String activityId;
   
    public SlowAsyncTask(Activity activity) {
        activityManager = ((AsyncHelperApplication)activity.getApplication()).getActivityManager();
        // Activity の ID を取得して保存。
        activityId = activityManager.getActivityId(activity);
    }
   
    @Override
    protected String doInBackground(String... params) {
        // 時間のかかる非同期処理。
        try {Thread.sleep(3000);} catch (Exception e) { }
        return params[0];
    }

    @Override
    protected void onPostExecute(String result) {
        // Activityは保存しておいたIDから取得する。
        Activity activity = activityManager.getActivity(activityId);
        TextView message = (TextView) activity.findViewById(R.id.message);
        message.setText(result);
    }
}

ソースの解説

とりあえずこれでちゃんと動作している。

API14 からサポートされた ActivityLifecycleCallbacks を利用して Activity の create/destory をフックし入れ替わりを管理している。ID の保存の為に Activity の Bundle を1つ汚染することになるが最小限だろう。

AsyncTask.onPostExecute()が create と destory の間に発生する事を危惧したが起こり得ないようである。onPostExecute() は UI スレッドで呼ばれる事が保証されているので UI の準備の整わない状態で呼ばれないと解釈した。