zoukankan      html  css  js  c++  java
  • Android -- 从源码解析Handle+Looper+MessageQueue机制

    1,今天和大家一起从底层看看Handle的工作机制是什么样的,那么在引入之前我们先来了解Handle是用来干什么的

            handler通俗一点讲就是用来在各个线程之间发送数据的处理对象。在任何线程中,只要获得了另一个线程的handler,则可以通过  handler.sendMessage(message)方法向那个线程发送数据。基于这个机制,我们在处理多线程的时候可以新建一个thread,这个thread拥有UI线程中的一个handler。当thread处理完一些耗时的操作后通过传递过来的handler向UI线程发送数据,由UI线程去更新界面。 
            主线程:运行所有UI组件,它通过一个消息队列来完成此任务。设备会将用户的每项操作转换为消息,并将它们放入正在运行的消息队列中。主线程位于一个循环中,并处理每条消息。如果任何一个消息用时超过5秒,Android将抛出ANR。所以一个任务用时超过5秒,应该在一个独立线程中完成它,或者延迟处理它,当主线程空闲下来再返回来处理它。
    

      上面的总结成一句话来说就是,由于主线程不能做耗时操作而子线程不能更新UI,解决线程间通信问题。

    2,这里防止有些同学压根没有了解过Handle,所以这里还是带着大家简单的从使用入手,先知道怎么使用之后,再来通过源码来理解它的使用

    • 常见使用

      先看一下效果:

      很简单的功能,就是类似于一个定时器的东西,TextView不停的去更新UI,要实现这个功能很简单,在java中我们可以通过Timer或者ScheduledExecutorService来实现,而现在我们打算使用Android的一些技术来实现,这里有可能有同学已经想到了一种简单的方法就是类似于一下的代码:

    public class FourActivity extends AppCompatActivity{
    
        private TextView tv_textview;
        private int count = 0;// 用于显示到textView上的数据
       
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_four);
            // 子线程中产生数据,然后需要将数据传递到主线程中,设置到Textview。
            tv_textview = (TextView) findViewById(R.id.tv_main_show);
    
            // 数据产生自子线程
            new Thread() {
                public void run() {
                    // 子线程中执行的内容:
                    while (true) {
                        if (count < 100) {
                            count++;// 反复的设置到textivew上
                            tv_textview.setText(count + "");// (错误代码)。 
                            try {
                                Thread.sleep(1000);// 模拟网络延迟。睡眠。
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
    
                        } else {
                            break;
                        }
                    }
                };
            }.start();
    
        }
    
    }
    

      是的上面的代码貌似看上去没什么问题,逻辑也很清楚,但是运行之后我们会发现报错了,报错的原因也很简单,就是子线程中不能去更新UI,那么我们又想更新UI该怎么办呢?这里就要使用到我们的Handle,具体实现代码如下:

    public class FourActivity extends AppCompatActivity{
    
        private TextView tv_textview;
        private int count = 0;// 用于显示到textView上的数据
    
        // 在主线程中创建handler对象,用于子线程发送消息,主线程处理消息。
        private Handler handler = new Handler() {
            // 重写handler处理消息的方法。
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 0:
                        int count = msg.arg1;
                        int num = msg.arg2;
                        String str = (String) msg.obj;
                        tv_textview.setText(count + "," + str);
                        // Log.i("tag", "===num:" + num + ",what:" + what);
                }
    
            };
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_four);
            // 子线程中产生数据,然后需要将数据传递到主线程中,设置到Textview。
            tv_textview = (TextView) findViewById(R.id.tv_main_show);
    
            // 数据产生自子线程
            new Thread() {
                public void run() {
                    // 子线程中执行的内容:
                    while (true) {
                        if (count < 100) {
                            count++;// 反复的设置到textivew上
    //                        tv_textview.setText(count + "");// 错误代码。
                            // 将子线程中产生的数据count,传递到UI主线程中。
                            // 写法一:Message从消息池中取数据。由handler发送给主线程。
                             Message msg = Message.obtain();//
                            // 从消息池中获取一个Message对象,用于存储数据,如果消息池中没有Message,会自动创建一个,以供使用。不要自己new
                            // // Message,为了提高效率。
                             msg.arg1 = count;// arg1,arg2,用于存储int类型的数据的。
                             msg.arg2 = 10;
                             msg.obj = "么么哒";// 用于存储Objct类型的数据
                            // // Bundle,用于存储大量的数据。
                             msg.what = 0;// 用于标记该Mesage,用于区分的。
                            //
                             handler.sendMessage(msg);//
                            // 在子线程中使用主线程的handler对象,发送消息到主线程。 
                            try {
                                Thread.sleep(1000);// 模拟网络延迟。睡眠。
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
    
                        } else {
                            break;
                        }
                    }
                };
            }.start();
    
        }
    
    }
    

      我们可以看到要实现这个功能,首先要创建一个全局变量的handle,重写它的handleMessage方法,在里面进行一些收到消息的处理,然后在子线程中发送消息。

      这里我们需要注意到的属性和方法是:

    1,获取Message对象的话使用 Message msg = Message.obtain(); 在消息池中去拿消息对象,而不要动手创建
    2,Message对象中有几个属性和方法 ,具体代码如下:
         msg.arg1 = count;// arg1,arg2,用于存储int类型的数据的。
         msg.arg2 = 10;
          msg.obj = "么么哒";// 用于存储Objct类型的数据
           // // Bundle,用于存储大量的数据。
           msg.what = 0;// 用于标记该Mesage,用于区分的。
         首先我们想传递的如果是int类型的数据的话我们可以直接使用Message中的arg1和arg2变量,如果我们想传递object对象的话我们可以使用message中的obj变量,如果想传递大的变量的话,我们可以使用bundle对象,然后可以用what来区分Message的分类
    

      我们可以一看到当我们调用handler.sendMessage(msg)的时候,执行的是我们handleMessage()方法中的逻辑。ok,这是handle的一种实现方法,我们再看下一种方法

     new Thread() {
                public void run() {
                    // 子线程中执行的内容:
                    while (true) {
                        if (count < 100) {
                            count++;// 反复的设置到textivew上
                            // 写法二:
                             Message msg = handler.obtainMessage();//
    //                         由handler来获取的消息,自然就归handler来处理
                             msg.arg1 = count;
                             msg.sendToTarget();// 将msg发送给对应的目标:target:就是handler
    
                            try {
                                Thread.sleep(1000);// 模拟网络延迟。睡眠。
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
    
                        } else {
                            break;
                        }
                    }
                };
            }.start();
    

      这时候我们是通过handle来拿到对应的Message对象的,然后直接通过message对象的sendToTarget方法发送消息,而不需要像我们之前的handler.sendMessage()方法,然后我们还有下面一种方法

    new Thread() {
                public void run() {
                    // 子线程中执行的内容:
                    while (true) {
                        if (count < 100) {
                            count++;// 反复的设置到textivew上
                            // 写法三:
                            Message msg = Message.obtain();
                            msg.arg1 = count;
                            msg.setTarget(handler);
                            msg.sendToTarget();
                            try {
                                Thread.sleep(1000);// 模拟网络延迟。睡眠。
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
    
                        } else {
                            break;
                        }
                    }
                };
            }.start();
    

      这里我们还是通过消息池拿到我们的Message对象,但是调用了msg.setTarget()来绑定handle,所以也可以直接调用msg.sendToTarget()方法

    • post()方法的使用

      先看一下的基本使用

    /**
     * step1:启动子线程获取数据
     * 
     * step2:在主线程中创建Handler对象
     * 
     * step3:在子线程中直接操作handler.post(Runnable r):重写该接口的是实现类。。
     * 
     * step4:run()---》执行在main线程中。
     * 
     * @author wangjitao
     * 
     */
    

      再看看具体的实现

    public class MainActivity extends Activity {
    
    	private TextView tv_show;
    	private Handler handler = new Handler();
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.activity_main);
    		tv_show = (TextView) findViewById(R.id.tv_show);
    	}
    
    	// 点击按钮,由子线程向主线程发送数据
    	public void click(View v) {
    		new Thread() {
    			@Override
    			public void run() {
    				// 产生数据,发送到主线程中
    				final int num = 100;
    				final String str = "HelloWorld";
    				ArrayList<String> list = new ArrayList<String>();
    				Collections.addAll(list, "abc", "aa", "bb");
    				Bundle bundle = new Bundle();
    				bundle.putString("str", str);
    				bundle.putSerializable("list", list);
    				// msg.setData(bundle);
    				handler.post(new Runnable() {// handler未来要做的事,写在run里。
    
    					@Override
    					public void run() {
    						// 执行在主线程中的。
    						Log.i("tag", "==线程id:" + Thread.currentThread().getId()
    								+ ",线程名称:" + Thread.currentThread().getName());
    						tv_show.setText("str," + str + ",num:" + num);
    					}
    				});
    
    			}
    		}.start();
    	}
    }
    

      这里关键的是调用handle.post方法,且在方法里面直接执行了更新UI操作,但是大家运行一下,不报错,所以这里的和大家说一下,这里的post方法是将运行在主线程中,所以就可以更新ui,我们过一下看看源码它是怎么实现的。

    • 从主线程中传递message到子线程  

      我们上面实现了从子线程中发送消息,在主线线程中获取消息,那么我们现在想在主线程中发送消息,在子线程中处理消息,这个我们该怎么实现呢?

    public class MainActivity extends Activity {
    	private Handler handler = null;
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.activity_main);
    	}
    
    	// 点击按钮,创建子线程
    	public void click_start(View v) {
    		new MyThread().start();
    	}
    
    	// 点击该按钮,数据从主线程传递到子线程中
    	public void click_send(View v) {
    		// 由handler发送数据
    		Message msg = Message.obtain();//
    		msg.what = 1;
    		msg.arg1 = 100;
    		msg.obj = "我是从主线程中传递来的数据";
    		handler.sendMessage(msg);// msg:target:handler
    	}
    
    	// 自定义类,继承Thread,表示用于子线程
    	class MyThread extends Thread {
    		@Override
    		public void run() {
    			// 子线程中要执行的代码:
    			// 用于接收主线程传来的数据
    			// 由handler接收处理数据就可以。
    			/**
    			 * 报错信息: java.lang.RuntimeException: Can't create handler inside
    			 * thread that has not called Looper.prepare()
    			 */
    			// 应该先执行:Looper.prepare();
    			/**
    			 * 表示将当前的线程升级为:Looper线程
    			 * 
    			 * 1.创建一个Looper对象。注意:一个线程中只能有一个Looper对象。用ThreadLocal<T>
    			 * 
    			 * 2.一个Looper对象,负责维护一个消息队列:new MessageQueue
    			 */
    			Looper.prepare();// 将子线程升级,成Looper线程。就可以操作handler对象。否则一个普通的子线程,不能操作handler。
    			handler = new Handler() {
    				@Override
    				public void handleMessage(Message msg) {
    					switch (msg.what) {
    					case 1:
    						int num = msg.arg1;
    						String str = (String) msg.obj;
    						Log.i("tag", "===子线程:" + Thread.currentThread().getId()
    								+ ",线程名字:" + Thread.currentThread().getName()
    								+ ",数据:" + num + ",str:" + str);
    						break;
    
    					default:
    						break;
    					}
    				}
    			};
    			Looper.loop();// 用于循环处理消息。
    		}
    	}
    
    }
    

      这里要注意,要想子线程收到messsage ,首先要将子线程升级成Looper线程,主要调用下面这两个方法

      Looper.prepare();// 将子线程升级,成Looper线程。就可以操作handler对象。否则一个普通的子线程,不能操作handler。
      Looper.loop();// 用于循环处理消息。
    
    • 源码解析

    既然上面的先调用的是Looper.prepare();方法,那么我们来看看prepare方法吧

     public static void prepare() {
            prepare(true);
        }
    
        private static void prepare(boolean quitAllowed) {
            if (sThreadLocal.get() != null) {
                throw new RuntimeException("Only one Looper may be created per thread");
            }
            sThreadLocal.set(new Looper(quitAllowed));
        }
    
    private Looper(boolean quitAllowed) {
            mQueue = new MessageQueue(quitAllowed);
            mThread = Thread.currentThread();
        }
    

      再看一下它的一些重要的成员变量

                    //ThreadLocal线程本地变量,用于为该线程中只有一个Looper对象。
    		static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    		//Looper内部维护的消息队列
    		final MessageQueue mQueue;
    		//当前线程对象本身
    		final Thread mThread;

      可以看到,这里我们调用Looper.prepare()方法方法的时候,首先判断sThreadLocal 是否已经存在Looper对象,如果存在则抛异常,这里是因为每个线程中只能包含一个Looper对象去维护,如果不存在,则new Looper对象,且初始化内部消息维护队列 mQueue、线程本身mThread。

      看完prepare()方法之后我们继续往下看,Looper.loop()。先看一下源码

    public static void loop() {
            final Looper me = myLooper();
            if (me == null) {
                throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
            }
            final MessageQueue queue = me.mQueue;
            Binder.clearCallingIdentity();
            final long ident = Binder.clearCallingIdentity();
    
            for (;;) {
                Message msg = queue.next(); // might block
                if (msg == null) {
                    // No message indicates that the message queue is quitting.
                    return;
                }
    
                // This must be in a local variable, in case a UI event sets the logger
                final Printer logging = me.mLogging;
                if (logging != null) {
                    logging.println(">>>>> Dispatching to " + msg.target + " " +
                            msg.callback + ": " + msg.what);
                }
    
                final long traceTag = me.mTraceTag;
                if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                    Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
                }
                try {
                    msg.target.dispatchMessage(msg);
                } finally {
                    if (traceTag != 0) {
                        Trace.traceEnd(traceTag);
                    }
                }
    
                if (logging != null) {
                    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
                }
    
                // Make sure that during the course of dispatching the
                // identity of the thread wasn't corrupted.
                final long newIdent = Binder.clearCallingIdentity();
                if (ident != newIdent) {
                    Log.wtf(TAG, "Thread identity changed from 0x"
                            + Long.toHexString(ident) + " to 0x"
                            + Long.toHexString(newIdent) + " while dispatching to "
                            + msg.target.getClass().getName() + " "
                            + msg.callback + " what=" + msg.what);
                }
    
                msg.recycleUnchecked();
            }
        }
    
    
    
    public static @Nullable Looper myLooper() {
            return sThreadLocal.get();
        }
    

      然后我将上面的代码提取出重要的

               //循环工作
    	    public static void loop() {
    	    //获取当前的looper对象
    	    	final Looper me = myLooper();
    	    //获取消息队列
    	    	final MessageQueue queue = me.mQueue;
    	    //循环处理消息
    	    	 for (;;) {
    	    	 	Message msg = queue.next(); // 从消息队列中获取消息
    	    	 	
    	    	 	msg.target.dispatchMessage(msg);//将msg交给对应的handler去分发。msg.target:就是对应的handler
    	    	 	//handler.dispatchMessage(msg);
    	    	 	
    	    	 	 msg.recycle();//回收消息到消息池,便于下次使用。
    	    	 }
    	    
    	    }
    	    //从线程的本地变量中获取Looper对象
    	    public static Looper myLooper() {
    	        return sThreadLocal.get();
    	    }
    	}
    

      首先我们通过调用本身的 myLooper()来获取当前的Looper对象,然后获取mQueue消息队列,然后就是for循环了,一直获取mQueue消息队列中的Massage对象,知道消息队列中没有消息然后退出循环,然后在循环中最关键的是一下这句代码

    msg.target.dispatchMessage(msg);
    

      首先msg.target,这个大家有没有很熟悉,我们上面使用的时候曾经使用过msg.sendToTarget()和msg.setTarget(handler),看里面的参数!是一个handle,为了验证我们的猜想我们看看message源码中的target属性

    public final class Message implements Parcelable {
       ...........省略代码
        /*package*/ int flags;
    
        /*package*/ long when;
        
        /*package*/ Bundle data;
        
        /*package*/ Handler target;
        
        /*package*/ Runnable callback;
        
        // sometimes we store linked lists of these things
        ...........省略代码
    }
    

      果然,这里target是Handle对象,也就是说是调用的handle.dispatchMessage(msg)的对象,我们来看看handle源码

    public void dispatchMessage(Message msg) {
            if (msg.callback != null) {
                handleCallback(msg);
            } else {
                if (mCallback != null) {
                    if (mCallback.handleMessage(msg)) {
                        return;
                    }
                }
                handleMessage(msg);
            }
        }
    
    
     public interface Callback {
            public boolean handleMessage(Message msg);
        }
        
    

      看到没  这里我们mCallback.handleMessage还是最终调用的是handle中的handleMessage方法!!!,这样整个Looper源码的逻辑就通了,首先创建Looper对象用来维护本线程中的消息队列,然后for循环,一直将消息队列中的message分发到对应的handle上去。

      这里我们的Looper中有以下注意事项

    	总结:Looper的注意事项
    	1.每一个线程只能有最多一个Looper对象。
    	2.当创建Looper的同时,MessageQueue一同被创建。
    	3.调用loop()方法,循环从消息队列上获取消息,分发给对应的handler。
    

      下面我们来看看Handle的源码

      首先先看一下全局变量

                   //与当前的handler对象关联的消息队列
    		final MessageQueue mQueue;
    		//与handler关联的Looper对象
    		final Looper mLooper;
    		final Callback mCallback;
    

      在构造方法中进行一些赋值

    //创建Handler对象
    		public Handler() {
    	        this(null, false);
    	    }
    	    
    	    public Handler(Callback callback, boolean async) {
    	    	mLooper = Looper.myLooper();
    	    	if (mLooper == null) {
    	            throw new RuntimeException(
    	                "Can't create handler inside thread that has not called Looper.prepare()");
    	        }
    	        mQueue = mLooper.mQueue;//将Looper维护的消息队列,赋值给当前handler所关联的消息队列。
            	mCallback = callback;
    	    }
    

      这里的构造和我们的Thread构造很像,可通过直接匿名内部内的方式也可以使用创建CallBack子类的方式得到Handle对象这里调用 Looper.myLooper()拿到,然后设置一下消息队列,其实还有一个构造方法,如下:

    public Handler(Looper looper, Callback callback, boolean async) {
            mLooper = looper;
            mQueue = looper.mQueue;
            mCallback = callback;
            mAsynchronous = async;
        }
    

      这里由于我们不怎么使用,所以只说一下几个参数的含义传第一个参数进来一个looper来代替默认的looper,第二个参数callback接口用于处理handler传递的Message,第三个是说是否要异步

      ok,我们继续往下看方法,我们handle常使用sendMessage方法,来看看它里面是怎么使用的

     public final boolean sendMessage(Message msg)
        {
            return sendMessageDelayed(msg, 0);
        }
    
    
    public final boolean sendMessageDelayed(Message msg, long delayMillis)
        {
            if (delayMillis < 0) {
                delayMillis = 0;
            }
            return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
        }
    
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
            MessageQueue queue = mQueue;
            if (queue == null) {
                RuntimeException e = new RuntimeException(
                        this + " sendMessageAtTime() called with no mQueue");
                Log.w("Looper", e.getMessage(), e);
                return false;
            }
            return enqueueMessage(queue, msg, uptimeMillis);
        }
    
    
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
            msg.target = this;
            if (mAsynchronous) {
                msg.setAsynchronous(true);
            }
            return queue.enqueueMessage(msg, uptimeMillis);
        }
    

      最后其实调用的是handle类中的enqueueMessage方法,然后 msg.target = this;很重要 ,绑定发送message消息对象和当前的handle对象,把message插入到消息队列中。

      下面我们继续看handle.dispatchMessage()由于在Looper中已经讲过了的,所以在这里不在和大家废话了。

      再看看我们的HandleMessage()方法

                //由接收数据的线程, 重写的方法,表示处理msg
    	    public void handleMessage(Message msg) {
        	    }
    

      我们一般会重写该方法,进行一些接收消息的操作。

      继续往看handle.post()方法

    public final boolean post(Runnable r)
        {
           return  sendMessageDelayed(getPostMessage(r), 0);
        }
    
    private static Message getPostMessage(Runnable r) {
            Message m = Message.obtain();
            m.callback = r;
            return m;
        }
    

      可以看到这里最终还是调用了sendMessageDelayed方法,和我们的sendMessage方法一样,但是不同的区别是什么呢?这里调用了getPostMessage()中,我们创建了一个Message对象,并将传入的Runnable对象赋值给Message的callback成员字段,然后返回该Message,然后在post方法中该携带有Runnable信息的Message传入到sendMessageDelayed方法中。由此我们可以看到所有的postXXX方法内部都需要借助sendMessageXXX方法来实现,所以postXXX与sendMessageXXX并不是对立关系,而是postXXX依赖sendMessageXXX,所以postXXX方法可以通过sendMessageXXX方法向消息队列中传入消息,只不过通过postXXX方法向消息队列中传入的消息都携带有Runnable对象(Message.callback),这时候要要给大家贴上我们之前看到的一些代码。

    public void dispatchMessage(Message msg) {
            if (msg.callback != null) {
                handleCallback(msg);
            } else {
                if (mCallback != null) {
                    if (mCallback.handleMessage(msg)) {
                        return;
                    }
                }
                handleMessage(msg);
            }
        }
    
    
        private static void handleCallback(Message message) {
            message.callback.run();
        }
    

      这里我们首先判断msg的callback是否为空,显然在调用post方法的时候我们不为空,所以走的是handleCallback方法,而在这个里面直接调用的是我们Runnable中的run

    方法,也就是说直接执行Runnable中的run方法。这里大家可能会有疑问了,之前不是说handle.post方法是运行在主线程中吗,我这里明明new了一个Runnable,为什么还运行在主线程中啊,首先我们要明白一个误区,不是创建一个Runnable对象就是标明是新建一个子线程,在我们上例的情况中我们的Handle是创建在Activity中的,Handler是绑定到创建它的主线程中的,Handler跟UI主线程是同一个线程,所以它本来就是在主线程中,再看看源码google给的注释

    * Causes the Runnable r to be added to the message queue.
    * The runnable will be run on the thread to which this handler is 
    * attached. 
    

      注释的意思是runnable会在handler所在的线程中执行。按照注释的意思以及我们经常使用的情况来看,runnable的逻辑无疑是在主线程中执行的。这也解决了我们为什么post方法是执行在主线程中的。

      我们来总结一下Handle相关的知识点

    Handler的属性和方法:
    	属性:
    		final MessageQueue mQueue;
    		final Looper mLooper;
    		final Callback mCallback;
    		
    		
    	方法:5个重要方法
    		sendMessage(msg);
    		handleMessage();
    		post();
    		sendEmptyMessage();
    		obtainMessage();
    		
    		
    		dispatchMessage();
    		sendMessageDelayed();
    		sendMessageAtTime();
    
    • Message源码
    	public final class Message implements Parcelable {
    		 public int what;
    		 public int arg1;
    		 public int arg2;
    		 public Object obj;
    	}
    		what
    		arg1
    		arg2
    		obj
    		方法:
    		obtain();
    		setTarget(Handler target) 
    		setData()
    		sendToTarget();
    

      Message中的源码超级简单,这里就不给大家废话了,只需要知道这几个重要的方法和属性

      到这里我们基本上就分析完了。其实很简单有没有.......................

      

      

      

  • 相关阅读:
    公共控件
    winform 窗口 属性
    ADO
    笔记备忘
    常识 备忘
    Symbol
    Promise
    定义类 属性 方法 执行
    x is string str ======x is string 变量名
    ManualResetEvent多线程进行,全部完成后,回调
  • 原文地址:https://www.cnblogs.com/wjtaigwh/p/6769983.html
Copyright © 2011-2022 走看看