AsyncTask的小分析

471 查看

前言

今天项目经理给无(jing)所(pi)事(li)事(jin)的我安排了一个小任务,要在他的专属阅读APP进入首页后加载一张网络上的美女图片,说这样才能安心看书,恩,这还不简单,我第一时间想到了AsyncTask。

实现

protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        /*......
                        此处省略1万行代码
        ......*/
        
        BeautifulGirlTask girlTask = new BeautifulGirlTask();
        girlTask.execute();
        
        /*......
                        此处省略2万行代码
        ......*/
    }

搞定,坐等升职加薪迎娶白富美喽。
第二天,项目经理说有时美女图片加载很慢,过很久才出来,严重影响了他学习。学你妹,仔细查了查代码我发现其他同学也为项目经理写了很多AsynTask小任务,难道这之间有什么影响吗。

分析

首先,先写一个用于测试的AsyncTask, doInBackground方法中让线程休眠1秒,来模拟任务执行,并且在任务开始执行时打印出任务id和线程id,

public class TestTask extends AsyncTask<Void, Void, Void>
{
    private int id;
    
    public TestTask(int id)
    {
        this.id = id;
    }

    @Override
    protected Void doInBackground(Void... params)
    {
        Log.d("morven","morven--- task-"+id+" start"+",tid="+Thread.currentThread().getId());
        
        try
        {
            Thread.sleep(1000);
        }
        catch (InterruptedException e)
        {
            Log.e("TAG", "sleep interrupt");
        }
        return null;
    }
}

然后,循环创建18个任务并执行,

for (int i=0; i<18; i++)
{
    TestTask task = new TestTask(i);
    task.execute();
}

来看看执行结果:

每个任务的启动时间都间隔一秒,说明任务执行是线性的,恩,这么一说我想起来某某大牛的blog里说AsyncTask的执行可以是线性的也可以是并发的;从线程id来看,从第9个任务开始都是使用的同一个线程。

再来试试并发执行,我们将task创建并执行的代码改成下面这样,顺便将cpu核心数也打印出来:

int CPU_COUNT = Runtime.getRuntime().availableProcessors();
Log.d("morven","morven--- CPU_COUNT="+CPU_COUNT);
for (int i=0; i<18; i++)
{
    TestTask task = new TestTask(i);
    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

看看结果:

8核手机就是牛,从日志里看task0-8都是在25:59同时触发执行的,task9-17都是在1秒后同时触发执行,观察线程id也能看出,后面9个task重用了前面9个task的线程,噢,线程池里有9个线程,这跟8核有什么关系吗。

源码分析

带着疑问,打开源码看看,

原来AsyncTask里有两个执行器,一个用于串行执行,一个用于并行执行,这不就是我们熟悉的java.concurrent里的线程池嘛。

先看并行执行的ThreadPoolExecutor,它的前两个参数CORE_POOL_SIZE和MAXIMUM_POOL_SIZE分别定义了线程池的核心线程数和最大线程数,sPoolWorkQueue提供了线程池的工作队列,线程池运行的规则如下:
(1)一个任务被提交给线程池,如果当前线程池中线程数量小于核心线程数(CORE_POOL_SIZE),那么创建一个新的线程去执行任务。
(2)如果任务被提交给线程池后,线程池中线程数量已等于核心线程数,那么看核心线程中是否有空闲线程,如果有则用空闲线程执行任务,否则将任务放入工作队列等待。
(3)如果任务提交后,没有空闲核心线程且工作队列也满了,那么判断当前核心线程数目是否小于最大核心线程数(MAXIMUM_POOL_SIZE),如果小于,那么创建一个新的线程作为核心线程执行该任务。

在源码中,我们看到,CORE_POOL_SIZE被赋值为CPU核心数+1,MAXIMUM_POOL_SIZE赋值为CPU核心数×2+1,

所以在前面的测试中,AsyncTask创建了9个核心线程,一次可并发执行9个任务。

工作队列的大小为128,

那么在我的机器上,如果一次提交9+128=137个任务,且这些任务都未执行完,那么工作队列将被放满,后续再提交的(2×8+1=17)-9 = 8个任务将会在新的线程中执行。

我们将任务的sleep时间改为100秒,创建200个task看看:

for (int i=0; i<200; i++)
{
    TestTask task = new TestTask(i);
    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
try
{
    Thread.sleep(100000);
}
catch (InterruptedException e)
{
    Log.e("TAG", "sleep interrupt");
}

看看结果:

线程池开始创建了9个线程去运行task0-8,后面提交的task都被放入工作队列中了,直到队列中放满128个task,当第138个任务(task-137)提交执行时,线程池创建了新的线程去执行,一共创建了8个,咦,看图中怎么是9个,有个tid=1的线程,恩,当工作队列已满且无法创建新线程时,任务将会被交给handler执行,也就是主线程,所以我们的UI已经卡死不能响应了。

结束语

AsyncTask给开发带来了极大的方便,但作为小白,总觉得交给别人管理的东西总是行为古怪,除非了解管理的机制。oh,我可不想做回原始人哦。

恩,元旦来啦,祝大家开(jia)心(ban)~