【Android开发进阶系列】多线程专题

1 概述

        在操作系统中,线程是操作系统调度的最小单元,同时线程又是一种受限的系统资源,即线程不可能无限制地产生,并且线程的创建和销毁都会有相应的开销。当系统中存在大量的线程时,系统会通过会时间片轮转的方式调度每个线程,因此线程不可能做到绝对的并行。

        如果在一个进程中频繁地创建和销毁线程,显然不是高效的做法。正确的做法是采用线程池,一个线程池中会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。

        关于线程池的理解,参考Java线程池详解,Android同样适用。

        Android中常见线程使用方式有以下几种:

    1) Activity.runOnUiThread(Runnable)

    2) View.post(Runnable);View.postDelay(Runnable, long)

    3) Handler

    4) AsyncTask


2 Activity.runOnUiThread方式

        利用Activity.runOnUiThread(Runnable)把更新ui的代码创建在Runnable中,然后在需要更新ui时,把这个Runnable对象传给Activity.runOnUiThread(Runnable)。

        Runnable对像就能在ui程序中被调用。如果当前线程是UI线程,那么行动是立即执行。如果当前线程不是UI线程,操作是发布到事件队列的UI线程。

public class TestActivity extends Activity{

   Button btn;

   @Override

   protected void onCreate(BundlesavedInstanceState) {

     // TODO Auto-generated method stub

     super.onCreate(savedInstanceState);

     setContentView(R.layout.handler_msg);

     btn = (Button) findViewById(R.id.button1);

     btn.setOnClickListener(new OnClickListener(){

        @Override

        public void onClick(View view) {

           // TODO Auto-generated method stub

           new Thread(new Runnable(){

             @Override

             public void run() {

                // TODO Auto-generatedmethod stub

                //模拟耗时的操作。

                try {

                  Thread.sleep(10000);

                } catch (InterruptedException e) {

                  // TODO Auto-generated catch block

                  e.printStackTrace();

                }


                // 更新主线程UI

                TestActivity.this.runOnUiThread(new Runnable(){

                  @Override

                  public void run() {

                    // TODO Auto-generated method stub

                    btn.setText("更新完毕!");

                  }

                });

              }

            }).start();

        }

    });

}


3 View.post()/postDelayed()方式

        我们知道,Handler有postDelayed()/post()等API,在UI线程中,通过默认构造方法newHandler(),会创建一个与当前线程的Looper绑定的Handler对象,UI线程的消息循环是由框架层打开(Looper.loop()),APP开发者无需关注。维护一个挂在UI线程的Handler成员变量用以发消息/处理消息,是惯常的代码风格。

        当然,还有另外一类API:View.postDelayed()/post()。Android官方文档介绍这类API也是向UI线程发消息,Runnable执行在UI线程中。与Handler.postDelayed()/post()一样,View.postDelayed()/post()的API Level是1,是非常古老的API。

public boolean post(Runnable action){

    final AttachInfo attachInfo = mAttachInfo;

    if(attachInfo != null){

        return attachInfo.mHandler.post(action);

    }


    // Assume that post will succeed later

    ViewRootImpl.getRunQueue().post(action);

    return true;

}

        将任务交由attachInfo中的Handler处理,保证在UI线程执行。从本质上说,它还是依赖于以Handler、Looper、MessageQueue、Message为基础的异步消息处理机制。相对于新建Handler进行处理更加便捷。因为attachInfo中的Handler其实是由该View的ViewRootImpl提供的,所以post方法相当于把个事件添加到了UI 事件列中。下面举一个常用的例子,比如在onCreate方法中获取某个view的宽高,而直接View#getWidth获取到的值是0。要知道View显示到界面上需要经历onMeasure、onLayout和onDraw三个过程,而View的宽高是在onLayout阶段才能最终确定的,而在Activity#onCreate中并不能保证View已经执行到了onLayout方法,也就是说Activity的声明周期与View的绘制流程并不是一一绑定。那为什么调用post方法就能起作用呢?首先MessageQueue是按顺序处理消息的,而在setContentView()后队列中会包含一条询问是否完成布局的消息,而我们的任务通过View#post方法被添加到队列尾部,保证了在layout结束以后才执行。

4 Handler+Thread方式

        Android主线程包含一个消息队列(MessageQueue),该消息队列里面可以存入一系列的Message或Runnable对象。通过一个Handler你可以往这个消息队列发送Message或者Runnable对象,并且处理这些对象。每次你新创建一个Handle对象,它会绑定于创建它的线程(也就是UI线程)以及该线程的消息队列,从这时起,这个handler就会开始把Message或Runnable对象传递到消息队列中,并在它们出队列的时候执行它们。    

Handler Thread原理图

        Handler可以把一个Message对象或者Runnable对象压入到消息队列中,进而在UI线程中获取Message或者执行Runnable对象,Handler把压入消息队列有两类方式,Post和sendMessage:

    Post方式:

        Post允许把一个Runnable对象入队到消息队列中。它的方法有:

post(Runnable)/postAtTime(Runnable,long)/postDelayed(Runnable,long)

        对于Handler的Post方式来说,它会传递一个Runnable对象到消息队列中,在这个Runnable对象中,重写run()方法。一般在这个run()方法中写入需要在UI线程上的操作。

handler post用法

    sendMessage:

        sendMessage允许把一个包含消息数据的Message对象压入到消息队列中。它的方法有:sendEmptyMessage(int)/sendMessage(Message)/sendMessageAtTime(Message, long)/sendMessageDelayed(Message, long)

        Handler如果使用sendMessage的方式把消息入队到消息队列中,需要传递一个Message对象,而在Handler中,需要重写handleMessage()方法,用于获取工作线程传递过来的消息,此方法运行在UI线程上。Message是一个final类,所以不可被继承。

handler定义
handler sendMessage用法

    优缺点:

        1. Handler用法简单明了,可以将多个异步任务更新UI的代码放在一起,清晰明了;

        2.处理单个异步任务代码略显多;

    适用范围

        1. 多个异步任务的更新UI;

5 AsyncTask

5.1 AsyncTask简介

        AsyncTask是一个抽象类,它是由Android封装的一个轻量级异步类(轻量体现在使用方便、代码简洁),它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新UI。

        AsyncTask的内部封装了两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler)。

        其中SerialExecutor线程池用于任务的排队,让需要执行的多个耗时任务,按顺序排列THREAD_POOL_EXECUTOR线程池才真正地执行任务InternalHandler用于从工作线程切换到主线程

5.1.1 AsyncTask的泛型参数

        AsyncTask的类声明如下:

public abstract class AsyncTask

    AsyncTask是一个抽象泛型类。

    其中,三个泛型类型参数的含义如下:

        Params开始异步任务执行时传入的参数类型;

        Progress异步任务执行过程中,返回下载进度值的类型;

        Result异步任务执行完成后,返回的结果类型;

        如果AsyncTask确定不需要传递具体参数,那么这三个泛型参数可以用Void来代替。

      有了这三个参数类型之后,也就控制了这个AsyncTask子类各个阶段的返回类型,如果有不同业务,我们就需要再另写一个AsyncTask的子类进行处理。

5.1.2 AsyncTask的核心方法

** onPreExecute()**

        这个方法会在后台任务开始执行之间调用,在主线程执行。用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。

doInBackground(Params...)

        这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。

        任务一旦完成就可以通过return语句来将任务的执行结果进行返回,如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress(Progress...)方法来完成。

onProgressUpdate(Progress...)

        当在后台任务中调用了publishProgress(Progress...)方法后,这个方法就很快会被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,在主线程中进行,利用参数中的数值就可以对界面元素进行相应的更新。

onPostExecute(Result)

        当doInBackground(Params...)执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI操作,在主线程中进行,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。

        上面几个方法的调用顺序:

onPreExecute() -->doInBackground() --> publishProgress() --> onProgressUpdate() -->onPostExecute()

        如果不需要执行更新进度则为onPreExecute() -->doInBackground() --> onPostExecute(),

        除了上面四个方法,AsyncTask还提供了onCancelled()方法,它同样在主线程中执行,当异步任务取消时,onCancelled()会被调用,这个时候onPostExecute()则不会被调用,但是要注意的是,AsyncTask中的cancel()方法并不是真正去取消任务,只是设置这个任务为取消状态,我们需要在doInBackground()判断终止任务。就好比想要终止一个线程,调用interrupt()方法,只是进行标记为中断,需要在线程内部进行标记判断然后中断线程。

5.1.3 AsyncTask的简单使用

class DownloadTask extends AsyncTask { 

    @Override 

    protected void onPreExecute() { 

        progressDialog.show(); 

    } 


    @Override 

    protected Boolean doInBackground(Void... params) { 

        try { 

            while (true) { 

                int downloadPercent = doDownload(); 

                publishProgress(downloadPercent); 

                if (downloadPercent >= 100) { 

                    break; 

                } 

            } 

        } catch (Exception e) { 

            return false; 

        } 

        return true; 

    } 


    @Override 

    protected void onProgressUpdate(Integer... values) { 

        progressDialog.setMessage("当前下载进度:" + values[0] + "%"); 

    } 


    @Override 

    protected void onPostExecute(Boolean result) { 

        progressDialog.dismiss(); 

        if (result) { 

            Toast.makeText(context, "下载成功", Toast.LENGTH_SHORT).show(); 

        } else { 

            Toast.makeText(context, "下载失败", Toast.LENGTH_SHORT).show(); 

        } 

    } 

        这里我们模拟了一个下载任务,在doInBackground()方法中去执行具体的下载逻辑,在onProgressUpdate()方法中显示当前的下载进度,在onPostExecute()方法中来提示任务的执行结果。如果想要启动这个任务,只需要简单地调用以下代码即可:

    new DownloadTask().execute(); 

5.2 使用AsyncTask的注意事项

    ① 异步任务的实例必须在UI线程中创建,即AsyncTask对象必须在UI线程中创建。

    ② execute(Params... params)方法必须在UI线程中调用。

    ③ 不要手动调用onPreExecute(),doInBackground(Params...

params),onProgressUpdate(Progress... values),onPostExecute(Result result)这几个方法。

    ④ 不能在doInBackground(Params... params)中更改UI组件的信息。

    ⑤ 一个任务实例只能执行一次,如果执行第二次将会抛出异常。


5.3 AsyncTask的源码分析

    先从初始化一个AsyncTask时,调用的构造函数开始分析。

public AsyncTask() {

        mWorker = new WorkerRunnable() {

            public Result call() throws Exception {

                mTaskInvoked.set(true);

                Result result = null;

                try {

                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

                    //noinspection unchecked

                    result = doInBackground(mParams);

                    Binder.flushPendingCommands();

                } catch (Throwable tr) {

                    mCancelled.set(true);

                    throw tr;

                } finally {

                    postResult(result);

                }

                return result;

            }

        };


        mFuture = new FutureTask(mWorker) {

            @Override

            protected void done() {

                try {

                    postResultIfNotInvoked(get());

                } catch (InterruptedException e) {

                    android.util.Log.w(LOG_TAG, e);

                } catch (ExecutionException e) {

                    throw new RuntimeException("An error occurred while executing doInBackground()",

                            e.getCause());

                } catch (CancellationException e) {

                    postResultIfNotInvoked(null);

                }

            }

        };

    }

        这段代码虽然看起来有点长,但实际上并没有任何具体的逻辑会得到执行,只是初始化了两个变量,mWorker和mFuture,并在初始化mFuture的时候将mWorker作为参数传入。mWorker是一个Callable对象,mFuture是一个FutureTask对象,这两个变量会暂时保存在内存中,稍后才会用到它们。 FutureTask实现了Runnable接口,关于这部分内容可以看这篇文章

        mWorker中的call()方法执行了耗时操作,即result = doInBackground(mParams);,然后把执行得到的结果通过postResult(result);,传递给内部的Handler跳转到主线程中。在这里这是实例化了两个变量,并没有开启执行任务。

        那么mFuture对象是怎么加载到线程池中,进行执行的呢?

        接着如果想要启动某一个任务,就需要调用该任务的execute()方法,因此现在我们来看一看execute()方法的源码,如下所示:

public final AsyncTask execute(Params... params) {

        return executeOnExecutor(sDefaultExecutor, params);

    }


        调用了executeOnExecutor()方法,具体执行逻辑在这个方法里面:

  public final AsyncTask 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;

    }

        可以 看出,先执行了onPreExecute()方法,然后具体执行耗时任务是在exec.execute(mFuture),把构造函数中实例化的mFuture传递进去了。

    exec具体是什么?

        从上面可以看出具体是sDefaultExecutor,再追溯看到是SerialExecutor类,具体源码如下:

private static class SerialExecutor implements Executor {

        final ArrayDeque mTasks = new ArrayDeque();

        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 类的execute方法。SerialExecutor 是个静态内部类,是所有实例化的AsyncTask对象公有的,SerialExecutor 内部维持了一个队列,通过锁使得该队列保证AsyncTask中的任务是串行执行的,即多个任务需要一个个加到该队列中,然后执行完队列头部的再执行下一个,以此类推。

        在这个方法中,有两个主要步骤。

    ① 向队列中加入一个新的任务,即之前实例化后的mFuture对象。

    ② 调用 scheduleNext()方法,调用THREAD_POOL_EXECUTOR执行队列头部的任务。


        由此可见SerialExecutor 类仅仅为了保持任务执行是串行的,实际执行交给了THREAD_POOL_EXECUTOR

THREAD_POOL_EXECUTOR又是什么?

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

threadPoolExecutor.allowCoreThreadTimeOut(true);

THREAD_POOL_EXECUTOR = threadPoolExecutor;

        实际是个线程池,开启了一定数量的核心线程和工作线程。然后调用线程池的execute()方法。执行具体的耗时任务,即开头构造函数中mWorker中call()方法的内容。先执行完doInBackground()方法,又执行postResult()方法,下面看该方法的具体内容:

private Result postResult(Result result) {

        @SuppressWarnings("unchecked")

        Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,

                new AsyncTaskResult(this, result));

        message.sendToTarget();

        return result;

}


        该方法向Handler对象发送了一个消息,下面具体看AsyncTask中实例化的Hanlder对象的源码:

private static class InternalHandler extends Handler {

        public InternalHandler() {

            super(Looper.getMainLooper());

        }


        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})

        @Override

        public void handleMessage(Message msg) {

            AsyncTaskResult result = (AsyncTaskResult) msg.obj;

            switch (msg.what) {

                case MESSAGE_POST_RESULT:

                    // There is only one result

                    result.mTask.finish(result.mData[0]);

                    break;

                case MESSAGE_POST_PROGRESS:

                    result.mTask.onProgressUpdate(result.mData);

                    break;

            }

        }

    }


        在InternalHandler 中,如果收到的消息是MESSAGE_POST_RESULT,即执行完了doInBackground()方法并传递结果,那么就调用finish()方法。

private void finish(Result result) {

        if (isCancelled()) {

            onCancelled(result);

        } else {

            onPostExecute(result);

        }

        mStatus = Status.FINISHED;

    }


        如果任务已经取消了,回调onCancelled()方法,否则回调 onPostExecute()方法。

        如果收到的消息是MESSAGE_POST_PROGRESS,回调onProgressUpdate()方法,更新进度。

        InternalHandler是一个静态类,为了能够将执行环境切换到主线程,因此这个类必须在主线程中进行加载。所以变相要求AsyncTask的类必须在主线程中进行加载。

        到此为止,从任务执行的开始到结束都从源码分析完了。

5.3.1 AsyncTask的串行和并行

        从上述源码分析中分析得到,默认情况下AsyncTask的执行效果是串行的,因为有了SerialExecutor类来维持保证队列的串行。如果想使用并行执行任务,那么可以直接跳过SerialExecutor类,使用executeOnExecutor()来执行任务。

5.3.2 AsyncTask使用不当的后果

    1)生命周期

        AsyncTask不与任何组件绑定生命周期,所以在Activity/或者Fragment中创建执行AsyncTask时,最好在Activity/Fragment的onDestory()调用 cancel(boolean);

    2) 内存泄漏

        如果AsyncTask被声明为Activity的非静态的内部类,那么AsyncTask会保留一个对创建了AsyncTask的Activity的引用。如果Activity已经被销毁,AsyncTask的后台线程还在执行,它将继续在内存里保留这个引用,导致Activity无法被回收,引起内存泄露。

    3) 结果丢失

        屏幕旋转或Activity在后台被系统杀掉等情况会导致Activity的重新创建,之前运行的AsyncTask(非静态的内部类)会持有一个之前Activity的引用,这个引用已经无效,这时调用onPostExecute()再去更新界面将不再生效。

6 参考链接

Android中的几种多线程实现

https://www.cnblogs.com/chendu123/p/6081301.html


Android多线程的四种方式

http://mobile.51cto.com/android-537207.htm


runOnUiThread更新主线程

https://www.cnblogs.com/wanqieddy/p/4153203.html


View.postDelayed()/post()原理与使用(1)

https://www.2cto.com/kf/201708/667701.html


不可不知的开发技巧之View.Post()

https://www.jianshu.com/p/b1d5e31e2011


Android中的线程状态之AsyncTask详解

https://www.jianshu.com/p/817a34a5f200

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

推荐阅读更多精彩内容