Android学习笔记 -- UI(主)线程与消息循环

November . 18 . 2018

Android主线程简介:

当Android应用程序启动后,系统会创建一个叫做“main”的线程。它就是主线程,也叫UI线程,非常重要。在Android系统中,主线程主要负责执行四大组件的执行。负责分发事件给构建,包括绘制事件。

Android中规定访问UI只能在主线程进行,如果在子线程中访问UI,那么程序就会抛出异常。

那么为什么安卓系统不允许在子线程中访问UI呢?这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态。如果在UI控件的访问加上锁机制的话,由于锁机制会阻塞线程的执行, 会降低UI运行效率。

主线程的主要责任:

  1. 快速的处理UI事件。Android希望UI线程能快速响应用户操作,如果UI线程花太多时间处理后台的工作,会让用户有非常糟糕的体验。当UI事件发生时,让用户等待时间超过5秒而未处理,Android系统就会给用户显示APP未响应提示信息。
  2. 快速的处理Broadcast消息。在BroadcastReceiver的onReceive()函数中,不宜占用太长的时间,否则会导致主线程无法处理其它的Broadcast消息或UI事件。如果占用时间超过10秒, Android系统就会给用户显示APP未响应提示信息。


为了更清楚的讲述UI线程的特点, 先提出一个贯穿全文的需求, 需求的大致要求是:

实现一个倒计时器, 获取用户输入的倒计时数, 点击开始按钮开始计时, 计时完毕显示完成.

需求十分的简单, 功能就是一个计时, 心中瞬间就有了思路, 不就是设置一个变量接收用户输入, 然后循环改变界面的输出嘛(年轻的我), 思路有了那就开始撸码吧.

附上源码(错误示范):

UI

222.PNG

页面逻辑

package example.senior0602;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity
{
    final String TAG = "测试";
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }
    // 点击'开始倒计时'
    public void startCount( View view)
    {
        long timeStart = System.currentTimeMillis();

        TextView textView = (TextView)findViewById(R.id.id_display);
        int number = 4;
        while (number > 0)
        {
            String str = String.valueOf(number);
            textView.setText(str);
            number --;
            Log.w(TAG, "倒计时: " + str);

            // 等1秒
            try{
                Thread.sleep(1000);
            }catch (Exception e){}
        }
        textView.setText("完成");
        Log.w(TAG, "倒计时结束");

        long timeEnd = System.currentTimeMillis();
        Log.w(TAG, "耗时: " + (timeEnd- timeStart) + " 毫秒");
    }
}

完成了, 看起来没有什么毛病, 编译运行

111.PNG

我靠, 怎么直接出现了完成... 我的倒计时效果呢?? 这算什么嘛.. 查看后台输出后发现, setText() 确确实实被调用了五次, 那么就说明倒计时的逻辑没有错, 但是UI怎么不会按照代码执行来改变呢, 经过查阅文档后了解到原来UI的改变并不是实时的, 中间还有一层 消息循环做中间层, 简单的说就是只有按钮的监听回调结束后, TextView 才会更新显示.

直接的文字可能会难以理解,下面用伪代码描述一下这个线程做了什么:

 void run() 
{ 
     msgQueue: 消息队列 
     while(true) 
     { 
         msg: 从msgQueue取得一个消息 
         switch(msg.type) 
         { 
             case 点击了Button: 
                  调用startCount(); 
                  break; 
 
             case 重新绘制TextView: 
                  重新绘制TextView; 
                  break; 
  } 
          ... 
     } 
}

显然,只有在退出startCount()之后,才有机会重绘TextView。那么TextView的setText()做了什么呢?

class TextView 
{ 
    void setText (String text ) 
    { 
        // 保存text的值 
        // 请求重绘: 内部发一个msg到msgQueue 
    } 
} 

也就是说,TextView只是保存了一个新的text,创建了一个要求绘制的消息 .

了解了UI更新机制后我们来重构我们的代码.

package example.countdown;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    final String TAG = "测试: ";
    MyHandler handler = new MyHandler();

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

    // 点击事件, UI线程应该尽快完成,
    // 耗时超过0.3s界面将会出现卡顿,
    // 耗时的操作应该分发到其它线程处理
    public void StartCount(View view) {
        MyCountThread myTask = new MyCountThread();
        myTask.start();
    }

    // 工作线程
    private class MyCountThread extends Thread {
        @Override
        public void run() {

            EditText editText = (EditText) findViewById(R.id.id_editText_count);

            int number = Integer.valueOf(editText.getText().toString());

            while (number > 0) {
                String str = String.valueOf(number);
                Log.w(TAG, "倒计时: " + str);

                // 更新 UI 显示
                Message msgCount = new Message();
                msgCount.what = 1;
                msgCount.arg1 = number;
                if (number == 1) {
                    msgCount.obj = "完成";
                }
                handler.sendMessage(msgCount);

                number--;

                try {
                    Thread.sleep(1000);
                }catch (Exception e ) { }
            }

            // 倒计时结束
            Message msgFinish = new Message();
            msgFinish.what = 2;
            msgFinish.obj = "完成!";
            handler.sendMessage(msgFinish);

            Log.w(TAG, "倒计时结束");
        }
    }

    // 消息处理器, 用来更新界面
    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg)
        {
            TextView textView = (TextView) findViewById(R.id.id_textView_count);
            switch (msg.what) {
                case 1:
                    int number = msg.arg1;
                    textView.setText(String.valueOf(number));
                    break;
                case 2:
                    String value = (String) msg.obj;
                    textView.setText(value);
                    break;
            }
        }
    }
}

计时的逻辑还是利用循环, 不过循环中并没有更新页面的代码, 而是利用一个 MyHandler 对象给UI线程发送了一个消息, MyHandler 继承了 Handler他是一个UI线程的消息队列, 每循环一次就会有一个消息加入到队列中, 这样界面卡死的问题就解决啦.