简述
线程是操作系统调度的基本单位,进程是操作系统进行资源分配和调度的基本单位,一个进程能够有包含多个线程,进程具有独立的内存空间,文件句柄,系统数据以及至少一个执行线程,不同的进程之间相互独立。
跨线程通信
新线程
如何去新建一个线程,很简单:
new Thread(() -> {});
是不是有点太短了哈哈哈,他的原本形式应该是:
new Thread(new Runnable() {
@Override
public void run() {}
});
这也是箭头函数的优势之一,可以减少我们的代码量,更方便阅读一点,那么,为什么我们需要额外的线程呢?原本运行的那个线程不够用吗?额外一个线程是做什么用的?
我们写好的每个应用程序打包安装到手机启动的时候,手机的系统都会为每个应用程序分配一个独立的进程,这个进程包含了应用程序的所有组件,包括我们android的四大组件,当然这里我不打算展开说app从点击到启动手机系统的过程,回到简述的一句话,每个进程至少有一个执行线程,关于这个线程,大家的文章可能出现两种叫法,一种叫主线程,一种叫做UI线程,其实都是说的同一个线程。
它负责什么事情呢,我们的触摸,点击,滑动都是在这个线程上被处理,但是除此之外,我们的app不仅仅是包括用户的事件,app本身的文件加载,网络请求,或者一些打点开软件后就一直执行的服务,如果单一的线程需要去处理那么那么多事情,肯定处理不过来或者哪个时候某件事情之间冲突了,线程也要讲究线权,不能啥活都让它干,于是一些比较繁琐的持续只需要得到结果的事情,外包出去,主线程干嘛呢,继续只需要干原本的接收用户的操作事件和更新ui这种操作,其它的不管,只要结果就好,那么这个时候我们就开始涉及到,多线程处理了。
(补充说明,关于上面加粗的文字:看到一个说法,如果多个线程同时更新同一个ui,那必然是线程不安全的,给所有的更新操作都上锁的话,那必然会导致性能下降。原文链接:详解Handler消息处理机制)
例子
假设我们现在的app有一个需求,我们要从网络提交一个请求,这里就不真的进行网络请求了,我们让这个新的线程延时3s来告诉我们这件事已经完成了。
new Thread(() -> {
try {
Thread.sleep(3000);
mBinding.jumpMain.setText("3s到了");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
然后我们把它放在一个点击事件里,等待3s后这个文字的变化,然后你就会发现,你app闪退了?!
打开Logcat查看报错消息:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
翻译过来的意思是:只有创建了视图层结构的原始线程才能触摸其视图。
很拗口,其实原因就是我们在新的这个线程并非主线程中试图修改UI,也就是setText这个方法,改文字也是改UI嘛,但是我们这个线程没有那个权利,Android要求UI控件的操作必须在主线程中执行,这也是它被叫做UI线程的原因。
所以现在就变成了,好了我的耗时操作做完了,但是我没法直接给你说,我要通过啥玩意才能跟你说我这活干完了,总不能钉钉打卡吧。
Handle
首先Handle是个什么东西呢,相当于消息的搬运工,把一个地方的消息搬运到另外一个地方,这里分别指的就是我们的子线程和主线程,我们来看看正常我们通过handle来传递一个消息是怎么样一个流程。
private Handler mhandler;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//在主线程中创建一个handle实例
mhandler = new Handler();
}
在子线程中调用Handle的post方法来向消息队列发送消息
new Thread(() -> {
try {
Thread.sleep(3000);
mHandle.post(new Runnable() {
@Override
public void run() {
mBinding.jumpThread.setText("嘿嘿嘿");
}
});
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
这就是我们比较常用到的一个传递消息到UI线程来修改某个控件的UI的写法。
当然,我们可以开始来分析下这个地方做了些什么事情,先是定义了一个全局变量mHandle,目的是让子线程内也能操作到这个对象,然后把实例化这个对象的操作放在主线程中,也就是我们正常写代码像是onCreate那些部分,相当于是让这个变量在主线程的内存中开辟一块空间,告诉它我其实是这里的人我有权限来操作这个线程,再然后在子线程需要修改UI的地方来调用这个主线程的内存空间。
其实做到这里的时候我发现它的逻辑似乎能够用接口的方法来实现,我也不知道其中是不是有什么联系因为源码我真的啃不太下来。我直接上代码:
private myHandle myHandleTest;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//其它代码
myHandleTest = new myHandle();
new Thread(() -> {
try {
Thread.sleep(3000);
myHandleTest.myPost(new myRunnable() {
@Override
public void myRun() {
mBinding.jumpThread.setText("啊啊啊修改成了");
}
});
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
public class myHandle{
private void myPost(myRunnable runnable){
runnable.myRun();
}
}
public interface myRunnable{
void myRun();
}
}
最终也是成功的修改了UI,也就是说前面的推测大抵没有偏离,就是借用定义在可以修改主线程的内存块让它去执行这些代码,而具体的丢入Message队列去一个个取这个过程并不是影响它能不能去修改的主要因素。
(ps:我也不知道我这么想会不会有错误,希望大佬们指点指正。)
对于上面说的这个问题之后,那么我们可以知道除非是有联动性的数据交互,比如说通过.what来标识这是个需要怎么处理的数据类型然后.obj来添加数据这些方法之外,我们可以在子线程内部单独去完成对主线程ui修改的这么一个过程,Handler其实它的构造方法还提供了一个:
public Handler(@NonNull Looper looper) {
this(looper, null, false);
}
通过传一个looper对象来创建一个Handler,这个方法可以让你在目标线程中创建一个Handler对象,那么根据上面所说的也就是可以完成子线程和子线程,子线程和主线程之间的一个通信而不需要翻到对应的代码模块中去实例化它,当然也只是少了这么一个步骤而已,大体的操作还是一样的,那我们要获取主线程怎么去获取,Looper对象提供了一个getMainLooper的方法,我们可以通过这个方法来传给Handler的构造函数,帮我们在线程之间搭起来一座桥。
new Thread(() -> {
try {
Thread.sleep(3000);
Handler handler = new Handler(Looper.getMainLooper());
handler.post(() -> mBinding.jumpThread.setText("这是我最后的波纹了JOJO"));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
总结
其实原本想分享对于跨线程通信的不止对Handler这个东西,想说说Rxjava等等太多太多了,但是技术能力有限,我也想尽可能的去讲简单简洁每一个篇节,当然后面肯定会补上的啦,每天都有在努力的更进能力,写的不好希望各位大佬们指正,由衷的感谢各位点评。