为什么AsyncTask调用execute方法后很久才执行doInBackground方法??

最近在项目中遇到一个比较奇葩的问题, 就是AsyncTask执行了execute方法之后, doInBackground方法很久才得到掉调用. 我们都知道doInBackground方法是在子线程中执行的, 而execute方法执行的线程(一般是在UI线程)肯定与doInBackground方法执行的线程不同.
从execute方法执行的线程切换到doInBackground方法执行的线程为什么会耗时这么久??
毫无疑问这是一个线程调度的问题. 我们不应该怀疑系统调度有问题, 那么剩下的只有我们自己调度的问题......

项目里有一个强制更新的功能, 如下图:

强制更新-未点击按钮

点击强制更新按钮后会显示进度调(正常情况), 如下图:


强制更新-下载中

点击"强制更新"按钮后, 按钮被禁止点击:
downloadApkBtn.setClickable(true);
但是按钮没有变灰, 按钮上的文本也没改成"正在下载..."
因此在某些手机上就出现这样的情况:

点击按钮后, 如图一, ProgressBar进度不动, 而且再点击按钮就没有反应了(按钮设置过selector, 是有按压效果的), 给用户感觉就是: 应用卡顿死掉了

初步怀疑是网络问题, 于是把网络连等相关步骤所耗的时间打印出来. 数据显示, 就算网络非常慢, 也不会花费十多秒的时间. (并且连接也有超时限制, 如果很久不能连接上, 那么就会超时, 总会得到一个结果) 有问题的手机, 点击按钮到进度条动起来的时间有几十秒, 甚至几分钟, 有的就一直卡着不动了. 对于用户来说, 几秒或者十几秒不动, 就直接退出了, 或者......

在调试中发现, 点击按钮后很久, doInBacground()方法中的日志输出都没打印出来. 因此可以得出结论: doInBackground()方法所在的线程没有执行.
于是我把execute()方法执行到doInBackground()方法执行之间的时间也打印出来, 代码如下:

private volatile long start_time;
private void downloadApk() {
    start_time = System.currentTimeMillis();
    new DownloadApkTask().execute();
}

class DownloadApkTask extends AsyncTask<Void, Integer, Boolean> {
    protected Boolean doInBackground(Void... params) {
        Log.e("_ajk_", "schedule time cast: " + ((System.currentTimeMillis() - start_time) / 1000.0));
        //省略 ...... 
    }
}

多次执行启动->点击强制更新按钮->退出动作, 日志输出如下:

macbook-stonedeMacBook-Pro:~ stone$ adb logcat *:E | grep schedule
E/_ajk_   (25685): schedule time cost: 0.014
E/_ajk_   (25685): schedule time cost: 21.618
E/_ajk_   (25685): schedule time cost: 27.859
E/_ajk_   (25685): schedule time cost: 0.004
E/_ajk_   (25685): schedule time cost: 38.368
E/_ajk_   (25685): schedule time cost: 10.722
E/_ajk_   (25685): schedule time cost: 1.66
E/_ajk_   (25685): schedule time cost: 45.632
E/_ajk_   (25685): schedule time cost: 25.374
E/_ajk_   (25685): schedule time cost: 23.161
E/_ajk_   (25685): schedule time cost: 28.733
E/_ajk_   (25685): schedule time cost: 44.852
E/_ajk_   (25685): schedule time cost: 62.171
E/_ajk_   (25685): schedule time cost: 93.201
E/_ajk_   (25685): schedule time cost: 95.367
E/_ajk_   (25685): schedule time cost: 108.974
E/_ajk_   (25685): schedule time cost: 122.609
E/_ajk_   (25685): schedule time cost: 129.158
E/_ajk_   (25685): schedule time cost: 134.632

为什么doInBackground()方法所在线程不能及时执行呢?

查看AsyncTask类源码, 发现AsyncTask里面是使用线程池来执行异步任务的, AsyncTask有两个方法来执行移步任务:

@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}

@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params) {
    if (mStatus != Status.PENDING) {
        switch (mStatus) {
            case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task has already been executed "
                        + "(a task can be executed only once)");
        }
    }

    mStatus = Status.RUNNING;

    onPreExecute();

    mWorker.mParams = params;
    exec.execute(mFuture);

    return this;
}

execute()方法使用的是AsyncTask内部定义的默认线程池, executeOnExecutor()使用的是开发者自定义的线程池. 我们来看看AsyncTask内部自定义的程池到底是什么?
AsyncTask内部定义的默认线程池是SerialExecutor, SerialExecutor及其相关变量的定义如下:

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
    }
};

private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);

/**
 * An {@link Executor} that can be used to execute tasks in parallel.
 */
public static final Executor THREAD_POOL_EXECUTOR
        = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

/**
 * An {@link Executor} that executes tasks one at a time in serial
 * order.  This serialization is global to a particular process.
 */
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

从SerialExecutor这个名字可以看出, 它是一个顺序执行任务的执行器. 实际上SerialExecutor并不去执行任务, 它的execute()方法会委托AsyncTask内部定义的一个线程池去执行任务, 这个默认线程池(THREAD_POOL_EXECUTOR)的定义上面已经给出. THREAD_POOL_EXECUTOR是一个可同时处理处理器个数+1个任务的线程池. 然而使用execute()方法执行AsyncTask时, 并没什么卵用, 因为SerialExecutor的execute()方法会对异步任务进行封装, 使得任务顺序执行, 跟在同一个线程中执行一样, 从而失去并发处理特性. 如果有一个耗时任务正在执行, 那么后续任务将会一直等待前面的任务完成. 因此:

AsyncTask虽然是异步任务, 这个异步指的是与ui线程异步. 如果你想要并发执行多个任务, execute()方法不能实现, executeOnExecutor()方法可以达到目的 (必须提供正确的Executor) ----- 异步跟并发并非同一个概念.

经过上面的分析可以得知, 下载apk的异步任务之前肯定有一个耗时任务在执行, 因此导致下载apk的异步任务一直处于等待队列之中而得不到执行. 那么如何进行验证呢?

我们通过DDMS来查看线程状态, 看看在下载任务之前是有正在处理的任务, 操作如下:
启动DDMS, 在DDMS窗口左侧的Devices视图中选择我们要调试的进程, 然后点击顶部的Update Threads按钮, 这时我们可以在右侧的Threads视图中看到我们选中的进程的所有线程信息, 如图:

使用DDMS查看线程状态

查看AsyncTask类的源码, 我们知道AsyncTask默认线程池中的工作线程的命名格式是AsyncTask #index (index大小为 1~线程池的coreSize), 因此我们看名称为AsyncTask开头的线程就行了 (不得不吐槽一下DDMS做的太烂, 居然不能按线程名排序或分类, 如果线程多了, 那就AsyncTask使用的线程中间就会夹杂其他线程, 这给观察带来不便), 选中某个线程后下面的状态框中会输出此线程的堆栈信息.

通过DDMS查看线程状态, 发现出问题时有一个AsyncTask线程处于Native状态, Native状态是什么鬼? 关于线程状态可参考 DDMS中线程状态的说明, 此文对Native状态的说明是:

native – executing native code – 执行了原生代码,这个对于 带有消息队列的线程是正常的状态,表示消息队列没有任何消息,线程在native 代码中进行无限循环,直到消息队列中出现新的消息,消息队列才会返回Java 代码处理消息。

呃, 线程在native代码中进行无限循环......

我猜想是native层导致某个task的运行一直处于阻塞状态, 这样后面的task也就无法执行, 查看处于native状态的线程的堆栈信息, 如下:

at libcore.io.Posix.recvfromBytes(Native Method)  
at libcore.io.Posix.recvfrom(Posix.java:141)  
at libcore.io.BlockGuardOs.recvfrom(BlockGuardOs.java:164)    
at libcore.io.IoBridge.recvfrom(IoBridge.java:550)    
at java.net.PlainSocketImpl.read(PlainSocketImpl.java:506)    
at java.net.PlainSocketImpl.access$000(PlainSocketImpl.java:46)   
at java.net.PlainSocketImpl$PlainSocketInputStream.read(PlainSocketImpl.java:240) 
at org.apache.http.impl.io.AbstractSessionInputBuffer.fillBuffer(AbstractSessionInputBuffer.java:103) 
at org.apache.http.impl.io.AbstractSessionInputBuffer.read(AbstractSessionInputBuffer.java:134)   
at org.apache.http.impl.io.ContentLengthInputStream.read(ContentLengthInputStream.java:174)   
at org.apache.http.impl.io.ContentLengthInputStream.read(ContentLengthInputStream.java:188)   
at org.apache.http.impl.io.ContentLengthInputStream.close(ContentLengthInputStream.java:121)  
at org.apache.http.conn.BasicManagedEntity.streamClosed(BasicManagedEntity.java:179)  
at org.apache.http.conn.EofSensorInputStream.checkClose(EofSensorInputStream.java:266)    
at org.apache.http.conn.EofSensorInputStream.close(EofSensorInputStream.java:213) 
at com.anjuke.android.newbroker.activity.AutoUpdateActivity$DownloadApkTask.closeInputStream(AutoUpdateActivity.java:345) 
at com.anjuke.android.newbroker.activity.AutoUpdateActivity$DownloadApkTask.doInBackground(AutoUpdateActivity.java:289)   
at com.anjuke.android.newbroker.activity.AutoUpdateActivity$DownloadApkTask.doInBackground(AutoUpdateActivity.java:202)   
at com.anjuke.android.newbroker.util.image.AsyncTask$2.call(AsyncTask.java:337)   
at java.util.concurrent.FutureTask.run(FutureTask.java:237)   
at com.anjuke.android.newbroker.util.image.AsyncTask$SerialExecutor$1.run(AsyncTask.java:279) 
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)    
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)    
at java.lang.Thread.run(Thread.java:848)  

在堆栈中发现了这个com.anjuke.android.newbroker.util.image.AsyncTask, 这个类其实是从这里考过来的, 查看源码发现跟系统的区别不大, 只是对默认线程池进行了版本处理(android 11以下的版本使用单线程来处理异步任务), 代码如下:

/**
 * An {@link Executor} that executes tasks one at a time in serial order.
 * This serialization is global to a particular process.
 */
public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() :
        Executors.newSingleThreadExecutor(sThreadFactory);

根据堆栈信息可以得知, 关闭下载输入流时, 系统阻塞了, 这是非正常关闭输入流, 它是这样发生的:

启动App, 点击强制更新按钮, 这时正在下载文件了, 然后退出退出App (用户下载了一半, 可能不想下载了), 这时就会关闭输入流. 这时的输入流中的数据还没读完, 这样关闭入流就会阻塞 (会不会等待数据读取完毕在关闭??). 再启动app时, 再点击强制更新按钮, 这时启动的下载任务会一直等待前一个下载任务关闭输入流完成. --- 退出APP, APP的进程并没有杀掉( 除非用户按HOME键手动划掉应用或在系统设置的应用管理中强制结束进程, 用户一般不会这么做), 因此下一次启动的APP和上一次启动的APP会共用AsyncTask的线程池.

我们还是用数据说话, 我在程序中打印了关闭输入流的时间, 代码如下:

private void closeInputStream(InputStream inputStream) throws IOException {
    if (inputStream != null) {
        long startTime = System.currentTimeMillis();
        inputStream.close();
        Log.e("_close_", "close inputstream cost: " +((System.currentTimeMillis() - startTime) / 1000.0) + "s");
        inputStream = null;
    }
}

运行调试, 输出信息如下 (反复启动同一个app, 进程是同一个 从下面的日志中可以看出, 圆括号中的为进程ID --- 杀过一次进程):

macbook-stonedeMacBook-Pro:~ stone$ adb logcat *:E | grep _close_
E/_close_ (20394): close inputstream cost: 76.06s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 30.284s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 31.251s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 28.734s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 27.762s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 35.533s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 25.023s
E/_close_ (20394): close inputstream cost: 0.001s
E/_close_ (20394): close inputstream cost: 17.888s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 17.883s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (20394): close inputstream cost: 23.678s
E/_close_ (20394): close inputstream cost: 0.0s
E/_close_ (21037): close inputstream cost: 134.213s
E/_close_ (21037): close inputstream cost: 0.0s
E/_close_ (21037): close inputstream cost: 56.464s
E/_close_ (21037): close inputstream cost: 0.0s
E/_close_ (21037): close inputstream cost: 40.104s
E/_close_ (21037): close inputstream cost: 0.0s
E/_close_ (21037): close inputstream cost: 34.067s
E/_close_ (21037): close inputstream cost: 0.0s

由于doInBackground中多次调用了closeInputStream方法, finally块中关闭输入流时间为0. doInBackground方法的基本逻辑如下:

protected Boolean doInBackground(Void... params) {
    InputStream inputStream = null;
    // 省略 ...... 
    try {
        // 省略 ......
        if (inputStream != null) {
            while (!isCancelDownload && !isFinishing()) { 
                //download apk file .......
            }
        }
        closeInputStream(inputStream);
        return true;
    } catch (Exception e) {
        // 省略 ...... 
        return false;
    } finally {
        try {
            closeInputStream(inputStream);
        } catch (IOException e) {
            // 省略 ...... 
        }
    }
}

还有一个问题就是, 用户启动一次发现下载不了, 就有退出, 那么这一动作就会加入一个下载任务, 如果用户反复执行这一动作, 那么AsyncTask的默认线程池中就会堆积越来越多的下载任务, 而execute()方法导致任务顺序执行, 这样关闭输入流的时间也会累积起来, 也就是启动次数越多, 下载任务等待的时间就会越久. 由于AsyncTask是自定义的, 所以我可以更改源码, 因此我在AsyncTask中加入了Log语句Log.e("_size_", "pending size: " + mTasks.size());, 输出等待执行的任务数量, 更完整的代码如下:

@TargetApi(11)
private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
        Log.e("_size_", "pending size: " + mTasks.size());
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

多次执行启动-点击强制更新按钮-退出动作, log日志如下:

macbook-stonedeMacBook-Pro:~ stone$ adb logcat *:E | grep _size_
E/_size_  (21890): pending size: 0
E/_size_  (21890): pending size: 1
E/_size_  (21890): pending size: 2
E/_size_  (21890): pending size: 2
E/_size_  (21890): pending size: 3
E/_size_  (21890): pending size: 4
E/_size_  (21890): pending size: 5
E/_size_  (21890): pending size: 6
E/_size_  (21890): pending size: 7

日志说明上面的分析是正确的, 任务的确会积累在等待队列中.

至此问题已经分析完毕 !

解决办法: 使用AsyncTask的executeOnExecutor()方法来执行异步任务,且为executeOnExcutor()方法提供一个合理的线程池, 或者直接用Thread来执行下载任务.

总结:

  1. **AsyncTask的execute()方法是顺序执行异步任务的 (相当于Executors.newSingleThreadExecutor()), 每次只执行一个任务. 如果要并发执行多个异步任务, 必须使用AsyncTask的executeOnExecutor()方法, 且必须提供正确的线程池. **

  2. 建议: 尽量不要使用AsynTask的execute()方法来执行异步任务. 如果要使用execute()方法来执行异步任务, 你必须确定这个异步任务无论正常执行/或异常执行都不会耗时 (如果耗时, 会导致后续异步任务得不到及时执行), 且你并不关心它什么时候执行 (因为你无法确认在你执行异步任务之前别人是否已经使用execute()方法执行了一个耗时的异步任务).

如有错误, 欢迎指正!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容