共计 10740 个字符,预计需要花费 27 分钟才能阅读完成。
**segmentfault 对 mackdown 语法的反对不是很好,有些图片都显示不进去,大家能够去我的掘金查看这篇文章。
一、Android 中的 UI 线程概述
<font face=” 黑体 ”>Android 的 UI 线程是线程不平安的,也就是说想要更新应用程序中的 UI 元素,则必须在主线程中进行。所以主线程又叫做 UI 线程。若在子线程中更新 UI 程序会报错。然而咱们常常有这样一种需要:须要在子线程中实现一些耗时工作后依据工作执行后果来更新相应的 UI。这就须要子线程在执行完耗时工作后向主线程发送音讯,主线程来更新 UI。也就是线程之间的通信,线程间通信办法有很多,明天咱们次要来讲利用 Handler 来实现线程之间的通信。
二、罕用类
1、Handler
<font face=” 黑体 ”>Handler 是 Android 音讯机制的下层接口,通过 handler,能够将一个工作切换到 handler 所在的线程中执行,咱们通常应用 handler 来更新 UI,但更新 UI 仅仅是的应用场景之一,handler 并不是仅仅用来更新 UI 的。
1)、在子线程发送音讯去主线程更新 UI
<font face=” 黑体 ”> 咱们都晓得 Android 中的网络操作是要在子线程中进行的,当咱们申请到网络数据当前,必定须要展现数据到界面上。咱们这里的网络申请就以 HttpURLConnection 那篇博文中的网络申请为例子。
<font face=” 黑体 ”> 实现成果如下所示:
<font face=” 黑体 ”> 具体步骤如下:
<font face=” 黑体 ”>1、创立 Handler
// 1: 实例化
// 2: 在子线程中发送 (空) 音讯
// 3: 由 Handler 对象接管音讯, 并解决
// 只有 Handler 发消息了,必然会触发该办法,并且会传入一个 Message 对象
@SuppressLint("HandlerLeak")
// import android.os.Handler; 留神是 os 包下的 Handler
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {super.handleMessage(msg);
}
}
};
<font face=” 黑体 ”>2、网络申请到数据后发送 (空) 音讯
new Thread() {
@Override
public void run() {super.run();
strMsg = get();
Log.e("MainActivityTAG", strMsg + "==========");
// 发空音讯
mHandler.sendEmptyMessage(1001);
}
}.start();
<font face=” 黑体 ” color = red> 留神:这里的 get() 申请源码就是 HttpURLConnection 外面的 get() 申请。
<font face=” 黑体 ”>3、Handler 中接管音讯并解决
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {super.handleMessage(msg);
switch (msg.what) {
case 1001:
txt.setText(strMsg);
break;
}
}
}
};
<font face=” 黑体 ”> 这里的 1001 就是 <font color=red> mHandler.sendEmptyMessage(1001) </font> 外面发送过去的那个 1001。
2、Message
<font face=” 黑体 ”> Message 是在线程之间传递的音讯,它能够在外部携带大量的信息,用于在不同线程之间替换数据。罕用的属性有:
- <font face=” 黑体 ”>what 属性 <font face=” 黑体 ”> 用于辨别 Handler 发送音讯的不同线程起源
- <font face=” 黑体 ”>arg1 属性 <font face=” 黑体 ”> 子线程向主线程传递的整型数据
- <font face=” 黑体 ”>obj 属性 <font face=” 黑体 ”>Object
1)、在子线程发送 Message 音讯去主线程
<font face=” 黑体 ”> 咱们能够在 Message 对象中装载一些数据,携带在音讯中,发送给须要接管音讯的中央。
<font face=” 黑体 ”> 实现成果如下所示:
<font face=” 黑体 ”> 发送音讯具体代码如下所示:
new Thread() {
@Override
public void run() {super.run();
// what 用于辨别 Handler 发送音讯的不同线程起源
// arg1, arg2 如果子线程须要向主线程传递整型数据, 则可用这些参数
// obj Object
Message msg = new Message();
msg.what = 1002;
msg.arg1 = 666;
msg.arg2 = 2333;
msg.obj = new Random(); // 这里只是为了演示 msg 能够发送对象信息
mHandler.sendMessage(msg);
}
}.start();
<font face=” 黑体 ”> 接管音讯具体代码如下所示:
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {super.handleMessage(msg);
switch (msg.what) {
case 1001:
txt.setText(strMsg);
break;
case 1002:
String str2 = "发送过去的 Message 数据为:" +"what:" + msg.what + "arg1:" + msg.arg1 + "arg2:" + msg.arg2 +
", 随机数" + ((Random) msg.obj).nextInt();
Toast.makeText(MainActivity.this, str2, Toast.LENGTH_LONG).show();
break;
}
}
};
3、MessageQueue
<font face=” 黑体 ”>MessageQueue 就是音讯队列的意思,它次要用于寄存所有通过 Handler 发送过去的音讯。这部分音讯会始终寄存于音讯队列当中,期待被解决。每个线程只会有一个 MessageQueue 对象。咱们下面的看到的 handleMessage() 办法其实就是从这个 MessageQueue 外面把办法提取进去去解决。咱们在开发的过程中是无奈间接的接触 MessageQueue 的,然而它确始终在起作用。
<font face=” 黑体 ”> 问题:为什么 Handler 发送过去的 Message 不能间接在 handleMessage() 办法中解决。而是要先寄存到 MessageQueue 外面?
<font face=” 黑体 ”> 答:其实起因就是同一个线程一下只能解决一个音讯,并不具备并发的性能。所以就先通过队列将所有的音讯都保留下来并安顿每个音讯的解决程序(队列的特点:先进先出),而后咱们的 UI 线程在挨个将它们拿进去解决。
4、Looper
<font face=” 黑体 ”>Looper 是每个线程中 MessageQueue 的管家,调用 Looper 的 loop() 办法,就会进入到一个有限循环当中,而后每当 MessageQueue 中存在一条音讯,Looper 就会将这条音讯取出,并将它传递到 Handler 的 handleMessage() 办法中。每个线程只有一个 Looper 对象,而且仅对应一个音讯队列。那么 Looper 到底是怎么工作的,接下来咱们就要通过代码来实现一下 Looper 的用法。
<font face=” 黑体 ”> 下面咱们都是在子线程中发送音讯到主线程去解决,那么反过来能够吗?就是在主线程发送音讯到子线程去解决,答案是能够的。咱们用代码来实现一下,具体步骤如下:
<font face=” 黑体 ”>1、首先咱们须要在子线程中定义一个 Handler 来接管音讯
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
private Handler mHandler2;
new Thread() {@SuppressLint("HandlerLeak")
@Override
public void run() {super.run();
mHandler2 = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {super.handleMessage(msg);
Log.e("MainActivityTAG", "由主线程传递过去的 Message,它的 what 是:" + msg.what);
}
};
}
}.start();}
<font face=” 黑体 ”>2、而后由主线程发送音讯
case R.id.btn3:
mHandler2.sendEmptyMessage(10000);
break;
<font face=” 黑体 ”> 咱们当初来回顾一下这个流程,首先咱们点击按钮的时候调用 sendEmptyMessage() 办法,这个时候这个音讯就进入到 MessageQueue 外面,而后由 Looper 读出这一个音讯并交由 handleMessage() 这个办法来解决,所以 MessageQueue 和 Looper 都是在幕后工作的。
<font face=” 黑体 ”> 首先能够必定的是主线程肯定有本人的 Looper,那么子线程是否有它本人的 Looper 呢?要答复这个问题,咱们先来运行一下下面的代码,看看能不能胜利的实现由主线程向子线程发送音讯。
<font face=” 黑体 ”> 运行后果如下:
<font face=” 黑体 ”> 运行上述代码报了一个 <font color = red> “Can’t create handler inside thread Thread[Thread-6,5,main] that has not called Looper.prepare()”</font> 的谬误。意思就是不能再还没有调用 <font color = red>Looper.prepare() </font> 的线程外面去创立 Handler。那么为什么主线程中咱们没有调用这个办法不会报错呢?其实答案就是零碎会主动的为主线程创立 <font color = red>Looper.prepare() </font> 这个办法。那么咱们就手动的为子线程创立一下这个办法。
<font face=” 黑体 ”> 增加上 <font color = red> Looper.prepare() </font> 这个办法之后,咱们上述的代码就不会报错了。然而这个时候咱们还是无奈在子线程接管到音讯。起因就是在子线程中咱们不仅要手动加上 <font color = red> Looper.prepare() </font> 这个办法,还要加上 <font color = red> Looper.loop() </font> 这个办法。
<font face=” 黑体 ”><font color = red> Looper.prepare() </font> 这个办法的意思是筹备开始一个循环,而 <font color = red> Looper.loop() </font> 这个办法才是真正的循环,作用就是使得 handleMessage() 始终处于期待状态,不会被完结掉。
<font face=” 黑体 ”> 咱们来看下代码:
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
private Handler mHandler2;
new Thread() {@SuppressLint("HandlerLeak")
@Override
public void run() {super.run();
Looper.prepare(); // 筹备, 开启一个音讯循环. 零碎会主动为主线程开启音讯循环
mHandler2 = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {super.handleMessage(msg);
Log.e("MainActivityTAG", "由主线程传递过去的 Message,它的 what 是:" + msg.what);
}
};
Looper.loop(); // 循环. 相当于产生了一个 while(true){...}
}
}.start();}
<font face=” 黑体 ”> 当初咱们来看一下运行后果:
当初咱们就曾经胜利的实现了由主线程发送音讯到子线程。
三、运行机制
<font face=” 黑体 ”> 以上呢咱们曾经将 Handler 机制外面所波及的四个类都讲完了。当初咱们来聊一下它的运行机制。
<font face=” 黑体 ”> 上面是我从网上找到的一张 Handler 运行机制图片,咱们就看着这张图片再来讲一下 Handler 的运行机制。
<font face=” 黑体 ”> 首先咱们来看下右边这一部分,一个 LooperThread 线程,这个线程外面有一个 Looper,而后 Looper 外部有 MessageQueue 音讯队列,这三者是一一对应的。
<font face=” 黑体 ”> 只有主线程才会主动的由 Looper 来治理,而其余线程的话必须要显示的调用 Looper.prepare() 这个办法。同时如果心愿子线程处于继续接管音讯的状态,咱们还须要调用 Looper.loop() 办法使其处于期待的状态。
<font face=” 黑体 ”> 咱们创立的 Handler 办法在发送音讯的时候其实是将音讯发送到 MessageQueue 音讯队列中。咱们看左边 MessageQueue 中有多待处理的音讯。而后通过 Looper 一直的取出音讯到对应的 Handler 的 handleMessage() 办法中去解决。这就是整个 Handler 的运行机制。
四、利用 Handler 实现计时器案例
<font face=” 黑体 ”> 先来看一下成果,如下图所示:
<font face=” 黑体 ”> 这个计时器案例次要性能就是按下播放按键的时候,计时器就开始计时工作,按下暂停键计时器就进行工作,并显示以后用时。首先计时性能必定是在子线程实现的。而计时性能必定须要扭转界面的 UI,所以这里咱们就利用 Handler 来将子线程的信息发送到 UI 线程。
<font face=” 黑体 ”> 实现步骤如下所示:
-
<font face=” 黑体 ”> 在主线程创立 Handler,并覆写 handleMessage() 办法。
@SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { @Override public void handleMessage(@NonNull Message msg) {super.handleMessage(msg); } };
-
<font face=” 黑体 ”> 每隔 1s,工夫 ++,并且发送音讯到主线程更新 UI,咱们这里先用 Thread.sleep() 这个办法来实现每隔 1s,一会咱们回去优化这个代码。
new Thread() { @Override public void run() {super.run(); int i = 1; while (flag) { try {sleep(1000); } catch (InterruptedException e) {e.printStackTrace(); } Message msg = new Message(); msg.arg1 = i; mHandler.sendMessage(msg); // 工夫 ++ i++; } } }.start();
-
<font face=” 黑体 ”> 在主线程中用 00:00 的模式来显示计时工夫。
@SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { @Override public void handleMessage(@NonNull Message msg) {super.handleMessage(msg); int min = msg.arg1 / 60; int sec = msg.arg1 % 60; // 00:00 String time = (min < 10 ? "0" + min : ""+ min) +":"+ (sec < 10 ?"0"+ sec :"" + sec); timer.setText(time); if (!flag) {title.setText("计时器"); state.setImageResource(R.mipmap.start); txt.setText("用时:" + time); } } };
<font face=” 黑体 ”> 残缺代码如下所示:
public class TimerActivity extends AppCompatActivity {
private TextView title, timer, txt;
private ImageView state;
private boolean flag = false; // 1: 用于区别以后对按钮的点击是属于开启计时器还是进行计时器 2: 管制 while 循环
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {super.handleMessage(msg);
int min = msg.arg1 / 60;
int sec = msg.arg1 % 60;
// 00:00
String time = (min < 10 ? "0" + min : ""+ min) +":"+ (sec < 10 ?"0"+ sec :"" + sec);
timer.setText(time);
if (!flag) {title.setText("计时器");
state.setImageResource(R.mipmap.start);
txt.setText("用时:" + time);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_timer);
title = findViewById(R.id.title);
timer = findViewById(R.id.timer);
txt = findViewById(R.id.txt);
state = findViewById(R.id.state);
state.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {if (!flag) {
// 原本是禁止状态, 行将开始计时
flag = true;
title.setText("工作中");
state.setImageResource(R.mipmap.stop);
txt.setText("");
new Thread() {
@Override
public void run() {super.run();
int i = 1;
while (flag) {
try {sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
Message msg = new Message();
msg.arg1 = i;
mHandler.sendMessage(msg);
// 工夫 ++
i++;
}
}
}.start();} else {flag = false;}
}
});
}
}
<font face=” 黑体 ” color = red> 留神:下面 flag 标记位有两个作用:
- <font face=” 黑体 ”> 用于区别以后对按钮的点击是属于开启计时器还是进行计时器;
- <font face=” 黑体 ”> 管制 while 循环。
五、利用 Handler 的 postDelayed 计划优化计时器案例
<font face=” 黑体 ”> 其实这个优化计划就是用 Handler.postDelayed() 的计划来代替原来的 Thread.sleep() 办法。
<font face=” 黑体 ”> 咱们间接来看残缺的代码:
public class TimerActivity2 extends AppCompatActivity {
private TextView title, timer, txt;
private ImageView state;
private boolean flag = false;
private String time;
private int i;
private Runnable runnable = new Runnable() {
@Override
public void run() {
int min = i / 60;
int sec = i % 60;
time = (min < 10 ? "0" + min : ""+ min) +":"+ (sec < 10 ?"0"+ sec :"" + sec);
timer.setText(time);
i++;
mHandler.postDelayed(runnable, 1000);
}
};
// post postDelay postAtTime
private Handler mHandler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_timer);
title = findViewById(R.id.title);
timer = findViewById(R.id.timer);
txt = findViewById(R.id.txt);
state = findViewById(R.id.state);
state.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {if (!flag) {
flag = true;
title.setText("工作中");
state.setImageResource(R.mipmap.stop);
txt.setText("");
i = 1;
new Thread() {
@Override
public void run() {super.run();
mHandler.postDelayed(runnable, 1000);
}
}.start();} else {
flag = false;
title.setText("计时器");
state.setImageResource(R.mipmap.start);
txt.setText("用时:" + time);
mHandler.removeCallbacks(runnable);
}
}
});
}
}
<font face=” 黑体 ”> 下面代码外面其实是利用了递归的思维实现计时原理的,因为咱们在外层的 Runnable 对象中又递归调用了 runnable。所以当咱们进行计时性能时,须要移除回调操作,就是将咱们的 runnable 对象移除掉,须要调用 mHandler.removeCallbacks(runnable)。
六、Handler 内存溢出问题
<font face=” 黑体 ”> 能够看到下面咱们在用 handler 的时候都加了一个注解 <font color=red> @SuppressLint(“HandlerLeak”)。</font> 其实起因就是 Handler 在 Android 中用于音讯的发送与异步解决时,经常在 Activity 中作为一个匿名外部类来定义,此时 Handler 会隐式地持有一个外部类对象(通常是一个 Activity)的 <font color=red>强援用</font>。当 Activity 曾经被用户敞开时,因为 Handler 还持有 Activity 的援用造成 Activity 无奈被 GC 回收,从而造成内存泄露。解决办法个别有两种;
- <font face=” 黑体 ”>被动的革除所有的 Message,当 Activity 销毁的时候咱们调用 mHandler.removeCallbacksAndMessages(null) 这个办法;
-
<font face=” 黑体 ”>利用弱援用来解决这个问题,定义一个 MyHandler 的动态外部类(此时不会持有外部类对象的援用),在构造方法中传入 Activity 并对 Activity 对象减少一个弱援用,这样 Activity 被用户敞开之后,即使异步音讯还未处理完毕,Activity 也可能被 GC 回收,从而防止了内存泄露。写法如下所示:
private MyHandler handler = new MyHandler(this); static class MyHandler extends Handler { WeakReference weakReference; public MyHandler(SecondActivity activity) {weakReference = new WeakReference(activity); } @Override public void handleMessage(Message msg) {}}
七、小结
<font face=” 黑体 ”> 到此为止跟 Handler 无关的问题咱们大抵上都曾经讲完了,并且也实现了一个小案例。咱们讲了跟 Handler 无关的几个罕用类,而后依据这些罕用类了解了 Handler 的运行机制,又利用 Handler 实现了一个计时器,并优化了代码。最初咱们又简略的说了一下 Handler 可能会造成内存透露的问题。
<font face=” 黑体 ”> 我的项目源码下载地址。