Claws Garden

Android基础04--复杂应用组件

Handler

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

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

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

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

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

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

 1// 立即发送消息
 2public final boolean sendMessage(Message msg)
 3public final boolean post(Runnable r);
 4
 5// 延时发送消息
 6public final boolean sendMessageDelayed(Message msg, long delayMillis)
 7public final boolean postDelayed(Runnable r, long delayMillis);
 8
 9// 定时发送消息
10public boolean sendMessageAtTime(Message msg, long uptimeMillis);
11public final boolean postAtTime(Runnable r, long uptimeMillis);
12public final boolean postAtTime(Runnable r, Object token, long uptimeMillis);
13
14// 取消消息。可以将延时的消息提前取消掉防止生效。
15public final void removeCallbacks(Runnable r);
16public final void removeMessages(int what);
17public final void removeCallbacksAndMessages(Object token);

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

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

 1Handler mHandler = new Handler() {
 2
 3    @Override
 4    public void handleMessage(@NonNull Message msg) {
 5        super.handleMessage(msg);
 6        // 根据msg.what来区分消息种类,采取不同的行动
 7        switch (msg.what) {
 8            case MSG_START_DOWNLOAD:
 9                mText.setText("开始下载");
10                break;
11            case MSG_DOWNLOAD_SUCCESS:
12                mText.setText("下载成功");
13                break;
14            case MSG_DOWNLOAD_FAIL:
15                mText.setText("下载失败");
16                break;
17        }
18    }
19};

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

 1//声明Handler;
 2Handler handler;
 3new Thread(new Runnable(){
 4    @Override
 5    public void run(){
 6        //创建当前线程的Looper
 7        Looper.prepare();
 8        //在子线程创建handler对象
 9        handler=new Handler(){
10            @Override
11            public void handleMessage(Message msg){
12                //这里是消息处理,它是运行在子线程的
13            }
14        };
15        //开启Looper的消息轮询
16        Looper.loop();
17    }
18}).start();
19
20    mBanner.setOnClickListener(new View.OnClickListener(){
21    @Override
22    public void onClick(View v){
23        //在主线程发送一个消息到子线程
24        Message msg=new Message();
25        handler.sendMessage(msg);
26    }
27});

**当我们在子线程使用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线程中执行。

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

 1package com.bytedance.clockapplication;
 2
 3import android.annotation.SuppressLint;
 4import android.os.AsyncTask;
 5import android.widget.TextView;
 6
 7@SuppressLint("CI_StaticFieldLeak")
 8public class DownloadTask extends AsyncTask<String, Integer, String> {
 9    TextView mTextView;
10
11    DownloadTask(TextView textView) {
12        mTextView = textView;
13    }
14
15    @Override
16    protected void onPreExecute() {
17        super.onPreExecute();
18        mTextView.setText("开始下载....");
19    }
20
21    @Override
22    protected String doInBackground(String... strings) {
23        String url = strings[0];
24        try {
25            return downlaod(url);
26        } catch (Exception e) {
27            return "Fail";
28        }
29    }
30
31    private String downlaod(String url) throws InterruptedException {
32        for (int i = 0; i < 100; i++) {
33            publishProgress(i);
34            Thread.sleep(50);
35        }
36        return "Success";
37
38    }
39
40    @Override
41    protected void onProgressUpdate(Integer... values) {
42        super.onProgressUpdate(values);
43        mTextView.setText("下载进度:" + values[0]);
44    }
45
46    @Override
47    protected void onPostExecute(String s) {
48        super.onPostExecute(s);
49        mTextView.setText("下载完成:" + s);
50    }
51}

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

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

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

HandlerThread

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

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

HandlerThread

IntentService

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

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

 1package com.bytedance.clockapplication;
 2
 3import android.app.IntentService;
 4import android.content.Intent;
 5import android.support.annotation.Nullable;
 6
 7public class DownloadService extends IntentService {
 8    // 必须有一个匹配父类的构造方法
 9    public DownloadService(String name) {
10        super(name);
11    }
12
13    //处理意图,完成任务
14    @Override
15    protected void onHandleIntent(@Nullable Intent intent) {
16        if (intent!=null){
17            try{
18                String url=intent.getStringExtra("url");
19                // download file
20            }catch (Exception e){
21                e.printStackTrace();
22            }
23        }
24    }
25}

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

 1public class MainActivity extends AppCompatActivity {
 2
 3    @Override
 4    protected void onCreate(Bundle savedInstanceState) {
 5        super.onCreate(savedInstanceState);
 6        setContentView(R.layout.activity_main);
 7
 8        Intent intent = new Intent(this,DownloadService.class);
 9        intent.putExtra("url","http://...");
10        startService(intent);
11
12        Intent intent2 = new Intent(this,DownloadService.class);
13        intent2.putExtra("url","http://.../2");
14        startService(intent2);
15    }
16}

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

总结

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

自定义View

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

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

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

本节示例工程

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

#Android