QA@IT

Scala(sbt-android-plugin)でAndroidのAsyncTaskを使いたい

4400 PV

sbt-android-pluginを使用し、ScalaにてAndroidアプリを作成しているのですが、android.os.AsyncTask<Params, Progress, Result> を使用しようとすると書き方が悪いのかAbstractMethodErrorとなってしまい困っています。

下記のURL先のようにJavaで可変長引数メソッドを上書きしてみるとうまくいくのですが、Scalaだけで何とかする方法はありませんでしょうか?
https://gist.github.com/1175555

今回作成したサンプルのソース

package jp.ponko2.android.sample

import _root_.android.app.Activity
import _root_.android.os.Bundle
import _root_.android.os.AsyncTask
import _root_.android.os.AsyncTask.Status
import _root_.android.widget.{TextView, Toast}
import _root_.android.view.Window

class MainActivity extends Activity {
  private lazy val mTask = new TestTask

  override def onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)

    requestWindowFeature(Window.FEATURE_PROGRESS)
    requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS)

    val values = Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    setContentView(new TextView(this) {
      setText(values.mkString(" + ") + " = ")
    })

    mTask.execute(values:_*)
  }

  override protected def onDestroy() {
    super.onDestroy()

    if (mTask.getStatus == Status.RUNNING) {
      mTask cancel true
    }
  }

  private class TestTask extends AsyncTask[Int, Int, Int] {
    override protected def onPreExecute() {
      setProgressBarVisibility(true)
      setProgressBarIndeterminateVisibility(true)
    }

    override protected def doInBackground(values: Int*): Int = {
      val count = values.length
      var total = 0
      for (i <- 0 until count) {
        publishProgress(((i / count.toFloat) * 10000).toInt)
        Thread.sleep(500)
        total += values(i)
      }
      total
    }

    override protected def onProgressUpdate(progress: Int*) {
      setProgress(progress(0))
    }

    override protected def onPostExecute(result: Int) {
      setProgressBarVisibility(false)
      setProgressBarIndeterminateVisibility(false)
      Toast.makeText(MainActivity.this, result.toString, Toast.LENGTH_LONG).show()
    }
  }
}

ログ

FATAL EXCEPTION: AsyncTask #1
java.lang.RuntimeException: An error occured while executing doInBackground()
    at android.os.AsyncTask$3.done(AsyncTask.java:278)
    at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:273)
    at java.util.concurrent.FutureTask.setException(FutureTask.java:124)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:307)
    at java.util.concurrent.FutureTask.run(FutureTask.java:137)
    at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:208)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
    at java.lang.Thread.run(Thread.java:856)
Caused by: java.lang.AbstractMethodError: abstract method not implemented
    at android.os.AsyncTask.doInBackground(AsyncTask.java)
    at android.os.AsyncTask$2.call(AsyncTask.java:264)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
    ... 5 more

実行環境

  • Scala 2.9.2
  • sbt 0.12.0
  • android-plugin 0.6.2

回答

実行環境が異なるので大丈夫かわかりませんが以下のような書き方で正常に動作しています。

Scala 2.9.1
sbt 0.11.3
android-plugin 0.6.1

import _root_.android.app.Activity
import _root_.android.os.Bundle
import _root_.android.os.AsyncTask
import _root_.android.util.Log

class MainActivity extends Activity with TypedActivity {
  override def onCreate(bundle: Bundle) {
    super.onCreate(bundle)
    setContentView(R.layout.main)

    findView(TR.textview).setText("hello, world!")

    val values:Seq[Int] = 1 to 10

    task.execute(values: _*)
  }
  private lazy val task = new TestTask

  override def onDestroy() {
    super.onDestroy()

    task.cancel(true)
  }

  abstract class MyAsyncTask[A, B, C] extends AsyncTask[A, B, C] {
    override protected def doInBackground(values: A*): C = {
      doInBackgroundImpl(values: _*)
    }
    protected def doInBackgroundImpl(values: A*): C
    override protected def onProgressUpdate(progress: B*) = {
      onProgressUpdateImpl(progress: _*)
    }
    protected def onProgressUpdateImpl(progress: B*)
  }

  class TestTask extends MyAsyncTask[Int, Int, Int] {
    override protected def onPreExecute() {
      Log.d("SampleProject", "onPreExecute()")
    }

    override protected def doInBackgroundImpl(values: Int*): Int = {
      Log.d("SampleProject", "doInBackground()")
      values.foreach {v =>
        Log.d("SampleProject", v.toString)
        publishProgress(v)
        Thread.sleep(1000)
      }
      values.foldLeft(0)(_ + _)
    }

    override protected def onProgressUpdateImpl(progress: Int*) {
      Log.d("SampleProject", "onProgressUpdate()")
      progress.foreach(v => Log.d("SampleProject", v.toString))
      findView(TR.textview).setText(progress.toString)
    }

    override protected def onPostExecute(result: Int) {
      Log.d("SampleProject", "onPostExecute()")
      Log.d("SampleProject", result.toString)
    }
  }
}

上記の書き方は色々試行錯誤していてたまたま動いたもので理由などはよくわからず、漠然と逆コンパイルすれば何かわかるかなと思っていたぐらいなのですが、ご質問いただいたので実際に逆コンパイルで色々試してみました。

以下のように逆コンパイルのテスト用ににジェネリクスを使用したJavaクラスと
型をStringに設定したサブクラスをJavaとScalaで作成、
そして今回の解決策とした型を確定させないScalaのサブクラスを
定義しました。

public class JavaBase<T> {
  public void method1(T param) {
  }
  public void method2(T... params) {
  }
}
public class JavaSub extends JavaBase<String> {
  public void method1(String param) {
  }
  public void method2(String... params) {
  }
}
class ScalaSub extends JavaBase[String] {
  override def method1(param: String) {
  }
  override def method2(params: String*) {
  }
}
class ScalaSub2[T] extends JavaBase[T] {
  override def method1(param: T) {
  }
  override def method2(params: T*) {
  }
}

上記の各クラスを一旦classファイルにコンパイルし、逆コンパイルすると
以下のようになります。

public class JavaBase
{
    public void method1(Object obj)
    {
    }

    public transient void method2(Object aobj[])
    {
    }
}
public class JavaSub extends JavaBase
{
    public volatile void method1(Object obj)
    {
        method1((String)obj);
    }

    public void method1(String s)
    {
    }

    public volatile void method2(Object aobj[])
    {
        method2((String[])aobj);
    }

    public transient void method2(String as[])
    {
    }
}
public class ScalaSub extends JavaBase
    implements ScalaObject
{
    public volatile void method1(Object param)
    {
        method1((String)param);
    }

    public void method1(String s)
    {
    }

    public void method2(String params[])
    {
        method2(((Seq) (Predef$.MODULE$.wrapRefArray((Object[])params))));
    }

    public void method2(Seq seq)
    {
    }
}
public class ScalaSub2 extends JavaBase
    implements ScalaObject
{
    public void method1(Object obj)
    {
    }

    public void method2(Object params[])
    {
        method2(((Seq) (Predef$.MODULE$.genericWrapArray(((Object) (params))))));
    }

    public void method2(Seq seq)
    {
    }
}

ざっと見ていただければわかると思いますが、ジェネリクスで定義した型(T型)はObject型に
置き換えられ、その中で確定させた型を引数とするメソッドを実行するようになっています。
また可変長引数はObject型の配列となっています。
ところが今回問題となっている型を確定させたScalaのサブクラス(ScalaSub)では
Object型の配列を引数とするメソッドが生成されていません
(かわりにStringの配列を引数とするメソッドとなっています)。

ここからは推測となりますが、仮想マシンはジェネリクスな可変長引数のメソッドを
実行する場合は、Object型の配列を引数とするメソッドを探して実行するはずのところ
Scalaのサブクラスにはそのようなメソッドがないため、実行エラーとなっていると考えられます。
可変長引数でない場合は、ScalaのサブクラスでもきちんとObject型引数メソッド内で
String型引数メソッドを実行するようになっているため正常に実行出来ているようです。

編集 履歴 (3)
  • ありがとうございます。この方法でうまくできました。
    しかし、AsyncTaskForAndroidといったものを経由してやらないとうまくいかない理由がとても気になります。
    もし理由を御存知でしたら教えていただけますでしょうか?
    -
  • 逆コンパイルした内容を追記してみました。
    どうもAndroid環境固有の問題と言うよりjavaとscalaの
    連携部分の問題のようでjavaのパラメータ化した型の可変長引数をもつメソッドをscala側でオーバーライドできないようです。
    個人的にはscalaコンパイラのバグではないか?と思います。
    -
  • なるほど。わざわざ調べていただき、ありがとうございます。理由がわかってスッキリしました! -
  • もう結論出ていると思いますが、現行のScalaではJavaを経由しないと無理らしいですね。
    issue検索したらそれっぽいのでてきましたが

    https://issues.scala-lang.org/browse/SI-3899
    https://issues.scala-lang.org/browse/SI-1459

    ちゃんと読んでないので、本当にこの件なのかどうかはわかりません
    -
  • なるほど。それっぽい感じがしますね。報告された日付?のところがかなり昔になっているようだし、根が深いバグなのかな…… -
  • (推測ですが)根が深いというか、このあたりの細かいJava連携の部分をそこまで重要視してないんじゃないですかね。他のこと優先してて手が回らないというか。直接関係ないですが、ちょっと似たようなこんな問題とかもありますし http://d.hatena.ne.jp/xuwei/20120512/1336789905 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。