【朝花夕拾】Handler篇
前言
转载请声明,转自【https://www.cnblogs.com/andy-songwei/p/9178735.html】,谢谢!
如果您的app中没有使用过Handler,那您一定是写了个假app;如果您笔试题中没有遇到Handler相关的题目,那您可能做了份假笔试题;如果您面试中没被技术官问到Handler的问题,那您也许碰到了个假面试……因为它太重要了,也太容易因使用不当二带来很多问题。笔者工作这么多年,对Handler经常感觉如芒在背,如鲠在喉,使用起来经常不太自信,所以不得不专门回过头来好好研究和整理一下,解决掉这个大麻烦。如果您也有我这样的感受,希望本文能给您带来一定的帮助。为了方便理解和记忆,笔者把Handler机制类比到生活中具体的场景,希望能够做到通俗易懂,加深读者的印象。另外,笔者技术有限,有描述不当的地方,请雅正。
本文主要分为如下几个部分
一、痴汉委托媒婆送情书:Handler发送Message
二、媒婆把情书存入信箱:将Message插入到MessegeQueue消息队列
三、妹子派丫鬟取情书:Looper取消息
四、妹子读情书:对消息的处理(注意:这一节是重难点,对理解Handler反馈消息比较重要,后面还会继续讲到)
五、妹子的回应:消息回调
六、妹子对痴汉说:“你的都是我的,我的还是我的”:回调函数所在线程问题分析
七、做好安全措施:Handler内存泄漏问题
八、爱情的结晶:笔试与面试要点
九、附录:HandlerThread
十、补充知识点
Android中子线程和UI线程(即主线程),就像古时候的痴男和怨女。某天,一痴汉看上了一妹子,一见钟情,想要表达爱意和提亲。可是,那个时代,即使再心急火燎的,也不能冒冒失失直接去找妹子求婚啊。怎么办呢?痴汉顺理成章想到了媒婆——Handler!稍有一点Android开发基础的朋友都知道“主线程不做耗时操作,子线程不更新UI ”。可是当子线程完成耗时操作,需要更新UI,要怎么办呢?Handler作为一名经验老到的媒婆,在痴汉(子线程)和妹子(UI线程)之间扮演了总要的信使角色。
一、痴汉委托媒婆送情书:Handler发送Message
① sendMessage(Message msg)源码截图
② sendEmptyMessage(int what)源码截图
③ post(Runnable r) 源码截图
④ postDelayed(Runnable r, long delayMillis)源码截图
看,多么熟悉的面孔!!!没错,sendMessage(...),sendEmptyMessage(...), post(...), postDelay(...),平时使用最广泛的四个方法,媒婆Handler主要就是靠的这四招,替子线程向UI线程递信(发送Message)的。从上图中左上角我们可以直观地看到,这四个方法都直接或间接递地在调用sendMessageDelay(...)方法。殊途同归,Handler无论使用何种方法,其宗旨都是在想办法传递信息。无疑,Handler这个媒婆很聪明,会根据不同的场景选择使用其中的一种合适的方法。
二、媒婆把情书存入信箱:将Message插入到MessegeQueue消息队列
sendMessageDelay(...)层层往下走,会调用enqueue(...)方法。该方法会触发MessageQueue调用自己的enqueueMessage(...)方法。
MessageQueue中enqueueMessage(...)方法的关键代码如下:
MessageQueue,就像妹子的信箱,媒婆(Handler)送来的情书(Message)就被存储这个里面。它的学名叫做消息队列,从源码总可以看到,它采用链表的形式,无限循环将消息加入其中。
三、妹子派丫鬟取情书:Looper取消息
情书总是不期而至,媒婆把情书放入信箱,却没有通知妹子,妹子又如何知道意中人有信件送达呢?原来,妹子是大家闺秀,专门有丫鬟伺候她,时时刻刻检查着信箱。这个丫鬟就叫Looper,扮演着重要的角色。
Looper中有个loop( )方法,正如以下代码所示,本质上是调用MessageQueue.next( )方法。这个方法可以对应MessageQueue.enqueueMessage(...)方法,一个无限循环存入Message,另一个就无限循环取出Message。就是Looper这个勤劳的丫鬟在不停查看信箱,有情书到了,就取出来交给妹子。如下代码显示了Looper从消息队列中取消息的关键代码,可以看到这是一个无限循环的过程。这里,要着重注意第14行, msg.target在前面的enqueueMessage(...)方法中有提到,"msg.target = this", 说明它就是Handler的一个实例,对应UI线程中new的Handler实例,它调用了dispatchMessage(msg),该方法的作用就是安排处理Message,后面(妹子读情书)中会详细讲到。
1 public static void loop() { 2 final Looper me = myLooper(); 3 if (me == null) { 4 throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); 5 } 6 final MessageQueue queue = me.mQueue; 7 ...for (;;) { 8 Message msg = queue.next(); // might block 9 if (msg == null) { 10 // No message indicates that the message queue is quitting. 11 return; 12 } 13 ...try { 14 msg.target.dispatchMessage(msg); 15 ... 16 } finally { 17 ... 18 } 19 ... 20 msg.recycleUnchecked(); 21 } 22 }
那么问题来了,Looper又是从哪里来的呢?难道是和孙猴子一样从石头中蹦出来的?Android程序本质上也是java程序。我们总是疑惑,为什么我们的app中怎么也找不到main( ) 入口?哈哈,其实仍然是有main( )入口函数的,在ActivityThread中:
1 public static void main(String[] args) {
2 ...
3 Looper.prepareMainLooper();
4 ...
5 Looper.loop();
6 ...
7 }
豁然开朗了吧,Looper的身世都在main(...)方法中!Looper.prepareMainLooper()作用是生成一个Looer实例,然后就Looper就调用了loop( )方法,开启了她作为丫鬟勤劳的一生。
只能说妹子的命是真好,有个好爹,她从出生开始,老爹就为她安排好了体贴的丫鬟,全心全意地为她服务着。
四、妹子读情书:对消息的处理(注意:这一节是重难点,对理解Handler反馈消息比较重要,后面还会继续讲到)
上一节 ”妹子派丫鬟取情书”中提到了该方法(loop( )方法中的第14行)。此刻,情书已经到了妹子的手中, 妹子要开始享受地读情书了。
这里,msg.callback就是Runnable类型,看到这里,有没有想到点什么呢?是不是想到了post(Runnable r)和postDelay( ...)的参数?没错,这里就有如下两种情形
(1) 如果您在发送消息的时候选择的是post(...)或者postDelay(...):
回头看第1节中,这两种方法直接或间接调用sendMessageDelayed(Message msg ,long delayMills)时,传入的Message实例方式为getPostMessage(Runnable r)
看到“m.callback = r”这一行,msg.callback != null就成立,后面调用handleCallback(msg):
这里的run( )就是调用了post(...)或postDelay(...)中参数Runnable实例的回调方法run( ),在run()中UI线程对界面更新。在这里,可能会有读者非常疑惑,子线程中怎么能直接更新UI呢?这里是不是笔者犯糊涂搞错了?其实这里,run( )中所在的线程仍然为UI线程,我们会在后续的小结中给出分析结果,到时候千万不要惊掉下巴噢!
(2) 如果选择的是sendMessage(Message mgs)或sendEmptyMessage(int what):
这两种方法传递给sendMessageDelayed(Message msg ,long delayMills) 的实例msg,一般有两种途径,一种是new Message()(不推荐),另外一种是 Message.obtain( )。
查看上述源码可知,根本没有msg.callback啥事了,其值就为null了。msg.callback != null不成立,从而走else分支,这里又要做出条件判断了。
咳咳,注意了,笔者又要上思维导图了
Handler有5个构造函数, 但实质上,都是调用的如下构造函数:
构造函数1
1 public Handler(Callback callback, boolean async) { 2 ... 3 mLooper = Looper.myLooper(); 4 if (mLooper == null) { 5 throw new RuntimeException( 6 "Can't create handler inside thread that has not called Looper.prepare()"); 7 } 8 mQueue = mLooper.mQueue; 9 mCallback = callback; 10 mAsynchronous = async; 11 }
构造函数2
1 public Handler(Looper looper, Callback callback, boolean async) { 2 mLooper = looper; 3 mQueue = looper.mQueue; 4 mCallback = callback; 5 mAsynchronous = async; 6 }
我们回过头来看看dispatchMessage(...)方法,mCallback就是实例化Handler时传入的。如果实例化时传入了Callback实例,则mCallback != null,这样就调mCallback .handleMessage(msg),在实例中override该方法。而如果在实例化的时候,没有传入Callback实例,那就调用自己的handleMessage(...),如下所示,其实它是个空函数,也需要在实例中去override。
此时可刻,读者应该很清楚咱们的妹子是如何处理信件了的吧,如果还不清楚,请对照源码截图,再看一遍 ^_^。
五、妹子的回应:消息回调
妹子看完了情书,总得给痴汉一些反馈吧,是开心?还是生气?要不然痴汉长期得不到反馈,说不定心灰意冷,要移情别恋。
对于妹子的反映,这里接着上一节内容继续往下看。
post(...)和postDelay(...)的情况更新界面
1 new Thread(new Runnable() { 2 @Override 3 public void run() { 4 new Handler(Looper.getMainLooper()).post(new Runnable() { 5 @Override 6 public void run() { 7 mTextView.setText("收到你的信,无比开心,咱们小花园见"); 8 } 9 }); 10 } 11 }).start(); 12 ... 13 new Thread(new Runnable() { 14 @Override 15 public void run() { 16 new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 17 @Override 18 public void run() { 19 mTextView.setText("收到你的信,无比开心,咱们小花园见"); 20 } 21 },1000); 22 } 23 }).start();
sendMessage(...)和sendEmptyMessage(...)的情况更新界面,下面默认为主线程的Looper
1 private Handler mHandler = new Handler() { 2 @Override 3 public void handleMessage(Message msg) { 4 super.handleMessage(msg); 5 switch (msg.what) { 6 case 0x001: 7 mTextView.setText("收到你的信,无比开心,咱们小花园见"); 8 break; 9 default: 10 break; 11 } 12 } 13 }; 14 ... 15 new Thread(new Runnable() { 16 @Override 17 public void run() { 18 Message msg = Message.obtain(); 19 msg.what = 0x001; 20 mHandler.sendMessage(msg); 21 } 22 }).start(); 23 ... 24 new Thread(new Runnable() { 25 @Override 26 public void run() { 27 mHandler.sendEmptyMessage(0x001); 28 } 29 }).start();
说明:这种就是上一节中第二中情况,笔者平时自己写代码的时候,没有在new Handler的时候传入Callback,这里就override了handler自己的handleMessage方法。
如此一来,痴汉和妹子就过上了甜蜜的生活了。
六、妹子对痴汉说:“你的都是我的,我的还是我的”:回调函数所在线程问题分析
还记得第4节中第(1)小节中留下的疑问吗?就是篇中的红色部分。要说new Handler中override的handleMessage(msg)方法在UI主线程之中,相信读者都不会有异议,就像妹子(UI主线程)说:"我的还是我的"。可是post(...)和postDelay(...)中Runnable的回调方法run(),也是执行在UI主线程中,那就有疑惑了。
无图无真相,先来看看一个例子:
打印结果为
实验结果这证明了之前的结论。其实,如果从基础语法来分析,Runnable只是一个接口,run( )是它的一个抽象方法。还是第4节中红色部分提到过,中途调用了msg.callback.run( ),就是调用的Runnalbe实例的run(),就是一个普通的回调方法而已,而不是开辟的一个子线程,不能因为看到run( )就认为是多线程。这一点如果java多线程基础比较扎实,就容易理解了,这里不做深入描述,请读者自行研究,有机会的话再单独写一篇java多线程的文章。
所以,妹子(UI线程)对"霸道"地对痴汉(子线程)说:“你的都是我的,我的还是我的”。^_^
结论:无论是override的handleMessage(msg)方法,还是这里的run( ),都是在主线程中完成的。仍然搞不清楚原理的,可以先记住这个结论。
七、做好安全措施:Handler内存泄漏问题
虽然是合法夫妻,但激情燃烧的同时,必要的安全措施还是要做的。
Hander内存泄漏的问题,也是很常见的问题。这里推荐别人写得不错的博客:
https://blog.csdn.net/javazejian/article/details/50839443
https://www.jianshu.com/p/ed9e15eff47a
八、爱情的结晶:笔试与面试要点
Handler既然如此的重要,那笔试面试啥的,都不在话下了。笔者这么多年参加面试情况看 ,80%的情况是要和笔试或者和技术面试官聊到的。所以,如果你要找工作了,一定要好好组织语言,把自己所知道的说清楚。关于这一块,有如下几个要点(根据自己经验终结):
(1)如果是笔试题,一般会问:Handler,Message,MessageQueue,Looper,ActivityThread之间的关系。
(2)如果是技术官面,一般会让你说说Handler机制。这里就按照我文中的思路,按照 Message封装消息,Handler发送Message,MessageQueue存入Message,Looper.loop( )循环取Message,发送到UI线程中,回调的方式给出回应即可。
(3)聊到内存泄漏的时候,可以扩展到Handler引起的泄漏问题,可以加分。
(4)跨进程通信有哪些方式,Handler便是其中之一。
(5)谈到AsyncTask的时候,可以提到是对Handler的封装(第三方插件EventBus,个人猜测也是封装了Handler,没有研究过其源码,读者可以自己去证实)。
九、HandlerThread
有这样一种场景,比如手机的人脸识别,有3个关键步骤:openCamera,compare,closeCamera。他们有如下的关系:(1)这三个步骤都是耗时操作;(2)每个步骤又必须要在上一个步骤完成后才能执行,即串行;(2)而且每完成一个步骤后,又需要切换到主线程执行一些更新界面等需要在主线程完成的工作操作,即需要频繁在主线程和子线程中来回切换。这个时候就要考虑到开辟子线程和线程间通信了,如果只用Handler和Thread来实现,可能就需要多次new Thread()了,这样是繁琐的,此时HandlerThread就粉墨登场了。
HandlerThread从字面意思可以理解为Handler和Thread的组合套拳。Android中对其的定义为:
Handy class for starting a new thread that has a looper. The looper can then be used to create handler classes. Note that start() must still be called.
使用步骤如下:
(1) 创建HandlerThread,即创建一个包含Looper的线程:
HandlerThread mHandlerThread = new HandlerThread("handler_thead");
(2) 启动HandlerThread,即启动这个线程:
mHandlerThread.start( );
(3) 获取改线程的Looper:
Looper mLooper = mHandlerThread.getLooper();
(4) 创建Handler,通过mLooper进行初始化,此时Handler的回调函数会在子线程mHandlerThread中运行:
1 Handler mThreadHandler = new Handler(mLooper) { 2 @Override 3 public void handleMessage(Message msg) { 4 super.handleMessage(msg); 5 //这个方法是运行在 mHandlerThread线程中的,可以执行耗时操作 6 Log.d("handler ", "消息: " + msg.what + " 线程: " + Thread.currentThread().getName()); 7 switch (msg.what){ 8 case 0x001: 9 handleOpenCamera(); 10 break; 11 case 0x002: 12 handleCompare(); 13 break; 14 case 0x003: 15 handleCloseCamera(); 16 break; 17 default: 18 break; 19 } 20 } 21 };
(5) 创建拥有主线程Looper的Handler,用于子线程完成后,通知UI线程做相应操作:
1 Handler mUIHandler = new Handler(getMainLooper()){ 2 @Override 3 public void handleMessage(Message msg) { 4 switch (msg.what){ 5 case 0x004: 6 mTextView.setText("相机打开"); 7 break; 8 case 0x005: 9 mTextView.setText("匹配完成"); 10 break; 11 case 0x006: 12 mTextView.setText("相机关闭"); 13 break; 14 default: 15 break; 16 } 17 } 18 };
(6) 在主线程中需要开启子线程处理事务的时候,用发送信息:
mThreadHandler.sendEmptyMessage(0x001/002/003);
当然,也可以用其它发送Message的方式发送,其回调方法也是在mThreadHandler子线程中。
(7) 子线程执行完毕后,发送消息通知主线程:
mUIHandler.sendEmptyMessage(0x004/0x005/0x006);
同样可以用另外3种发送Message的方式发送,这样就实现了和主线程通信。
(8) 释放ThreadHandler资源,有两种方式:
(1) mThreadHandler.quit();停止新消息加入,清除所有MessageQueue中的消息,包括延时和非延时的消息。
(2) mThreadHandler.quitSafely();停止新消息加入,清除所有MessageQueue中的延时消息,并把所有非延时消息派发出去给handler执行。
步骤看起来多,其实用起来很简单,这里就不单独贴出完整的demo了,相信对于机智的你来说,小菜一碟了。
十、补充知识点
除了前面总结的知识点外,笔者继续阅读了一些关于Handler的资料,比如任玉刚的《Android开发艺术探索》。这本书中对Handler也做了比较详细的讲解,如下针对该书的描述,做一些整理和补充,希望能加深读者对Handler的认识。
1、为什么要引入Handler
Handler的主要作用是切换线程,将线程切换到Handler所使用的Looper所在线程中去,我们大部分的开发者通常使用Handler是用于子线程通知主线程更新UI,我们需要明确的是更新UI只是Handler的其中一个作用而已。
那么为什么只能在主线程中更新UI,而不能在子线程中完成呢?因为Android系统规定,只能在主线程中访问UI,如果在子线程中访问UI,程序就会报错。在访问UI的时候,系统会调用ViewRootImpl类中的checkThread方法,如下所示:
1 //=======ViewRootImpl.java======= 2 final Thread mThread; 3 ...... 4 public ViewRootImpl(Context context, Display display) { 5 mThread = Thread.currentThread(); 6 } 7 ...... 8 void checkThread() { 9 if (mThread != Thread.currentThread()) { 10 throw new CalledFromWrongThreadException( 11 "Only the original thread that created a view hierarchy can touch its views."); 12 } 13 } 14 ......
我们知道ViewRootImpl是View体系根View DecorView与Activity的PhoneWindow之间的纽带,最初ViewRootImpl实例化的时候,是在主线程中完成的。所以,上述checkThread方法可以用来判断当前是否为主线程,不是则报异常,该异常应该比较常见的了。
那么,系统为什么不允许在子线程中访问UI呢?这是因为UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态。那么为什么系统不对UI控件的访问加上锁机制呢?主要是因为如果加上锁机制会有两个缺点:1)使访问逻辑变得复杂。2)降低访问UI的效率,因为锁机制会阻塞某些线程的执行。所以,最简单且高效的方法就是采用单线程模型来处理UI操作,对于开发者来说也不是很麻烦,只需要通过Handler切换一下UI访问的执行线程即可。
所以Handler的出现,就解决了子线程中不能访问UI的问题。
2、Handler回调所在线程问题
在对Handler理解不深入的时候,一直没有认真注意过new一个Handler后,回调方法所在的线程问题,总以为任何时候都可以在回调方法中更新UI。事实上,之所以会有这样的错误认识,是因为我们使用Handler的时候基本上都用于更新UI了,就犯了经验主义错误。实际上,回调方法所在线程,和发送消息的handler使用的Looper所在的线程一致。下面我们先通过一些实验开看看结果。
(1)在子线程中使用main Looper的情况
1 private void testHandler() { 2 Log.i("songzheweiwang", "thread1=" + Thread.currentThread()); 3 new Thread(new Runnable() { 4 @Override 5 public void run() { 6 new Handler(Looper.getMainLooper()).post(new Runnable() { 7 @Override 8 public void run() { 9 Log.i("songzheweiwang", "thread2=" + Thread.currentThread()); 10 } 11 }); 12 } 13 }).start(); 14 }
在主线程中调用如上方法,对应的log如下,可见回调方法是在主线程中:
1 08-31 12:48:49.342 9414-9414/com.example.demos I/songzheweiwang: thread1=Thread[main,5,main]
2 08-31 12:48:49.373 9414-9414/com.example.demos I/songzheweiwang: thread2=Thread[main,5,main]
(2)在子线程中使用子线程Looper的情况
1 private void testHandler() { 2 Log.i("songzheweiwang", "thread1=" + Thread.currentThread()); 3 new Thread(new Runnable() { 4 @Override 5 public void run() { 6 Looper.prepare(); 7 new Handler().post(new Runnable() { 8 @Override 9 public void run() { 10 Log.i("songzheweiwang", "thread2=" + Thread.currentThread()); 11 } 12 }); 13 Looper.loop(); 14 } 15 }).start(); 16 }
在主线程中调用该方法,得到的log如下,可见回调方法是在当前子线程中:
1 08-31 12:53:49.718 9655-9655/com.example.demos I/songzheweiwang: thread1=Thread[main,5,main]
2 08-31 12:53:49.719 9655-9750/com.example.demos I/songzheweiwang: thread2=Thread[Thread-7,5,main]
上述示例采用的是post方式,sendMessage方式也是一样的结果,这里就不举例了。我们平始使用Handler多数情况下是在主线程中new Handler的,默认情况下使用的是main Looper,然后在子线程中用该Handler实例来post或者sendMessage,所以默认情况下回调方法就是运行在主线程中,我们在该方法中访问UI就没有报错。
3、消息循环Looper
上一节示例中,第6行和13行红色部分展示了在子线程中,Handler使用子线程Looper的使用范例。如果去掉这两行代码,系统会报如下异常:
Looper.loop(),就是用来开启线程的消息循环,没有该行代码就无法收到消息,其作用在上一篇文章中说过,这里不赘述了。Looper.prepare(),是获取当前线程的Looper,如果没有Looper会报上述异常,源码如下所示:
1 public Handler(Callback callback, boolean async) { 2 ...... 3 mLooper = Looper.myLooper(); 4 if (mLooper == null) { 5 throw new RuntimeException( 6 "Can't create handler inside thread that has not called Looper.prepare()"); 7 } 8 ...... 9 }
Looper.myLooper()方法用于获取当前线程中的Looper,如果当前线程下没有Looper,就会报异常。每一个线程下都有自己专属的Looper,由TheadLocal来进行管理,至于ThreadLocal,有兴趣的可以自行研究。
我们是否会有疑问,平时在new Handler的时候,也没有去调用Looper.prepare()方法,为什么没有报错呢?
这是因为咱们new Handler的时候经常就是在主线程中完成的,会默认使用主线程的Looper。我们平时经常提到主线程,其实就是AcitivityThread,是在Zygote创建应用程序进程时调用其main方法启动的。主线程启动的时候会创建自己的Looper,源码如下:
1 public static void main(String[] args) { 2 ...... 3 Looper.prepareMainLooper(); 4 5 ActivityThread thread = new ActivityThread(); 6 thread.attach(false); 7 8 if (sMainThreadHandler == null) { 9 sMainThreadHandler = thread.getHandler(); 10 } 11 ...... 12 Looper.loop(); 13 ..... 14 }
Looper.prepareMainLooper()里面也是调用的Looper.prepare()方法,所以在当前应用进程创建的时候,系统就为该进程的主线程创建好了Looper,后续在主线程中实例化Handler的时候,就默认为使用该Looper了。
4、一些Tip
(1)当有Handler发送Message时,会通过MessageQueue类的enqueueMessage方法将Message加入到Handler内部的MessageQueue中,我们一般称之为消息队列。但实际上它的数据结构为单链表,而不是队列,因为队列是先进先出,中间不能插入和删除元素,但是单链表可以。Message会根据post/sendMessage时指定处理的时间来在插入到链表中,或者通过quit方法将消息从链表中移除。
(2)取Message的方法MessageQueue.next()方法是个死循环,没有消息时会阻塞。Looper.loop()也是个死循环,而且调用了MessageQueue.next()方法,当MessageQueue没有消息时也会阻塞,而当有消息加入时就会立即处理。让这两个死循环终止的唯一条件就是Looper执行quit/quitSafety方法让自己退出,这样会将消息队列标记为退出状态,否者会一直死循环下去。
(3)在执行Looper.loop()的时候,会判断当前线程是否已经创建过Looper了,如果已经创建过,就会报异常,如下源码说明了这一点:
1 public static void prepare() { 2 prepare(true); 3 } 4 private static void prepare(boolean quitAllowed) { 5 if (sThreadLocal.get() != null) { 6 throw new RuntimeException("Only one Looper may be created per thread"); 7 } 8 sThreadLocal.set(new Looper(quitAllowed)); 9 }
通过这种方式,就保证了一个线程中,只会实例Looper一次,也就是只会有一个Looper实例。
1 private Looper(boolean quitAllowed) { 2 mQueue = new MessageQueue(quitAllowed); 3 mThread = Thread.currentThread(); 4 }
实例Looper的时候,会新建一个MessageQueue,那么一个线程中就只会维护一个MessageQueue。
结语
目前先总结到这里,后续会对文中提到的子线程问题,内存泄漏等问题展开,或单独成文,或在本文中补充。笔者知识有限,有很多不足之处,请不吝赐教。也感谢您的阅读,希望能对读者有一定的帮助,即便读者可能很少很少,谢谢!!!