异步消息处理线程启动后会进入一个无限的循环体之中,每循环一次,从其内部的消息队列中取出一个消息,然后回调相应的消息处理函数,执行完成一个消息后则继续循环。若消息队列为空,线程则会阻塞等待。
解析异步消息处理机制Handler
Android中的异步消息处理主要由四个部分组成,Message、Handler、MessageQueue和Looper。
- Message
Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。上一小节我们使用到了Message的what字段,初次之外还可以使用arg1和arg2字段来携带一些整型数据,使用obj字段携带一个Object对象。 - Handler
Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage()方法,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage()方法中。 - MessageQueue
MessageQueue是消息队列的意思,它主要是用于存放所有的Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只有一个MessageQueue对象。
4.Looper
Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限循环当中,然后每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handleMessage()方法中。每个线程中也只会有一个Looper对象。
首先需要在主线程当中创建一个Handler对象,并重写handleMessage()方法。然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过Handler将这条信息发送出去。之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回Handler的handleMessage()方法中。由于Handler是在主线程中创建的,所以此时handleMessage()方法中的代码也会在主线程运行,于是我们在这里就可以安心地进行UI操作了。
Android异步消息处理机制让你深入理解Looper、Handler、Message三者关系
Looper主要是prepare()和loop()两个方法
一个线程中只有一个Looper实例
looper方法必须在prepare方法之后运行。
Looper主要作用:
1、 与当前线程绑定,保证一个线程只会有一个Looper实例,同时一个Looper实例也只有一个MessageQueue。
2、 loop()方法,不断从MessageQueue中去取消息,交给消息的target属性的dispatchMessage去处理。好了,我们的异步消息处理线程已经有了消息队列(MessageQueue),也有了在无限循环体中取出消息的哥们,现在缺的就是发送消息的对象了,于是乎:Handler登场了。
Handler
使用Handler之前,我们都是初始化一个实例,比如用于更新UI线程,我们会在声明的时候直接初始化,或者在onCreate中初始化Handler实例
1、首先Looper.prepare()在本线程中保存一个Looper实例,然后该实例中保存一个MessageQueue对象;因为Looper.prepare()在一个线程中只能调用一次,所以MessageQueue在一个线程中只会存在一个。
2、Looper.loop()会让当前线程进入一个无限循环,不端从MessageQueue的实例中读取消息,然后回调msg.target.dispatchMessage(msg)方法。
3、Handler的构造方法,会首先得到当前线程中保存的Looper实例,进而与Looper实例中的MessageQueue想关联。
4、Handler的sendMessage方法,会给msg的target赋值为handler自身,然后加入MessageQueue中。
5、在构造Handler实例时,我们会重写handleMessage方法,也就是msg.target.dispatchMessage(msg)最终调用的方法。
好了,总结完成,大家可能还会问,那么在Activity中,我们并没有显示的调用Looper.prepare()和Looper.loop()方法,为啥Handler可以成功创建呢,这是因为在Activity的启动代码中,已经在当前UI线程调用了Looper.prepare()和Looper.loop()方法
在主线程中可以直接创建Handler对象,而在子线程中需要先调用Looper.prepare()才能创建Handler对象。
1、首先Looper.prepare()在本线程中保存一个Looper实例,然后该实例中保存一个MessageQueue对象;因为Looper.prepare()在一个线程中只能调用一次,所以MessageQueue在一个线程中只会存在一个。大家可能还会问,那么在Activity中,我们并没有显示的调用Looper.prepare()和Looper.loop()方法,为啥Handler可以成功创建呢,这是因为在Activity的启动代码中,已经在当前UI线程调用了Looper.prepare()和Looper.loop()方法
2、Looper.loop()会让当前线程进入一个无限循环,不端从MessageQueue的实例中读取消息,然后回调msg.target.dispatchMessage(msg)方法。
3、Handler的构造方法,会首先得到当前线程中保存的Looper实例,并与Looper实例中的MessageQueue相关联。
4、Handler的sendMessage方法,会给msg的target赋值为handler自身,然后加入MessageQueue中。
5、在构造Handler实例时,我们会重写handleMessage方法,也就是msg.target.dispatchMessage(msg)最终调用的方法。
Handler+Looper+MessageQueue深入详解
概述:Android使用消息机制实现线程间的通信,线程通过Looper建立自己的消息循环,MessageQueue是FIFO的消息队列,Looper负责从MessageQueue中取出消息,并且分发到消息指定目标Handler对象。Handler对象绑定到线程的局部变量Looper,封装了发送消息和处理消息的接口。
实例:我们先介绍Android线程通讯的一个例子,这个例子实现点击按钮之后从主线程发送消息"hello"到另外一个名为” CustomThread”的线程。
package
import
public class LooperThreadActivity extends Activity{
/** Called when the activity is first created. */
private final int MSG_HELLO = 0;
private Handler mHandler;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
new CustomThread().start(); //新建并启动CustomThread实例
findViewById(R.id.send_btn).setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {//点击界面时发送消息
String str = "hello";
Log.d("Test", "MainThread is ready to send msg:" + str);
mHandler.obtainMessage(MSG_HELLO, str).sendToTarget();//发送消息到CustomThread实例
}
});
}
class CustomThread extends Thread{
@Override
public void run() {
//建立消息循环的步骤
Looper.prepare();//1、初始化Looper
mHandler = new Handler(){//2、绑定handler到CustomThread实例的Looper对象
public void handleMessage (Message msg) {//3、定义处理消息的方法
switch(msg.what) {
case MSG_HELLO:
Log.d("Test", "CustomThread receive msg:" + (String) msg.obj);
}
}
};
Looper.loop();//4、启动消息循环
}
}
}
我们看到,为一个线程建立消息循环有四个步骤:
初始化Looper
绑定handler到CustomThread实例的Looper对象
定义处理消息的方法
启动消息循环
初始化Looper:Looper.prepare()
一个线程在调用Looper的静态方法prepare()时,这个线程会新建一个Looper对象,并放入到线程的局部变量中,而这个变量是不和其他线程共享的。
在Looper的构造函数中,创建了一个消息队列对象mQueue,此时,调用Looper.prepare()的线程就建立起一个消息循环的对象(此时还没开始进行消息循环)。
- 绑定handler到CustomThread实例的Looper对象:mHandler = new Handler()
Handler通过mLooper= Looper.myLooper();绑定到线程的局部变量Looper上去,同时Handler通过mQueue = mLooper.mQueue;获得线程的消息队列。此时,Handler就绑定到创建此Handler对象的线程的消息队列上了。
定义处理消息的方法:Override public void handleMessage(Message msg){}
子类需要覆盖这个方法,实现接收到消息后的处理方法。启动消息循环:Looper.loop()
使用Thread+Handler实现非UI线程更新UI界面
public class ThreadhandlerActivity extends Activity{
private static final int MSG_SUCCESS = 0;
private static final int MSG_FAILURE = 1;
private Thread mThread;
private Handler mHandler = new Handler(){
public void handleMessage(Message msg){//此方法在ui线程运行
switch(msg.what) {
case MSG_SUCCESS:
mImageView.setImageBitmap((Bitmap) msg.obj);//imageview显示从网络获取到的logo
Toast.makeText(getApplication(), getApplication().getString(R.string.get_pic_success), Toast.LENGTH_LONG).show();
break;
case MSG_FAILURE:
Toast.makeText(getApplication(), getApplication().getString(R.string.get_pic_failure), Toast.LENGTH_LONG).show();
break;
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mImageView= (ImageView) findViewById(R.id.imageView);//显示图片的ImageView
mButton = (Button) findViewById(R.id.button);
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mThread == null) {
mThread = new Thread(runnable);
mThread.start();//线程启动
}
else {
Toast.makeText(getApplication(), getApplication().getString(R.string.thread_started), Toast.LENGTH_LONG).show();
}
}
});
}
Runnable runnable = new Runnable() {
@Override
public void run() {//run()在新的线程中运行
HttpClient hc = new DefaultHttpClient();
HttpGet hg = new HttpGet("http://www.oschina.net/img/logo.gif");//获取oschina的logo
final Bitmap bm;
try {
HttpResponse hr = hc.execute(hg);
bm = BitmapFactory.decodeStream(hr.getEntity().getContent());
} catch (Exception e) {
mHandler.obtainMessage(MSG_FAILURE).sendToTarget();//获取图片失败
return;
}
mHandler.obtainMessage(MSG_SUCCESS,bm).sendToTarget();//获取图片成功,向ui线程发送MSG_SUCCESS标识和bitmap对象
// mImageView.setImageBitmap(bm); //出错!不能在非ui线程操作ui元素
// mImageView.post(new Runnable() {//另外一种更简洁的发送消息给ui线程的方法。
//
// @Override
// public void run() {//run()方法会在ui线程执行
// mImageView.setImageBitmap(bm);
// }
// });
}
};
}
Android开发中Handler的经典总结
当应用程序启动时,Android首先会开启一个主线程(也叫UI线程),主线程为管理界面中的UI控件,进行事件分发。
- Handler定义
主要接收子线程发送的数据,并用此数据配合主线程更新UI。
Android主线程是线程不安全的,更新UI只能在主线程中更新,子线程中操作是危险的。
Handler运行在主线程中,它与子线程可以通过Message对象来传递数据,这个时候,Handler就承担着接收子线程传过来的(子线程用sendMessage()方法传递)Message对象,(里面包含数据),把这些消息放入主线程队列中,配合主线程进行更新UI。
- Handler一些特点
Handler可以分发Message对象和Runnable对象到主线程中,每个Handler实例,都会绑定到创建它的线程池中(一般是位于主线程),它有两个作用:
(1)安排消息或Runnable在某个主线程中某个地方执行;
(2)安排一个动作在不同的线程中执行。
Handler中分发消息的一些方法
post(Runnable)
postAtTime(Runnable,long)
postDelayed(Runnable long)
sendEmptyMessage(int)
sendMessage(Message)
sendMessageAtTime(Message,long)
sendMessageDelayed(Message,long)
以上post类方法允许你安排一个Runnable对象到主线程队列中,sendMessage类方法,允许你安排一个带数据的Message对象到队列中,等待更新。
- Handler实例
子类需要继承Handler类,并重写handleMessage(Message msg)方法,用于接收线程数据。
public class MyHandlerActivity extends Activity {
Button button;
MyHandler myHandler;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.handlertest);
button = (Button) findViewById(R.id.button);
myHandler = new MyHandler();
//当创建一个新的Handler实例时,它会绑定到当前线程和消息的队列中,开始分发数据
//Handler有两个作用:(1)定时执行Message和Runnable对象
//(2):让一个动作,在不同的线程中执行。
//它安排消息,用以下方法
//post(Runnable)
//postAtTime(Runnable, long)
// postDelayed(Runnable, long)
//sendEmptyMessage(int)
//sendMessage(Message)
//sendMessageAtTime(Message, long)
//sendMessageDelayed(Message, long)
//以上方法以post开头的允许你处理Runnable对象
//sendMessage()允许你处理Message对象(Message里可以包含数据)
MyThread m = new MyThread();
new Thread(m).start();
}
/**
* 接收消息,处理消息,此Handler会与当前主线程一块运行
**/
class MyHandler extends Handler {
public MyHandler(){
}
public MyHandler(Looper L){
super(L);
}
//子类必须重写此方法,接收数据
@Override
public void handleMessage(Message msg){
// TODO Auto-generated method stub
Log.d("MyHandler","handleMessage....");
super.handleMessage(msg);
//此处可以更新UI
Bundle b = msg.getDada();
String color = b.getString("color");
MyHandlerActivity.this.button.append(color);
}
}
class MyThread implements Runnable{
public void run(){
try{
Thread.sleep(10000);
} catch(InterruptedException e){
//TODO Auto-generated catch block
e.printStackTrace();
}
Log.d("thread....","mThread....");
Message msg = new Message();
Bundle b = new Bundle(); //存放数据
b.putString("color","我的");
msg.setData(b);
MyHandlerActivity.this.myHandler.sendMessage(msg);//向Handler发送消息,更新UI
}
}
}
一个Handler允许你发送和处理消息以及与一个线程的消息队列相关的Runnable对象。每个Handler实例都和单个线程以及该线程的消息队列有关。当你创建了一个新handler,它就会和创建它的线程/消息队列绑定,在那以后,它就会传递消息以及runnable对象给消息队列,然后执行它们。
需要使用Handler有两大主要的原因:
(1)在将来的某个时间点调度处理消息和runnable对象;
(2)将需要执行的操作放到其他线程之中,而不是自己的
若在主线程中实例化一个Handler对象,例如:
Handler mHandler = new Handler();
此时它并没有新派生一个线程来执行此Handler,而是将此Handler附加在主线程上,故此时若你在Handler中执行耗时操作的话,还是会弹出ANR对话框!
创建Handler
- 使用默认的构造方法:new Handler()
- 使用带参的构造方法,参数是一个Runnable对象或者回调对象
Handler只是简单地往消息队列中发送消息而已
它们有更方便的方法可以帮助与UI线程通信。
在Android,线程分为有消息循环的线程和没有消息循环的线程,有消息循环的线程一般都会有一个Looper。我们的主线程(UI线程)就是一个消息循环的线程。针对这种消息循环的机制,我们引入了一个新的机制Handler,我们有消息循环,就要往消息循环里面发送相应的消息,自定义消息一般都会有自己对应的处理,消息的发送和清除,消息的处理,把这些都封装在Handler里面,注意Handler只是针对那些有Looper的线程,不管是UI线程还是子线程,只要你有Looper,我就可以往你的消息队列里面添加东西,并做相应的处理。
如果在子线程里面新建handler,就会出现错误,原因就是一个线程对应一个或零个Looper和MessageQueue。handler是一种消息机制,而子线程的启用默认是没有Looper对象的(主线程有),所以在子线程使用Handler对象的时候,要先使用Looper.prepare(),启用一个Looper,然后新建Handler对象,再使用Looper.loop().
至此子线程就有了自己的Looper,可以接收和处理信息。
android.os.Looper:
Looper用于封装了android线程中的消息循环,默认情况下一个线程是不存在消息循环(message loop)的,需要调用Looper.prepare()来给线程创建一个消息循环,调用Looper.loop()来使消息循环起作用,从消息队列里取消息,处理消息。
注:写在Looper.loop()之后的代码不会被立即执行,当调用后mHandler.getLooper().quit()后,loop才会中止,其后的代码才能得以运行。Looper对象通过MessageQueue来存放消息和事件。一个线程只能有一个Looper,对应一个MessageQueue。
一个典型的Looper Thread实现:
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
android.os.Handler:
Handler用于跟线程绑定,来向线程的消息循环里面发送消息、接收消息并处理消息。
通过以下函数来向线程发送消息或Runnable:
1.post(Runnable), postAtTime(Runnable, long), postDelayed(Runnable, long);
当线程接收到Runnable对象后即立即或一定延迟调用。
2.sendEmptyMessage(int), sendMessage(Message)
, sendMessageAtTime(Message, long), and sendMessageDelayed(Message, long)。
当线程接受到这些消息后,根据你的Handler重构的handleMessage(Message)根据接收到的消息来进行处理。
一个Activity主线程中可以有多个Handler对象,但MessageQueue是只有一个,对应的Looper也是只有一个。
Looper类的静态成员函数prepareMainLooper是专门应用程序的主线程调用的,为了让其它地方能够方便地通过Looper类的getMainLooper函数来获得应用程序主线程中的消息循环对象。
Android Handler详细使用方法实例
handler使用例1.
这个例子是最简单的介绍handler使用的,是将handler绑定到它所建立的线程中。
单击Start按钮,程序会开始启动线程,并且线程程序完成后延时1s会继续启动该线程,每次线程的run函数中完成对界面输出nUpdateThread…文字,不停的运行下去,当单击End按钮时,该线程就会停止,如果继续单击Start,则文字又开始输出了。
MainActivity.java;
主要代码
public class MainActivity extends Activity{
//使用handler时首先要创建一个handler对象
Handler handler = new Handler();
//要用Handler来处理多线程可以使用Runnable接口,这里先定义该接口
//线程中运行该接口的run函数
Runnable update_thread = new Runnable()
{
public void run()
{
handler.postDelayed(update_thread,1000);
}
};
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState );
...
}
//btn监听实现
//调用handler的post方法,将要执行的线程对象添加到队列当中
//将线程接口立刻送到线程队列
handler.post(update_thread);
//将接口从线程队列中移除
handler.removeCallbacks(update_thread);
}
post方法虽然发送的是一个实现了Runnable接口的类对象,但是它并非创建了一个新线程,而是执行了该对象中的run方法。也就是说,整个run中的操作和主线程处于同一个线程。
为了解决这个问题,就需要使得handler绑定到一个新开启线程的消息队列上,在这个处于另外线程上的消息队列中处理传过来的Runnable对象和消息。
这个例子将学会怎样不使用runnable来启动一个线程,而是用HandlerThread的looper来构造一个handler,然后该handler自己获得消息,并传递数据,然后又自己处理消息,当然这是在另一个线程中完成的。
消息结构中传递简单的整型可以采用它的参数arg1和arg2,或者传递一些小的其它数据,可以用它的object,该object可以是任意的对象。当需要传送比较大的数据是,可以使用消息的setData方法,该方法需要传递一个Bundle的参数。Bundle中存放的是键值对的map,只是它的键值类型和数据类型比较固定而已。
public class HandleTest2 extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState );
setContentView(R.layout.main);
//生成一个HandlerThread对象
HandlerThread handlerThread = new HandlerThread("handler_thread");
//在使用HandlerThread的getLooper()方法之前,必须先调用该类的start(),同时开启一个新线程;
handlerThread.start();
//将由HandlerThread获取的Looper传递给Handler对象,即由处于另外线程的Looper代替handler初始化时默认绑定的消息队列来处理消息。
//HandlerThread顾名思义就是好可以处理消息循环的线程,它是一个拥有Looper的线程,可以处理消息循环;其实与其说Handler和一个线程绑定,倒不如说Handler和Looper是一一对应的。
MyHandler myHandler = new MyHandler(handlerThread.getLooper());
Message msg = myHandler.obtainMessage();
//将msg发送到目标对象,所谓的目标对象,就是生成该msg对象的handler对象
Bundle b = new Bundle();
b.putInt("age",20);
b.putString("name","Jhon");
msg.setData(b);
//将msg发送到自己的handler中
msg.sendToTarget(); //将msg发送到myHandler
}
//定义类
class MyHandler extends Handler{
public MyHandler(){
}
public MyHandler(Looper looper){
super(looper);
}
@Override
public void handleMessage(Message msg){
Bundle b = msg.getData();
int age = b.getInt("age");
String name = b.getString("name");
}
}
}
这样,当使用sendMessage方法传递消息或者使用post方法传递Runnable对象时,就会把它们传递到与handler对象绑定的处于另外一个线程的消息队列中,它们将在另外的消息队列中被处理。而主线程还会在发送完成时候继续进行,不会影响当前的操作。
这里需要注意,这里用到的多线程并非由Runnable对象开启的,而是ThreadHandler对象开启的。Runnable对象只是作为一个封装了操作的对象被传递,并未产生新线程。
另外再强调一遍,在UI线程(主线程)中:
mHandler=new Handler();
mHandler.post(new Runnable(){
void run(){
//执行代码..
}
});
这个线程其实是在UI线程之内运行的,并没有新建线程。
常见的新建线程的方法是:
Thread thread = new Thread();
thread.start();
HandlerThread thread = new HandlerThread("string");
thread.start();
Handler+Looper+MessageQueue深入详解
1.主线程给自己发送Message
Looper looper = Looper.getMainLooper();//获取主线程的Looper对象
//这里以主线程的Looper对象创建了handler,
//所以,这个handler发送的Message会被传递给主线程的MessageQueue。
handler = new MyHandler(looper);
handler.removeMessages(0);
//构建Message对象
//第一个参数:是自己指定的message代号,方便在handler选择性地接收
//第二三个参数没有什么意义
//第四个参数需要封装的对象
Message msg = handler.obtainMessage(1,1,1,"someword");
handler.sendMessage(msg);//发送消息
2.其他线程给主线程发送Message
public class MainActivity extends Activity {
private Handler handler;
//可以看出这里启动了一个线程来操作消息的封装和发送的工作
new MyThread().start();
//加载一个线程类
class Mythread extends Thread{
public void run(){
Looper looper = Looper.getMainLooper();//主线程的Looper对象
//这里以主线程的Looper对象创建了handler
//所以,这个handler发送的Message会被传递给主线程的MessageQueue
handler = new MyHandler(looper);
//构建Message对象
//第一个参数:是自己指定的message代号,方便在handler选择性地接收
//第二三个参数没有什么意义
//第四个参数需要封装的对象
Message msg = handler.obtainMessage(1,1,1,"其他线程发消息了");
handler.sendMessage(msg); //发送消息
}
}
}
- 主线程给其他线程发送Message
public class MainActivity extends Activity {
private Button btnTest;
private TextView textView;
private Handler handler;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btnTest = (Button)this.findViewById(R.id.btn_01);
textView = (TextView)this.findViewById(R.id.view_01);
//启动线程
new MyThread().start();
btnTest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
//这里handler的实例化在线程中
//线程启动的时候就已经实例化了
Message msg = handler.obtainMessage(1,1,1,"主线程发送的消息");
handler.sendMessage(msg);
}
});
}
class MyHandler extends Handler{
public MyHandler(Looper looper){
super(looper);
}
public void handleMessage(Message msg){
super.handleMessage(msg);
textView.setText("我是主线程的Handler,收到了消息:"+ (String)msg.obj);
}
}
class MyThread extends Thread{
public void run(){
Looper.prepare(); //创建该线程的Looper对象,用于接收消息
//注意了:这里的handler是定义在主线程中的哦,呵呵,
//前面看到直接使用了handler对象,是不是在找,在什么地方实例化的呢?
//现在看到了吧???呵呵,开始的时候实例化不了,因为该线程的Looper对象
//还不存在呢。现在可以实例化了
//这里Looper.myLooper()获得的就是该线程的Looper对象了
handler = new ThreadHandler(Looper.myLooper());
//这个方法,有疑惑吗?
//其实就是一个循环,循环从MessageQueue中取消息。
//不经常去看看,你怎么知道你有新消息呢???
Looper.loop();
}
//定义线程类中的消息处理类
class ThreadHandler extends Handler{
public ThreadHandler(Looper looper){
super(looper);
}
public void handleMessage(Message msg){
//这里对该线程中的MessageQueue中的Message进行处理
//这里我们再返回给主线程一个消息
handler = new MyHandler(Looper.getMainLooper());
Message msg2 = handler.obtainMessage(1,1,1,"子线程收到:"+(String)msg.obj);
handler.sendMessage(msg2);
}
}
}
}
多个任务的话,还有一个问题,这里是并行还是串行执行?
Android 中Message,MessageQueue,Looper,Handler详解+实例
http://tanghaibo001.blog.163.com/blog/static/9068612020111287218197/
Android 异步消息处理机制 让你深入理解 Looper、Handler、Message三者关系
http://blog.csdn.net/lmj623565791/article/details/38377229?utm_source=tuicool&utm_medium=referral