onPostExecute不被称为AsyncTask的(处理程序运行时异常)不被、异常、程序、onPostExecute

2023-09-04 02:58:12 作者:兲亮以後、說晚安ぃ

我有一个的AsyncTask 的获取一些数据,然后更新UI与这个新的数据。它一直在努力罚款个月,但我最近增加了一个功能,当有新的数据显示通知。现在,当我的应用程序是通过通知启动,有时我得到这个例外, onPostExecute 不叫。

这是当应用程序启动会发生什么:

1)展开UI,并找到意见

2)取消了检查新数据的报警(通过 AlarmManager ),并解除报警。 (这是这样,如果用户禁用报警它,他/她会重新启动下一次之前将被取消。)

3)启动的AsyncTask 。如果该应用程序是从通知启动,传递中的数据的一点点然后取消通知。

我卡在这可能是导致此异常。看来,除了从的AsyncTask code,所以我不知道我怎么能解决这个问题。

谢谢!

下面是例外:

  I /我的应用程序(501):doInBackground退出
W / MessageQueue(501):处理器{442ba140}发送消息到一个处理程序上的死线
W / MessageQueue(501):java.lang.RuntimeException的:处理器{442ba140}发送消息到一个处理程序上的死线
W / MessageQueue(501):在android.os.MessageQueue.enqueueMessage(MessageQueue.java:179)
W / MessageQueue(501):在android.os.Handler.sendMessageAtTime(Handler.java:457)
W / MessageQueue(501):在android.os.Handler.sendMessageDelayed(Handler.java:430)
W / MessageQueue(501):在android.os.Handler.sendMessage(Handler.java:367)
W / MessageQueue(501):在android.os.Message.sendToTarget(Message.java:348)
W / MessageQueue(501):在android.os.AsyncTask $ 3.done(AsyncTask.java:214)
W / MessageQueue(501):在java.util.concurrent.FutureTask中$ Sync.innerSet(FutureTask.java:252)
W / MessageQueue(501):在java.util.concurrent.FutureTask.set(FutureTask.java:112)
W / MessageQueue(501):在java.util.concurrent.FutureTask中$ Sync.innerRun(FutureTask.java:310)
W / MessageQueue(501):在java.util.concurrent.FutureTask.run(FutureTask.java:137)
W / MessageQueue(501):在java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)
W / MessageQueue(501):在java.util.concurrent.ThreadPoolExecutor中的$ Worker.run(ThreadPoolExecutor.java:561)
W / MessageQueue(501):在java.lang.Thread.run(Thread.java:1096)
 
Android性能优化典范 第5季

编辑:这是我在我的主要活动(一个打开的通知)的onCreate 方法。有一些 onClickListeners ,我省略了以节省空间。我不认为他们有任何的影响,因为它们的按钮连接到没有被pressed。

  @覆盖
公共无效的onCreate(包savedInstanceState){
    super.onCreate(savedInstanceState); //调用父

    的setContentView(R.layout.main); //创建一个XML文件中的用户界面

    //查找UI元素
    控制=(SlidingDrawer)findViewById(R.id.drawer); //包含
    //按钮
    //漫画=(ImageView的)findViewById(R.id.comic); //显示漫画
    字幕=(TextView中)findViewById(R.id.subtitleTxt); //文本框的
    // 字幕
    prevBtn =(按钮)findViewById(R.id. prevBtn); //将previous按钮
    nextBtn =(按钮)findViewById(R.id.nextBtn); //下一个按钮
    randomBtn =(按钮)findViewById(R.id.randomBtn); //随机按钮
    fetchBtn =(按钮)findViewById(R.id.comicFetchBtn); //转到特定的ID按钮
    mostRecentBtn =(按钮)findViewById(R.id.mostRecentBtn); //该按钮即可进入最新的漫画
    comicNumberEdtTxt =(EditText上)findViewById(R.id.comicNumberEdtTxt); //文本框来缩放图像视图设置
    zoomControl =新DynamicZoomControl();

    zoomListener =新长征pressZoomListener(本);
    zoomListener.setZoomControl(zoomControl);

    zoomComic =(ImageZoomView)findViewById(R.id.zoomComic);
    zoomComic.setZoomState(zoomControl.getZoomState());
    zoomComic.setImage(BitmapFactory.de codeResource(getResources(),R.drawable.defaultlogo));
    zoomComic.setOnTouchListener(zoomListener);

    zoomControl.setAspectQuotient(zoomComic.getAspectQuotient());

    resetZoomState();

    //进入新的ID
    IMM =(InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); //用于隐藏软键盘

    Log.i(LOG_TAG的第一本漫画开始装载);
    。INT notificationComicNumber = getIntent()getIntExtra(漫画,-1);
    Log.i(LOG_TAG,喜剧人数从意向:+ notificationComicNumber);
    如果(notificationComicNumber == -1){
        取=新MyFetcher(这一点,zoomComic,字幕,控制,comicNumberEdtTxt,IMM,zoomControl);
        fetch.execute(MyFetcher.LAST_DISPLAYED_COMIC);
    } 其他 {
        取=新MyFetcher(这一点,zoomComic,字幕,控制,comicNumberEdtTxt,IMM,zoomControl);
        fetch.execute(notificationComicNumber);
        ((NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE))cancelAll();
    }
    Log.i(LOG_TAG的结束加载新的漫画);

    Log.i(LOG_TAG,第一次运行开始检查);
    //获取共享preferences
    preFS = getShared preferences(preFS,Context.MODE_PRIVATE);

    //检查,如果这是应用程序的这个版本第一次运行
    如果(prefs.getBoolean(firstRun-+ MAJOR_VERSION_NUMBER,真)){
        prefs.edit()putBoolean(firstRun-+ MAJOR_VERSION_NUMBER,假).commit()。
        firstRunVersionDialog();
    }

    //检查,如果这是程序的第一个运行
    如果(prefs.getBoolean(firstRun,真)){
        prefs.edit()putBoolean(firstRun,假).commit()。
        firstRunDialog();
    }
    Log.i(LOG_TAG,首次运行检查完成);

            // OnClickListener S为按钮省略以节省空间
 

编辑2:我已经挖通过Android源$ C ​​$ C追查其中的例外是来自。这是处理程序线456和 sendMessageAtTime 457

  msg.target =这一点;
发送= queue.enqueueMessage(味精,uptimeMillis);
 

这是 enqueueMessage MessageQueue

 最后布尔enqueueMessage(消息味精,时长){
        如果(msg.when!= 0){
            抛出新AndroidRuntimeException(MSG
                    +此消息已被使用。);
        }
        如果(msg.target == NULL和放大器;&安培;!mQuitAllowed){
            抛出新的RuntimeException(主线程不准跳槽);
        }
        同步(本){
            如果(mQuiting){
                RuntimeException的E =新的RuntimeException(
                    msg.target +发送消息到一个死线程处理程序);
                Log.w(MessageQueue,e.getMessage(),E);
                返回false;
            }否则,如果(msg.target == NULL){
                mQuiting = TRUE;
            }

            msg.when时=;
            //Log.d("MessageQueue,Enqueing:+ MSG);
            消息P = mMessages;
            如果(P == NULL ||当== 0 ||当< p.when){
                msg.next = P;
                mMessages =味精;
                this.notify();
            } 其他 {
                消息preV = NULL;
                而(P = NULL和放大器;!&安培; p.when< =时){
                    preV = P;
                    P = p.next;
                }
                msg.next = prev.next;
                prev.next =味精;
                this.notify();
            }
        }
        返回true;
    }
 

我是什么 mQuiting 有点糊涂,但它看起来像previous时间 enqueueMessage 被称为 msg.target 为null。

解决方案

要概括乔纳森·佩罗的解决方案,他确定具体的错误,我用在使用AsyncTask的任何一类以下。活套/处理器/后是如何运行的东西在UI线程在Android应用程序上的任何地方没有传承的句柄活动或其他方面。添加这种静态初始化块类中:

  {// http://stackoverflow.com/questions/4280330/onpostexecute-not-being-called-in-asynctask-handler-runtime-exception
    活套活套= Looper.getMainLooper();
    处理程序处理程序=新的处理程序(活套);
    handler.post(新的Runnable(){
      公共无效的run(){
        尝试 {
          的Class.forName(android.os.AsyncTask);
        }赶上(ClassNotFoundException异常E){
          e.printStackTrace();
        }
      }
    });
}
 

我们曾试图让单元测试运行时遇到的问题。我发现了一个解决方法是,但还没有具体确定的问题。我们只知道,尝试使用AsyncTask的<>在Android的JUnit测试造成onPostExecute()不被调用。现在大家知道为什么。

这篇文章展示了如何在Android JUnit测试运行多线程异步code:

Using CountDownLatch Android中AsyncTask的基础JUnit测试

对于非UI的单元测试使用,我创建android.test.InstrumentationTestCase一个简单的子类。它有一个OK的标志和的CountDownLatch。复位()或复位(计数)创建一个新的CountDownLatch({1,}计数)。好()设置OK = TRUE,count--,并在锁定calls.countDown()。不好()设置OK =假,并计算了所有的方式。 waitForIt(秒)等待超时或coundown锁存到零。然后调用assertTrue(OK)。

然后测试这样的:

  someTest(){
  复位();
  asyncCall(参数,新someListener(){
    公共无效成功(参数){好(); }
    公共无效失败(参数){不好(); }
  });
  waitForIt();
}
 

在AsyncTask的静态初始化错误,因为,我们不得不运行我们的实际测试里面一个Runnable传递给runTestOnUiThread()。通过上述的适当的静态初始化,这不应该是必要的,除非通话被测试需要在UI线程上运行。

另外成语我现在用的是测试当前线程是否是UI线程,然后运行在正确的线程所请求的操作不管。有时候,这是有道理的,以允许呼叫者要求同步与异步,覆盖在必要的时候。例如,网络请求应始终在后台线程中运行。在大多数情况下,AsyncTask的线程池是为这个完美的。只要认识到,只有一定数量将同时运行,阻止其他请求。为了测试当前线程是否是UI线程:

 布尔onUiThread = Looper.getMainLooper()getThread()== Thread.currentThread()。
 

然后使用的AsyncTask℃的简单的子类(只是doInBackground()和onPostExecute()需要);>要在非UI线程或handler.post()或postDelayed()运行到UI线程上运行

给调用者运行的同步或异步的选项看起来像(让这里没有显示一个本地有效onUiThread值;添加本地布尔同上):

 无效的方法(最终的args,同步,监听,callbakOnUi){
  Runnable运行=新的Runnable(){公共无效的run(){
    //方法的code ...用的args或类成员。
    如果(听众!= NULL)监听器(结果);
    //或者,如果调用code预计监听器的UI线程上运行:
    如果(callbackOnUi&安培;&安培;!onUiThread)
      handler.post(新的Runnable(){公共无效的run(){监听器()}});
    否则听者();
  };
  如果(同步)run.run(); 。其他新MyAsync()执行(运行);
  //或联网code:
  如果(同步和放大器;&安培;!onUiThread)run.run(); 。其他新MyAsync()执行(运行);
  //或者,对于一些必须要在UI线程上运行:
  如果(同步和放大器;&安培; onUiThread)run.run()其他handler.post(运行);
}
 

此外,使用的AsyncTask可以非常简单和简洁。使用RunAsyncTask.java的定义如下,然后写code是这样的:

  RunAsyncTask大鼠=新RunAsyncTask();
    rat.execute(新的Runnable(){公共无效的run(){
        doSomethingInBackground();
        交(新的Runnable(){公共无效的run(){somethingOnUIThread();}});
        postDelayed(新的Runnable(){公共无效的run(){somethingOnUIThreadInABit();}},100);
    }});
 

或者干脆:新RunAsyncTask()执行(新的Runnable(){公共无效的run(){doSomethingInBackground();}});

RunAsyncTask.java:

 包st.sdw;
进口android.os.AsyncTask;
进口android.util.Log;
进口android.os.Debug;

公共类RunAsyncTask扩展的AsyncTask< Runnable接口,串,龙> {
    字符串变量=RunAsyncTask;
    对象上下文= NULL;
    布尔isDebug = FALSE;
    公共RunAsyncTask(对象的背景下,字符串变量,布尔调试){
      this.context =背景;
      TAG =标签;
      isDebug =调试;
    }
    保护龙doInBackground(可运行...运行){
      长结果= 0L;
      长时间启动= System.currentTimeMillis的();
      对于(Runnable运行:运行){
        跑跑();
      }
      返回System.currentTimeMillis的() - 启动;
    }
    保护无效onProgressUpdate(字符串...值){}
    保护无效onPostExecute(长时间){
      如果(isDebug&安培;&放大器;时间> 1)Log.d(TAG,RunAsyncTask跑进来:+时间+MS);
      ν=零;
    }
    在preExecute保护无效(){}
    / **城堆,可靠地触发碰撞原生堆损坏。根据需要调用。 * /
    公共静态无效memoryProbe(){
      System.gc()的;
      运行时运行时间=调用Runtime.getRuntime();
      双人分配=新的双(Debug.getNativeHeapAllocatedSize())/ 1048576.0;
      双可用=新的双(Debug.getNativeHeapSize())/ 1048576.0;
      双人自由=新的双(Debug.getNativeHeapFreeSize())/ 1048576.0;
      长maxMemory = runtime.maxMemory();
      长totalMemory = runtime.totalMemory();
      长freeMemory = runtime.freeMemory();
     }
 }
 

I have an AsyncTask that fetches some data and then updates the UI with this new data. It has been working fine for months, but I recently added a feature that displays a notification when there is new data. Now when my app is launched through the notification, sometimes I get this exception and onPostExecute is not called.

This is what happens when the app is launched:

1) Expand the UI and find views

2) Cancel the alarm (through AlarmManager) that checks for new data and reset the alarm. (This is so that if the user disables the alarm it is cancelled before the next time he/she reboots.)

3) Start the AsyncTask. If the app was launched from the notification, pass in a little bit of the data and then cancel the notification.

I'm stuck on what could be causing this exception. It seems that the exception is from the AsyncTask code, so I'm not sure how I can fix it.

Thanks!

Here is the exception:

I/My App(  501): doInBackground exiting
W/MessageQueue(  501): Handler{442ba140} sending message to a Handler on a dead thread
W/MessageQueue(  501): java.lang.RuntimeException: Handler{442ba140} sending message to a Handler on a dead thread
W/MessageQueue(  501):  at android.os.MessageQueue.enqueueMessage(MessageQueue.java:179)
W/MessageQueue(  501):  at android.os.Handler.sendMessageAtTime(Handler.java:457)
W/MessageQueue(  501):  at android.os.Handler.sendMessageDelayed(Handler.java:430)
W/MessageQueue(  501):  at android.os.Handler.sendMessage(Handler.java:367)
W/MessageQueue(  501):  at android.os.Message.sendToTarget(Message.java:348)
W/MessageQueue(  501):  at android.os.AsyncTask$3.done(AsyncTask.java:214)
W/MessageQueue(  501):  at java.util.concurrent.FutureTask$Sync.innerSet(FutureTask.java:252)
W/MessageQueue(  501):  at java.util.concurrent.FutureTask.set(FutureTask.java:112)
W/MessageQueue(  501):  at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:310)
W/MessageQueue(  501):  at java.util.concurrent.FutureTask.run(FutureTask.java:137)
W/MessageQueue(  501):  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)
W/MessageQueue(  501):  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561)
W/MessageQueue(  501):  at java.lang.Thread.run(Thread.java:1096)

EDIT: Here is my onCreate method in my main activity (the one opened by the notification). There are some onClickListeners that I omitted to save space. I don't think they should have any effect, since the buttons they are attached to are not being pressed.

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); // Call the parent

    setContentView(R.layout.main); // Create the UI from the XML file

    // Find the UI elements
    controls = (SlidingDrawer) findViewById(R.id.drawer); // Contains the
    // buttons
    // comic = (ImageView) findViewById(R.id.comic); // Displays the comic
    subtitle = (TextView) findViewById(R.id.subtitleTxt); // Textbox for the
    // subtitle
    prevBtn = (Button) findViewById(R.id.prevBtn); // The previous button
    nextBtn = (Button) findViewById(R.id.nextBtn); // The next button
    randomBtn = (Button) findViewById(R.id.randomBtn); // The random button
    fetchBtn = (Button) findViewById(R.id.comicFetchBtn); // The go to specific id button
    mostRecentBtn = (Button) findViewById(R.id.mostRecentBtn); // The button to go to the most recent comic
    comicNumberEdtTxt = (EditText) findViewById(R.id.comicNumberEdtTxt); // The text box to Zooming image view setup
    zoomControl = new DynamicZoomControl();

    zoomListener = new LongPressZoomListener(this);
    zoomListener.setZoomControl(zoomControl);

    zoomComic = (ImageZoomView) findViewById(R.id.zoomComic);
    zoomComic.setZoomState(zoomControl.getZoomState());
    zoomComic.setImage(BitmapFactory.decodeResource(getResources(), R.drawable.defaultlogo));
    zoomComic.setOnTouchListener(zoomListener);

    zoomControl.setAspectQuotient(zoomComic.getAspectQuotient());

    resetZoomState();

    // enter the new id
    imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); // Used to hide the soft keyboard

    Log.i(LOG_TAG, "beginning loading of first comic");
    int notificationComicNumber = getIntent().getIntExtra("comic", -1);
    Log.i(LOG_TAG, "comic number from intent: " + notificationComicNumber);
    if (notificationComicNumber == -1) {
        fetch = new MyFetcher(this, zoomComic, subtitle, controls, comicNumberEdtTxt, imm, zoomControl);
        fetch.execute(MyFetcher.LAST_DISPLAYED_COMIC);
    } else {
        fetch = new MyFetcher(this, zoomComic, subtitle, controls, comicNumberEdtTxt, imm, zoomControl);
        fetch.execute(notificationComicNumber);
        ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll();
    }
    Log.i(LOG_TAG, "ending loading of new comic");

    Log.i(LOG_TAG, "first run checks beginning");
    // Get SharedPreferences
    prefs = getSharedPreferences("prefs", Context.MODE_PRIVATE);

    // Check if this is the first run of the app for this version
    if (prefs.getBoolean("firstRun-" + MAJOR_VERSION_NUMBER, true)) {
        prefs.edit().putBoolean("firstRun-" + MAJOR_VERSION_NUMBER, false).commit();
        firstRunVersionDialog();
    }

    // Check if this is the first run of the app
    if (prefs.getBoolean("firstRun", true)) {
        prefs.edit().putBoolean("firstRun", false).commit();
        firstRunDialog();
    }
    Log.i(LOG_TAG, "First run checks done");

            // OnClickListener s for the buttons omitted to save space

EDIT 2: I've been digging through Android source code tracking down where the exception is coming from. This is lines 456 and 457 of sendMessageAtTime in Handler:

msg.target = this;
sent = queue.enqueueMessage(msg, uptimeMillis);

And this is enqueueMessage from MessageQueue:

    final boolean enqueueMessage(Message msg, long when) {
        if (msg.when != 0) {
            throw new AndroidRuntimeException(msg
                    + " This message is already in use.");
        }
        if (msg.target == null && !mQuitAllowed) {
            throw new RuntimeException("Main thread not allowed to quit");
        }
        synchronized (this) {
            if (mQuiting) {
                RuntimeException e = new RuntimeException(
                    msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                return false;
            } else if (msg.target == null) {
                mQuiting = true;
            }

            msg.when = when;
            //Log.d("MessageQueue", "Enqueing: " + msg);
            Message p = mMessages;
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                this.notify();
            } else {
                Message prev = null;
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
                msg.next = prev.next;
                prev.next = msg;
                this.notify();
            }
        }
        return true;
    }

I'm a little confused about what mQuiting is, but it looks like the previous time enqueueMessage was called msg.target was null.

解决方案

To generalize Jonathan Perlow's solution to the bug he identified specifically, I use the following in any class that uses AsyncTask. The looper/handler/post is how you can run something on the UI thread anywhere in an Android app without passing down a handle to an activity or other context. Add this static initialization block inside the class:

{ // http://stackoverflow.com/questions/4280330/onpostexecute-not-being-called-in-asynctask-handler-runtime-exception
    Looper looper = Looper.getMainLooper();
    Handler handler = new Handler(looper);
    handler.post(new Runnable() {
      public void run() {
        try {
          Class.forName("android.os.AsyncTask");
        } catch (ClassNotFoundException e) {
          e.printStackTrace();
        }
      }
    });
}

We had run into the problem when trying to get unit tests to run. I found a workaround for that, but hadn't specifically identified the problem. We only knew that trying to use AsyncTask<> in Android JUnit test caused onPostExecute() not to be called. Now we know why.

This post shows how to run multithreaded async code in an Android JUnit test:

Using CountDownLatch in Android AsyncTask-based JUnit tests

For use with non-UI unit tests, I created a simple subclass of android.test.InstrumentationTestCase. It has an "ok" flag and a CountDownLatch. reset() or reset(count) creates a new CountDownLatch({1,count}). good() sets ok=true, count--, and calls.countDown() on the latch. bad() sets ok=false, and counts down all the way. waitForIt(seconds) waits for timeout or the coundown latch to zero. Then it calls assertTrue(ok).

Then tests are like:

someTest() {
  reset();
  asyncCall(args, new someListener() {
    public void success(args) { good(); }
    public void fail(args) { bad(); }
  });
  waitForIt();
}

Because of the AsyncTask static initialization bug, we had to run our actual tests inside a Runnable passed to runTestOnUiThread(). With proper static initialization as above, this shouldn't be necessary, unless the call being tested needs to run on the UI thread.

The other idiom I now use is to test whether the current thread is the UI thread and then run the requested action on the proper thread regardless. Sometimes, it makes sense to allow the caller to request sync vs. async, overriding when necessary. For instance, network requests should always be run on a background thread. In most cases, AsyncTask thread pooling is perfect for this. Just realize that only a certain number will run at once, blocking additional requests. To test whether the current thread is the UI thread:

boolean onUiThread = Looper.getMainLooper().getThread() == Thread.currentThread();

Then use a simple subclass (just doInBackground() and onPostExecute() are needed) of AsyncTask<> to run on a non-UI thread or handler.post() or postDelayed() to run on the UI thread.

Giving the caller the option to run sync or async looks like (getting a locally valid onUiThread value not shown here; add local booleans as above):

void method(final args, sync, listener, callbakOnUi) {
  Runnable run = new Runnable() { public void run() {
    // method's code... using args or class members.
    if (listener != null) listener(results);
    // Or, if the calling code expects listener to run on the UI thread:
    if (callbackOnUi && !onUiThread)
      handler.post(new Runnable() { public void run() {listener()}});
    else listener();
  };
  if (sync) run.run(); else new MyAsync().execute(run);
  // Or for networking code:
  if (sync && !onUiThread) run.run(); else new MyAsync().execute(run);
  // Or, for something that has to be run on the UI thread:
  if (sync && onUiThread) run.run() else handler.post(run);
}

Also, using AsyncTask can be made very simple and concise. Use the definition of RunAsyncTask.java below, then write code like this:

    RunAsyncTask rat = new RunAsyncTask("");
    rat.execute(new Runnable() { public void run() {
        doSomethingInBackground();
        post(new Runnable() { public void run() { somethingOnUIThread(); }});
        postDelayed(new Runnable() { public void run() { somethingOnUIThreadInABit(); }}, 100);
    }});

Or simply:new RunAsyncTask("").execute(new Runnable(){public void run(){ doSomethingInBackground(); }});

RunAsyncTask.java:

package st.sdw;
import android.os.AsyncTask;
import android.util.Log;
import android.os.Debug;

public class RunAsyncTask extends AsyncTask<Runnable, String, Long> {
    String TAG = "RunAsyncTask";
    Object context = null;
    boolean isDebug = false;
    public RunAsyncTask(Object context, String tag, boolean debug) {
      this.context = context;
      TAG = tag;
      isDebug = debug;
    }
    protected Long doInBackground(Runnable... runs) {
      Long result = 0L;
      long start = System.currentTimeMillis();
      for (Runnable run : runs) {
        run.run();
      }
      return System.currentTimeMillis() - start;
    }
    protected void onProgressUpdate(String... values) {        }
    protected void onPostExecute(Long time) {
      if (isDebug && time > 1) Log.d(TAG, "RunAsyncTask ran in:" + time + " ms");
      v = null;
    }
    protected void onPreExecute() {        }
    /** Walk heap, reliably triggering crash on native heap corruption.  Call as needed. */  
    public static void memoryProbe() {
      System.gc();
      Runtime runtime = Runtime.getRuntime();
      Double allocated = new Double(Debug.getNativeHeapAllocatedSize()) / 1048576.0;
      Double available = new Double(Debug.getNativeHeapSize()) / 1048576.0;
      Double free = new Double(Debug.getNativeHeapFreeSize()) / 1048576.0;
      long maxMemory = runtime.maxMemory();
      long totalMemory = runtime.totalMemory();
      long freeMemory = runtime.freeMemory();
     }
 }