Handler

Handler,字面理解叫做操作者,实际上是一个可以管理线程、实现线程之间通讯的工具。Handler的使用解决了两个问题:

  • 调度Android系统在某个时间点执行特定的任务
  • 将需要执行的任务加入到用户创建线程的任务队列中去

由于一个Handler发送消息的特性,可以借助它利用消息队列的机制,实现线程之间的消息交换,非常实用,可以说有子线程的地方就有Handler。

Handler有两种使用方式,即发送Message消息和运行Runable任务。事实上运行Runable的内部实现也是进一步调用了发送消息到消息队列中,所以本质上没有区别。

一个Handler对象的作用是:往创建它的线程的消息队列中添加消息,并借助Looper对消息队列中的消息进行处理。也就是说,一个Handler一旦创建起来,就和创建它的线程绑定,即使在其他线程中发送消息,也会发送到创建它线程的消息队列中,就实现了线程之间的通信。另外,Looper的存在是创建Handler的先决条件,如果一个线程中没有事先准备好Looper,就不能在这个线程中创建Handler。而主线程中系统会提前创建好Looper,所以主线程的Handler不必担心这个;但是一旦要想在自己的子线程中创建Handler,就一定要先准备Looper。

在创建Handler的时候通常会重写的一个方法就是handleMessage,也就是处理、消费消息队列中的消息的方法,或者说接受到消息后要做的事情。

在使用创建好的Handler对象发送消息时,有以下这些方法可以调用:

// 立即发送消息
public final boolean sendMessage(Message msg)
public final boolean post(Runnable r);

// 延时发送消息
public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean postDelayed(Runnable r, long delayMillis);

// 定时发送消息
public boolean sendMessageAtTime(Message msg, long uptimeMillis);
public final boolean postAtTime(Runnable r, long uptimeMillis);
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis);

// 取消消息。可以将延时的消息提前取消掉防止生效。
public final void removeCallbacks(Runnable r);
public final void removeMessages(int what);
public final void removeCallbacksAndMessages(Object token);

使用post相关的方法可以方便地执行任务,而使用sendMessage系列方法可以方便地发送消息,使用的时候可以随机应变,但是内部的实现本质相同。

Message对象封装了一个消息的内容,其中值得关心的是what属性和obj属性。what是一个int,可以协助区分不同的消息,采取不同的处理措施;而obj是一个Object对象,是消息携带的内容。例如:

Handler mHandler = new Handler() {

    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
        // 根据msg.what来区分消息种类,采取不同的行动
        switch (msg.what) {
            case MSG_START_DOWNLOAD:
                mText.setText("开始下载");
                break;
            case MSG_DOWNLOAD_SUCCESS:
                mText.setText("下载成功");
                break;
            case MSG_DOWNLOAD_FAIL:
                mText.setText("下载失败");
                break;
        }
    }
};

如果在子线程(自定义的线程)中创建和使用Handler,则需要遵循一套先准备Looper的顺序:

//声明Handler;
Handler handler;
new Thread(new Runnable(){
    @Override
    public void run(){
        //创建当前线程的Looper
        Looper.prepare();
        //在子线程创建handler对象
        handler=new Handler(){
            @Override
            public void handleMessage(Message msg){
                //这里是消息处理,它是运行在子线程的
            }
        };
        //开启Looper的消息轮询
        Looper.loop();
    }
}).start();

    mBanner.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View v){
        //在主线程发送一个消息到子线程
        Message msg=new Message();
        handler.sendMessage(msg);
    }
});

当我们在子线程使用Handler时,如果Handler不再需要发送和处理消息,那么一定要退出子线程的消息轮询。因为Looper.loop()方法里是一个死循环,如果我们不主动结束它,那么它就会一直运行,子线程也会一直运行而不会结束。使用Looper.myLooper().quit();Looper.myLooper().quitSafely();来结束Looper的轮询。

另外,不要new Message,而是使用Message.obtain()来获取消息进行定义,这样可以利用起来Message的缓存机制,提高效率。

多线程

Android中,UI的更新只能在主线程中进行,如果放在子线程中可能会出问题,不允许这样做。因此主线程也叫做UI线程。一些耗时的操作也应该放在子线程中异步执行,避免阻塞主线程导致程序卡死。下面介绍一些多线程的基础和进阶组件的使用方法。

Thread

来自Java,多线程的根基。直接拿来使用可以完成一些简单的任务。只需要重写run方法就可以给线程布置好任务,然后调用start方法可以让线程运行起来。使用比较简单,参考下图:

Thread

ThreadPool

提供了一个对线程集中管理、复用的地方。由于创建线程的代价比较大,如果能够事先准备一些线程,然后在需要的时候直接把任务丢入池子让线程池帮忙分配任务,可以最好地利用每一个线程,避免浪费。

通常,在单个任务处理时间比较短且任务数量很大的时候会使用线程池,避免频繁创建和销毁线程,比如网络请求和数据库读写等操作。定时任务可以使用定时线程池,而特定任务也可以使用单一线程池,如日志写入。

AsyncTask

如果使用Thread+Handler的处理机制,模板代码较多,代码臃肿。因此Google提供了一个工具类AsyncTask,帮助我们更好地封装了异步多线程的操作,使用起来更加方便。

通过继承AsyncTask<Params, Progress, Result>,使用以下核心方法定制自己的异步任务:

  1. onPreExecute 执行前的动作。这个方法会在UI线程中执行
  2. doInBackground 后台执行的任务。这个方法在子线程中进行。
  3. publishProgress 后台通知前台任务的进度。这个方法在子线程中执行。不需要重写,在子线程中调用即可。
  4. onProgressUpdate 接受后台的任务进度通知并更新UI。在UI线程中进行。
  5. onPostExecute 执行完成后的动作。在UI线程中执行。

请看一个后台下载任务的例子:

package com.bytedance.clockapplication;

import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.widget.TextView;

@SuppressLint("CI_StaticFieldLeak")
public class DownloadTask extends AsyncTask<String, Integer, String> {
    TextView mTextView;

    DownloadTask(TextView textView) {
        mTextView = textView;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mTextView.setText("开始下载....");
    }

    @Override
    protected String doInBackground(String... strings) {
        String url = strings[0];
        try {
            return downlaod(url);
        } catch (Exception e) {
            return "Fail";
        }
    }

    private String downlaod(String url) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            publishProgress(i);
            Thread.sleep(50);
        }
        return "Success";

    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
        mTextView.setText("下载进度:" + values[0]);
    }

    @Override
    protected void onPostExecute(String s) {
        super.onPostExecute(s);
        mTextView.setText("下载完成:" + s);
    }
}

这里AsyncTask<String, Integer, String>的三个类型分别对应执行时传入的参数Params、过程中通知时采用的消息类型和执行结果的类型。

可以通过如下的方式进行使用:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mText = findViewById(R.id.text);
    // 执行时传入参数Params
    new DownloadTask(mText).execute("http://www.xxx.mp4");
}

HandlerThread

顾名思义,本质是封装了Handler的一个Thread子类,也会自动创建Looper。重写核心方法handleMessage可以处理自己消息队列中的Message,做出响应。

下面是一个股票交易App的定时查询价格的例子:

HandlerThread

IntentService

Service可以运行在后台提供服务而不提供用户界面,就像Linux中的守护程序。IntentService在使用的时候,可以通过传入一个Intent在后台启动一个服务,自动创建子线程执行一定的任务。IntentService封装良好,但是要知道,所有传入同一个IntentService对象的任务是串行执行的,也就是执行完了前一个才处理后一个。IntentService会在最后一项任务执行完之后自动销毁。

通常的应用是,在后台播放歌曲、下载文件等。例如:

package com.bytedance.clockapplication;

import android.app.IntentService;
import android.content.Intent;
import android.support.annotation.Nullable;

public class DownloadService extends IntentService {
    // 必须有一个匹配父类的构造方法
    public DownloadService(String name) {
        super(name);
    }

    //处理意图,完成任务
    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        if (intent!=null){
            try{
                String url=intent.getStringExtra("url");
                // download file
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

在主线程中可以如下方式使用:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent intent = new Intent(this,DownloadService.class);
        intent.putExtra("url","http://...");
        startService(intent);

        Intent intent2 = new Intent(this,DownloadService.class);
        intent2.putExtra("url","http://.../2");
        startService(intent2);
    }
}

请注意,主线程中并不会被阻塞,而是直接把两个intent都交给了DownloadService,如何完成两个任务是DownloadService自己的事了,而IntentService会串行执行,所以DownloadService只能一个完成后在处理另一个,在两个都处理完成后自动销毁。

总结

工具 描述
Thread 多线程的基础
ThreadPool 对线程进行更好的管理
AsyncTask Android中为了简化多线程的使用, 而设计的默认封装
HandlerThread 开启一个线程,就可以处理多个耗时任务
IntentService Android中无界面异步操作的默认实现

自定义View

比如要做一个时钟的应用程序,指针这种View组件是安卓没有自带的,需要我们自己定义出来。这个时候就需要自定义View了。

View绘制的三个步骤分别是先测量宽高,然后确定位置,最后绘制形状。自定义View需要继承View类,通常只需要重写第三步的onDraw(Canvas canvas)方法,自定义形状就可以了。

绘制形状就是在Canvas画布上用Paint画笔作图,先设置好画笔的颜色、粗细、风格等属性,然后把画笔对象当做参数传给Canvas的许多作画方法就可以完成作图了。这里不再赘述。

本节示例工程

https://github.com/jingjiecb/Chapter-4