我想组件刷新失败最常见的情况应该是做个计时器类似的东西,发现它不动。当然也不一定是计时器,总之就是类似于每过一段时间将组件刷新一下的功能,这时候经常会发现组件没有动。
我当时遇到这个问题的时候查阅了各种资料,把我学习的书基本全部翻了一遍都没找到原因。最后去一个程序员聚集的群里问了之后才知道是为什么。这个问题很容易犯,所以在这里记录一下,免得自己忘记。
情景描述
首先我们来做一个文本框刷新吧。假设现在要用文本框做一个简单的计时器,显示时间,做个一分钟倒计时,文本框显示当前剩余的秒数。
一般思路
最容易想到的应该是用线程,线程启动后,每睡眠一秒钟刷新一下文本框,让它显示时间的数字-1。
编码
先写一个文本框来显示时间(大致写一下,能看就行了)
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一个倒计时计时器:"
android:textSize="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/time"
android:text="60"
android:textSize="50dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginStart="30dp"
android:layout_marginLeft="30dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginLeft="15dp"
android:text="秒"
android:textSize="50dp"
app:layout_constraintStart_toEndOf="@+id/time"
app:layout_constraintTop_toTopOf="@+id/time" />
页面大概会是这个样子(有点丑,不过问题不大):
然后为它写一个子线程:
public class MainActivity extends AppCompatActivity {
private Runnable runnable;
private TextView time;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createRunnable();
Thread mainThread = new Thread(runnable);
mainThread.start();
}
//创建一个runnable
private void createRunnable() {
time = findViewById(R.id.time);
runnable = new Runnable() {
@Override
public void run() {
//循环60次
for (int i = 0;i < 60;i ++)
{
//每过1秒刷新一次文本框显示的时间
try {
Thread.sleep(1000);
time.setText(Integer.valueOf(time.getText().toString()) - 1 + "");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
}
}
一个一般思路下定时刷新文本框的页面就写好了
运行结果
程序看起来好像没有问题,但是拿去模拟器上运行之后我们会发现出现了这样的问题(如果是在手机上运行就会出现闪退):
当我们把time.setText(Integer.valueOf(time.getText().toString()) - 1 + “”);这条刷新文本框的语句注释掉后就不会出现问题了,但是这样明显不能实现功能。(注意setText()方法中,不是字符串变量的要加上+ “”,否则会因为变量类型问题出现“app has stopped”,即闪退)。
在网上查阅各种资料后很可能会发现找不到原因,这时候可能就会非常头疼,代码看起来明明没有错,可就是运行不了(我就是这样)。后来找了个Android的群,去里面问了大佬之后才知道,原来Android不能够在子线程中更新UI。
原因
Android中的UI控件是非线程安全的,因此在多线程中并发访问可能会导致UI控件进入不可预测的状态,而其他的一些解决这个问题的方法会使得UI控件变得复杂和低效,甚至可能会阻塞某些进程的运行,因此在Android中禁止直接在子线程中修改UI属性。
解决方法
使用Handler机制,在子线程中需要修改UI的地方发送一条消息(Message)给指定Handler,然后在Handler的消息处理方法中对UI进行修改,这样可以将修改UI的任务切换到Handler所在线程中来执行。
成功的代码
public class MainActivity extends AppCompatActivity {
private TextView time;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
time = findViewById(R.id.time);
Thread mainThread = new Thread(runnable);
mainThread.start();
}
Runnable runnable = new Runnable() {
@Override
public void run() {
//循环60次
for (int i = 0;i < 60;i ++)
{
//每过1秒刷新一次文本框显示的时间
try {
Thread.sleep(1000);
/*Message msg = refreshHandler.obtainMessage();
msg.what = 1;*/
refreshHandler.sendMessage(new Message()); //将消息发送给refreshHandler
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Handler refreshHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
//刷新显示时间的文本框
time.setText(Integer.valueOf(time.getText().toString()) - 1 + "");
}
};
}
这样就可以顺利运行了,这也是对Handler的一个简单的应用。