安卓编程 多线程与Handler消息传递
介绍
在安卓编程中,许多时候都要使用到多线程,一般超过几毫秒的功能都应该放在后台线程去运行.
在此之前要先了解清楚线程与进程的概念.
进程: 每个APP都是一个进程
线程: 如微信APP中聊天界面,包含了许多线程
下图为微信聊天界面中的主线程与子线程
入门
多线程的创建有如下两种方式
两种方式都可以创建一个子线程,并且都要重写run方法,这个线程要做的事情都写在run方法里面,一但执行完run方法,线程就消亡了,有可能被Java内存回收,所以调用线程的时候最好使用start
方法而不是run
方法
这里要注意 run
方法和start
方法的区别:
- start: 另外开辟一个子线程执行任务
- run 在当前线程直接执行任务
为什么要使用Handler:
案例1: 显示下一句
在很多APP中,我们需要通过子线程来修改主UI,比如点击下一句按钮的时候修改TextView的内容
子线程直接操作主线程的错误案例
在安卓编程的线程安全性,子线程不允许直接修改主线程的内容
下面就通过代码来观察一下如果直接通过子线程修改主线程会发生什么
布局文件显示如下
为了实现点击按钮的时候使用新的线程来修改TextView的内容,在Button的点击事件中,我增加了如下代码
new Thread(new Runnable() {
@Override
public void run() {
tv.setText("我中过最惊喜的彩券");
}
}).start();
下面就运行运行程序查看一下效果
可以看到一点击按钮程序就炸了
其中有报错如下
Only the original thread that created a view hierarchy can touch its views.
只有创建这个view的线程才能操作这个view
这就验证了之前的结论,子线程不能直接修改主线程中的内容,而想要达成目的,我们只能借助android提供的Handler消息机制
Handler消息机制实现功能
Handler相当于一个中转站,子线程点击按钮后先告诉Handler, 然后由Handler传递消息给主线程,修改其中的UI组件
- 每个Handler实例都与单个线程和该线程的消息队列相关联。当您创建一个新的Handler时,它被绑定到创建它的线程的线程/Messagequeue–从那时起,它将把消息和可运行的消息传递到消息队列中,并在消息队列中出现时执行它们
- Handler通过Message对象来传递信息
- 可以通过obtain方法获取到Handler中的Message
- what参数可以用来设置用户自定义的标识(下文第一个案例就使用了what参数)
setData(Bundle data)
更加灵活,通过Bundle对象可以设置更多类型的数据(下文第二个案例将使用Bundle)
下面会结合案例更加易懂的解释一下Handler的使用,上面的参数先了解一下即可
注意: 创建的时候一定要使用android自带的Handler包
修改一下我的代码,增加Handler来传递信息,观察一下结果
final Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
if (msg.what == 1) {
tv.setText("我中过最惊喜的彩券");
button.setText("");
}
return false;
}
});
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
handler.sendEmptyMessage(1);
}
}).start();
}
});
成功完成了子线程修改主UI的目的,没有任何报错
案例一解析
这里再解释一下Handler消息机制是如何传递消息的
- 首先创建一个Handler对象,由于这个对象创建在MainActicity中创建,因此它被绑定到了MainActivity所在的线程(主线程上)
- Handler的
Callback
回调函数相当于一个循环,当它监听到了有子线程执行了sendMessage
的操作时就会被触发,如上文的小案例
因为可以有很多个子线程会发送消息,我不知道TextView要更新文字属于哪个子线程的消息,所以要设置一个判断
本案例中我使用的是发送一个空消息,设置what
参数的方式来发送消息
这里的what
参数相当于一个标识符,使得接收方能正确的接收到消息
如下图,发送消息的线程中设置的 what=1与接收消息时候的what==1
相对于,起到了一个用户自定义标识符的作用
案例一代码
布局文件的代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="25sp"
android:gravity="center"
android:textColor="#000"
android:text="错误案例: 子线程直接修改主线程" />
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="在我遇见你以前\n也拥有过完整的睡眠"
android:layout_margin="50dp"
android:padding="20dp"
android:textSize="30sp"
android:textColor="#3F51B5">
</TextView>
<Button
android:id="@+id/button"
android:layout_width="200dp"
android:layout_gravity="center"
android:layout_height="wrap_content"
android:background="#2196F3"
android:textColor="#fff"
android:textSize="18sp"
android:text="下一句" />
</LinearLayout>
MainActivity的代码
package com.example.review41;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
Button button;
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
tv = findViewById(R.id.tv);
final Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
if (msg.what == 1) {
tv.setText("我中过最惊喜的彩券");
button.setText("");
}
return false;
}
});
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
handler.sendEmptyMessage(1);
}
}).start();
}
});
}
}
案例2 计时器
Bundle对象
Bundle主要用于传递数据;它保存的数据,是以key-value(键值对)的形式存在的。
因为Handler自身不能传递多样类型的数据,所以要借助Bundle来传递数据
整套的流程如下
- 创建Bundle
- 设置Bundle的数据
- 将Bundle 使用 setData的方法添加到Message对象中
- 使用handler的sendMessage方法发送数据
完成计时器同样需要多线程,这里我创建了一个类来继承Thread,注意初始化类的时候需要传入handler对象,才可以在正确的线程之间达到数据传输的效果
计时器的思路很简单,比之前的点击显示下一句多了个资源的互斥访问,当点击开始按钮的时候,启用循环不停的发送消息,完成秒数的增加和显示,而点击暂停按钮的时候停止秒数增加
这里就使用了一个AtomicBoolean
对象来起到互斥访问的作用
计时器的核心代码如下,这里的isRunning
是一个AtomicBoolean
类型的对象,相当于一个资源信号量,通过它可以完成互斥访问的效果,在初始化类的时候被创建,在run方法中始终为true,只有当其他线程,如stop按钮启用的线程将它设置为fasle的时候计时才会停止
public void run() {
isRunning.set(true);
while (isRunning.get()) {
try {
Thread.sleep(10);
clock += 0.01f;
Message message = handler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putFloat("time", clock);
message.setData(bundle);
handler.sendMessage(message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
super.run();
}
代码
package com.example.review41;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
Handler handler;
TextView textView;
Button btn_start, btn_stop;
ClockThred thred;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.tv);
btn_start = findViewById(R.id.btn_start);
btn_stop = findViewById(R.id.btn_stop);
handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
Bundle data = msg.getData();
Float clock = data.getFloat("time");
textView.setText(String.format("%.2f", clock));
return false;
}
});
btn_stop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thred.stopClock();
btn_stop.setEnabled(false);
btn_start.setEnabled(true);
}
});
btn_start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thred = new ClockThred(handler);
thred.start();
btn_stop.setEnabled(true);
btn_start.setEnabled(false);
}
});
}
}
package com.example.review41;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import java.util.concurrent.atomic.AtomicBoolean;
public class ClockThred extends Thread {
private Handler handler;
private Float clock = 0.00f;
private AtomicBoolean isRunning;
public ClockThred(Handler handler) {
this.handler = handler;
this.isRunning= new AtomicBoolean(false);
}
public void stopClock(){
this.isRunning.set(false);
}
@Override
public void run() {
isRunning.set(true);
while (isRunning.get()) {
try {
Thread.sleep(10);
clock += 0.01f;
Message message = handler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putFloat("time", clock);
message.setData(bundle);
handler.sendMessage(message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
super.run();
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="30sp"
android:gravity="center"
android:textColor="#000"
android:text="计时器" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Your Name And Id" />
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:text="0.00" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp">
<Button
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="开始" />
<Button
android:id="@+id/stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="暂停" />
</LinearLayout>
</LinearLayout>