关于Android线程的那些事儿

1. 线程的定义

线程thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

一个进程中可以并发多个线程,每条线程并行执行不同的任务。每个线程都会分配一个私有内存区域,该区域主要用于在执行过程中存储方法、局部变量和参数。一旦线程终止私有内存区将会被销毁。更多关于进程与线程的区别和联系请参照上一篇文章关于Android进程的那些事儿,在此不再重复。


2. Android的单线程模式

Android的系统启动流程和应用启动流程这篇文章中有提到,Android应用启动时,zygote进程fork自身,开启一个Linux进程和一个主线程(Main Thread),主线程负责UI显示、更新、控件交互,因此主线程又叫做UI线程。

Android中的单线程模式必须遵循两条规则:

  • 不要阻塞 UI 线程
    原因:
    由于UI绘制和事件分发采用单线程模式,当UI线程中执行耗时操作(网络访问或数据库查询)时,UI线程会被阻塞,从而出现ANR(application not responding)或者类似NetworkOnMainThreadException之类的错误。

  • 不要在 UI 线程之外访问 Android UI 工具包
    原因:
    UI的显示、更新和控件交互主要通过Android UI 工具包(Android UI toolkit,包括android.widget和android.view)来实现的,而Android UI toolkit并非线程安全的工具包,如果我们在工作线程中刷新UI的时候,主线程也恰好在刷新UI,就会出现冲突。因此Android不允许在UI线程之外进行UI的相关操作,否则会出现CalledFromWrongThreadException的错误。


3. 工作线程和UI线程(主线程)

为了遵循上述单线程模式的两条规则,我们必须将耗时操作放在工作线程中,将UI操作放在UI线程中,同时有需要两个线程之间的切换。

3.1 主线程中开启工作线程

Java中提供了两种方法:Thread和Runnable

  • Thread
    继承Thread类覆写run()然后thread.start()

  • Runnable
    实现Runnable接口复写run()然后New Thread(Runnable).start()。

但是,在Android中这两种方法是不值得推荐的。最好使用Android自带的Handler,AsyncTask,IntentService, Loader,Service等方式来实现。下面会详细说明。

3.2 工作线程中访问UI线程

下面三个方法可以访问UI线程:

  • Activity.runOnUIThread(Runable)
  • View.post(Runable)
  • View.postDelayed(Runable,long)

但是如果主进程和工作线程之间需要更复杂的交互和操作,上面的方法就无能为力了,现在轮到Handler, AsyncTask, HandlerThread上场了,下面会展开详述。


4. 线程间通信

4.1 Handler

由于Android单线程模式下的两条原则的存在,线程间的通信显得尤为重要,Handler正是用来解决这个问题的。消息(Message)的传递需要Handler,MessageQueue, Looper共同来完成。其中Looper在Handler和MessageQueue之间起到桥梁和纽带的作用。在此之前,我们有必要了解一下相关概念

4.1.1 相关概念
  • Message
    Message类实现了Parcelable接口,因此它可以包含任意的数据对象,并把它传递给Handler。尽管Message的构造方法是public,但是我们还是推荐使用Message.obtain()或者Message.obtainMessage()来得到一个Message实例。

  • MessageQueue
    MessageQueue(消息队列)顾名思义,Message组成的一个List,但是Message并不是直接加入到MessageQueue的,而是通过与Looper绑定的Handler来实现的。

    获取当前线程的MessageQueue的方法是Looper.MyQueue()。

  • Looper
    Looper类用来操作一个线程的消息循环。用法如下:

class LooperThread extends Thread {
      public Handler mHandler;

      public void run() {
          Looper.prepare();//创建looper

          mHandler = new Handler() {
              public void handleMessage(Message msg) {
                  // process incoming messages here
              }
          };

          Looper.loop();//
      }
  }
  • Handler
    handler跟一个线程和该线程的messagequeue绑定的,当我们新建一个Handler的时候,它就会与当前线程,和线程的消息队列绑定在一起。Handler负责传递Message或者Runnable对象给MessageQueue,并当他们从消息队列出来的时候执行相关操作。

Handler有两个用途:
(1)安排message和runnable在将来的某个时刻得到执行
常用实现方法有
post(Runnable)
[postAtTime(Runnable, long)](https://developer.android.com/reference/android/os/Handler.html#postAtTime(java.lang.Runnable, long))
[postDelayed(Runnable, long)](https://developer.android.com/reference/android/os/Handler.html#postDelayed(java.lang.Runnable, long))
sendEmptyMessage(int)
sendMessage(Message)
[sendMessageAtTime(Message, long)](https://developer.android.com/reference/android/os/Handler.html#sendMessageAtTime(android.os.Message, long))
[sendMessageDelayed(Message, long)](https://developer.android.com/reference/android/os/Handler.html#sendMessageDelayed(android.os.Message, long))
其中,Message的处理是通过Handler的 handleMessage(Message)来实现的(需要继承Handler的基类)。
(2)在其他线程中执行某些操作。
Handler可以在线程间传递消息,工作原理如下

4.1.2 工作机制

Handler用来发送和处理Message或者Runnable对象,每个Handler实例都会绑定到一个线程和这个线程的MessageQueue。当创建Handler的时候,他会默认绑定到创建它的线程上,他会给MessageQueue发送message和runnable,在Looper轮训到该条消息的时候,回调创建该消息的Handler的handlerMessage方法,处理从message queue出来的message和runnable。
具体流程如下图:

Handler,MessageQueue,Looper的工作流程

当应用启动时,新的进程被创建的时候,它的主线程会运行一个message queue来管理上层应用对象(Activity,Broadcast Receiver)和创建的窗口。也可以创建工作线程,然后创建Handler,在工作线程中调用postRunnable或者sendMessage类方法来传递消息给主线程。这时message或者Runnable对象将会在handler的message queue中排队并在它出站时得到执行。

Looper, Handler and MessageQueue之间的关系

4.2 AsyncTask

4.2.1 使用方法

首先,继承AsyncTask类,至少重写doInBackground这个方法。比如

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
@Override
     protected Long doInBackground(URL... urls) {//这个方法必须被重写
         int count = urls.length;
         long totalSize = 0;
         for (int i = 0; i < count; i++) {
             totalSize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
             // Escape early if cancel() is called
             if (isCancelled()) break;
         }
         return totalSize;
     }
@Override
     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
     }
@Override
        protected void onPreExecute() {
        }
@Override
     protected void onPostExecute(Long result) {//doInBackground执行完成之后调用UI线程
         showDialog("Downloaded " + result + " bytes");
     }
 } 

然后在主线程中执行:

new DownloadFilesTask().execute(url1, url2, url3);
4.2.2 执行过程

一个异步任务的4步走:

  1. onPreExecute

  2. doInBackground

  3. onProgressUpdate

  4. onPostExecute
    从方法名可以理解这四个方法的功能和执行顺序,不再赘述。

4.2.3 注意事项
  1. 上述四个方法不可以手动调用
  2. 每个task的加载,创建,执行都必须在UI线程中执行。
  3. 该任务只能执行一次,第二次执行会抛异常
  4. AsyncTask开启线程的方法asyncTask.execute()默认是也是开启一个线程和一个队列的,不过也可以通过asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 0)开启一个含有5个新线程的线程池,也就是说有个5个队列了,假如说你执行第6个耗时任务时,除非前面5个都还没执行完,否则任务是不会阻塞的,这样就可以大大减少耗时任务延迟的可能性。

除了常用的Handler和AsyncTask之外,IntentService和HandlerThread也为线程间的通信提供了极大地便利

4.3 IntentService

有了intentService就可以不用自己启动和结束线程了,IntentService内部会自动执行线程的开启和销毁。

4.3.1 使用步骤

(1)继承IntentService类,并重写onHandleIntent()方法,这个方法中,执行耗时操作

public class myIntentService extends IntentService {

    public myIntentService() {
        super("myIntentService");
        // 注意构造函数参数为空,这个字符串就是worker thread的名字
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //根据Intent的不同进行不同的事务处理 
        String taskName = intent.getExtras().getString("taskName");  
        switch (taskName) {
        case "task1":
            Log.i("myIntentService", "do task1");
            break;
        case "task2":
            Log.i("myIntentService", "do task2");
            break;
        default:
            break;
        }       
    }
  //--------------------用于打印生命周期--------------------    
   @Override
  public void onCreate() {
        Log.i("myIntentService", "onCreate");
    super.onCreate();
}

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("myIntentService", "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        Log.i("myIntentService", "onDestroy");
        super.onDestroy();
    }
}

然后通过startService(Intent i)启动服务,Intent 会携带相关数据,onHandleIntent会对这些数据进行识别,。

//同一服务只会开启一个worker thread,在onHandleIntent函数里依次处理intent请求。

        Intent i = new Intent("cn.scu.finch");  
        Bundle bundle = new Bundle();  
        bundle.putString("taskName", "task1");  
        i.putExtras(bundle);  
        startService(i);  
4.3.2 执行过程

onCreate--->onStartCommand-->onHandleIntent-->onDestroy

  • intentservice的oncreate方法中会通过HandlerThread 开启新线程,创建对应的Looper,和MessageQueue。

  • 通过onStartCommand()传递给服务intent被依次插入到工作队列中。工作队列又把intent逐个发送给onHandleIntent()。

  • 通过在onHandleIntent((Intent)msg.obj)中调用你的处理程序.

  • 处理完后会自动停止自己的服务Ondestroy

4.3.3 注意事项
  • IntentService 会创建一个线程,来处理所有传给onStartCommand()的Intent请求。
  • 对于startService()请求执行onHandleIntent()中的耗时任务,会生成一个消息队列,每次只有一个Intent传入onHandleIntent()方法并执行。也就是同一时间只会有一个耗时任务被执行,其他的请求还要在后面排队, onHandleIntent()方法不会多线程并发执行。
  • 当所有startService()请求被执行完成后,IntentService 会自动销毁,所以不需要自己写stopSelf()或stopService()来销毁服务。
  • 提供默认的onBind()实现 ,即返回null,不适合绑定的 Service。采用StartService来启动,即使启动它的Activity被销毁也不会影响service的生命周期
  • 提供默认的 onStartCommand() 实现,将intent传入等待队列中,然后到onHandleIntent()的实现。
  • Service等四大组件默认是运行在主线程上的!没有界面不代表不再UI线程。

4.4. HandlerThread

HandlerThread是Thread的子类,可以创建一个带有looper的线程,并创建与这个looper绑定的Handler,代码如下

HandlerThread mThread = new HandlerThread("message");
mThread.start();
Handler mHandler = new Handler(mThread.getLooper())
        {
            @Override
            public void handleMessage(Message msg)
            {
//   to do sth     
            }
        };

可以看出,跟Handler非常类似,只是新建HandlerThread时,与该线程绑定的looper也被创建了,所以不再需要我们自己去调用Looper.prepare(),Loop.loop()。直接通过thread.getLooper()就可以获取到当前线程的looper,类似于根据looper获取MesssageQueue的方法:Looper.MyQueue()。

5. 总结

由于Android系统的单线程模式,线程之间的通信必不可少,必须掌握Handler,AsyncTask, IntentService, HandlerThread的工作原理并加以熟练使用。其中Handler跟HandlerThread工作原理是一样的,很多情况下使用HandlerThread可以简化很多工作。

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

推荐阅读更多精彩内容